mirror of
https://github.com/philips/supernote-obsidian-plugin.git
synced 2026-07-22 09:50:25 +00:00
Merge pull request #62 from ssylvia/dictionary
Add custom dictionary to settings
This commit is contained in:
commit
95479ba6c7
4 changed files with 297 additions and 6 deletions
227
src/customDictionary.ts
Normal file
227
src/customDictionary.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import { setIcon, Setting } from "obsidian";
|
||||
import SupernotePlugin from "./main";
|
||||
|
||||
/** Custom dictionary entry type */
|
||||
type CustomDictionaryEntry = {
|
||||
/** The source text from Supernote .note file */
|
||||
source: string,
|
||||
/** String to replace if the source text is matched */
|
||||
replace: string,
|
||||
/** Whether the source text should be matched case-sensitively */
|
||||
matchCase?: boolean,
|
||||
/** Whether the source text should be matched as a whole word */
|
||||
matchWord?: boolean
|
||||
}
|
||||
|
||||
/** Settings for Supernote's plugin custom dictionary options */
|
||||
export interface CustomDictionarySettings {
|
||||
/** Whether the custom dictionary is enabled and text replacement should occur when extracting text to Obsidian */
|
||||
isCustomDictionaryEnabled: boolean,
|
||||
/** The custom dictionary entries */
|
||||
customDictionary: CustomDictionaryEntry[]
|
||||
}
|
||||
|
||||
export const CUSTOM_DICTIONARY_DEFAULT_SETTINGS: CustomDictionarySettings = {
|
||||
isCustomDictionaryEnabled: false,
|
||||
customDictionary: [],
|
||||
}
|
||||
|
||||
// Create the UI for a single dictionary entry
|
||||
function createDictionaryEntryUI({entry, tbody, plugin}: {entry: CustomDictionaryEntry, tbody: HTMLElement, plugin: SupernotePlugin}) {
|
||||
const tr = tbody.createEl('tr');
|
||||
|
||||
// Create source text input
|
||||
const sourceTd = tr.createEl('td');
|
||||
const sourceInput = sourceTd.createEl('input');
|
||||
sourceInput.type = 'text';
|
||||
sourceInput.value = entry.source;
|
||||
|
||||
// Create replace text input
|
||||
const replaceTd = tr.createEl('td');
|
||||
const replaceInput = replaceTd.createEl('input');
|
||||
replaceInput.type = 'text';
|
||||
replaceInput.value = entry.replace;
|
||||
|
||||
// Create dictionary entry options
|
||||
const optionsTd = tr.createEl('td');
|
||||
const matchCaseButton = optionsTd.createEl('button');
|
||||
matchCaseButton.ariaLabel = 'Match case';
|
||||
setIcon(matchCaseButton, 'case-sensitive');
|
||||
if (entry.matchCase !== false) {
|
||||
matchCaseButton.addClass('active');
|
||||
}
|
||||
const matchWordButton = optionsTd.createEl('button');
|
||||
matchWordButton.ariaLabel = 'Match whole word';
|
||||
setIcon(matchWordButton, 'whole-word');
|
||||
if (entry.matchWord !== false) {
|
||||
matchWordButton.addClass('active');
|
||||
}
|
||||
optionsTd.addClass('supernote-settings-custom-dictionary-entry-options');
|
||||
const deleteButton = optionsTd.createEl('button');
|
||||
deleteButton.ariaLabel = 'Delete dictionary entry';
|
||||
setIcon(deleteButton, 'trash-2');
|
||||
|
||||
// Update dictionary entry when input changes
|
||||
const updateDictionaryEntry = async () => {
|
||||
const index = Array.from(tbody.children).indexOf(tr)
|
||||
plugin.settings.customDictionary[index] = {
|
||||
source: sourceInput.value,
|
||||
replace: replaceInput.value,
|
||||
matchWord: matchWordButton.hasClass('active'),
|
||||
matchCase: matchCaseButton.hasClass('active'),
|
||||
};
|
||||
await plugin.saveSettings();
|
||||
}
|
||||
sourceInput.addEventListener('input', updateDictionaryEntry);
|
||||
replaceInput.addEventListener('input', updateDictionaryEntry);
|
||||
|
||||
// Toggle match case when match case button is clicked
|
||||
matchCaseButton.addEventListener('click', async () => {
|
||||
matchCaseButton.toggleClass('active', !matchCaseButton.hasClass('active'));
|
||||
await updateDictionaryEntry();
|
||||
});
|
||||
|
||||
// Toggle match word when match word button is clicked
|
||||
matchWordButton.addEventListener('click', async () => {
|
||||
matchWordButton.toggleClass('active', !matchWordButton.hasClass('active'));
|
||||
await updateDictionaryEntry();
|
||||
});
|
||||
|
||||
// Delete dictionary entry when delete button is clicked
|
||||
deleteButton.addEventListener('click', async () => {
|
||||
const index = Array.from(tbody.children).indexOf(tr);
|
||||
plugin.settings.customDictionary.splice(index, 1);
|
||||
await plugin.saveSettings();
|
||||
if (plugin.settings.customDictionary.length === 0) {
|
||||
sourceInput.value = '';
|
||||
replaceInput.value = '';
|
||||
} else {
|
||||
tr.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create the UI for the dictionary table
|
||||
function createDictionaryTableUI(containerEl: HTMLElement, plugin: SupernotePlugin) {
|
||||
const CONTAINER_CLASSNAME = 'supernote-settings-custom-dictionary-entries';
|
||||
|
||||
// Remove existing dictionary entries to prepare for re-render
|
||||
containerEl.find(`.${CONTAINER_CLASSNAME}`)?.remove();
|
||||
|
||||
// Create the dictionary entries container
|
||||
const dictionaryEntriesContainer = containerEl.createDiv();
|
||||
dictionaryEntriesContainer
|
||||
.addClasses(['setting-item', CONTAINER_CLASSNAME]);
|
||||
dictionaryEntriesContainer.createDiv({ text: 'Custom Dictionary' })
|
||||
.addClass('setting-item-name');
|
||||
dictionaryEntriesContainer.createDiv({ text: 'Add an entry for every text string you would like to replace from Supernote\'s recognized text. Note, all case-sensitive matches will run as a group before case-insensitive matches.' })
|
||||
.addClasses(['setting-item-description']);
|
||||
|
||||
// Create the dictionary entries table
|
||||
const table = dictionaryEntriesContainer.createEl('table');
|
||||
const thead = table.createEl('thead');
|
||||
const trHead = thead.createEl('tr');
|
||||
const tbody = table.createEl('tbody');
|
||||
|
||||
trHead.createEl('th', { text: 'Source Text' });
|
||||
trHead.createEl('th', { text: 'Replacement' });
|
||||
trHead.createEl('th', { text: 'Options' });
|
||||
|
||||
const addEmptyRow = () => {
|
||||
createDictionaryEntryUI({
|
||||
entry: {source: '', replace: ''},
|
||||
tbody,
|
||||
plugin
|
||||
});
|
||||
}
|
||||
|
||||
// Create UI for existing dictionary entries
|
||||
for (const entry of plugin.settings.customDictionary) {
|
||||
createDictionaryEntryUI({
|
||||
entry,
|
||||
tbody,
|
||||
plugin
|
||||
});
|
||||
}
|
||||
|
||||
// Add an empty row if there are no dictionary entries
|
||||
if (plugin.settings.customDictionary.length === 0) {
|
||||
addEmptyRow();
|
||||
}
|
||||
|
||||
// Create the "Add dictionary entry" button
|
||||
const addEntryButton = dictionaryEntriesContainer.createEl('button', { text: 'Add dictionary entry' });
|
||||
addEntryButton.addEventListener('click', addEmptyRow);
|
||||
}
|
||||
|
||||
// Create the UI for the custom dictionary settings
|
||||
export function createCustomDictionarySettingsUI(containerEl: HTMLElement, plugin: SupernotePlugin): void {
|
||||
const customDictionaryContainer = containerEl.createDiv();
|
||||
customDictionaryContainer
|
||||
.addClasses(['supernote-settings-custom-dictionary']);
|
||||
customDictionaryContainer.createEl('h3', { text: 'Custom Dictionary' })
|
||||
// .addClass('setting-item-name');
|
||||
customDictionaryContainer.createDiv({ text: 'You can add custom entries to your dictionary to fix errors from Supernote\'s handwriting recognition. This also lets you automatically swap out certain text with your preferred wording, add special markdown characters, etc.' })
|
||||
.addClasses(['setting-item-description', 'supernote-settings-custom-dictionary-subtitle']);
|
||||
|
||||
// Create the "Enable Custom Dictionary" setting
|
||||
new Setting(customDictionaryContainer)
|
||||
.setName('Enable Custom Dictionary')
|
||||
.setDesc('Enable or disable the custom dictionary.')
|
||||
.addToggle(text => text
|
||||
.setValue(plugin.settings.isCustomDictionaryEnabled)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.isCustomDictionaryEnabled = value;
|
||||
setCustomDictionaryEntriesDisabledState(customDictionaryContainer, plugin);
|
||||
await plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
// Create the dictionary entries setting
|
||||
createDictionaryTableUI(customDictionaryContainer, plugin);
|
||||
setCustomDictionaryEntriesDisabledState(customDictionaryContainer, plugin);
|
||||
}
|
||||
|
||||
// Toggle disabled state of custom dictionary entries based on the custom dictionary setting
|
||||
function setCustomDictionaryEntriesDisabledState(containerEl: HTMLElement, plugin: SupernotePlugin) {
|
||||
const customDictionaryContainer = containerEl.find('.supernote-settings-custom-dictionary-entries');
|
||||
if (plugin.settings.isCustomDictionaryEnabled) {
|
||||
customDictionaryContainer?.classList.remove('disabled');
|
||||
} else {
|
||||
customDictionaryContainer?.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Escape special characters in a string to be used in a regular expression
|
||||
function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
// Replace text with custom dictionary entries
|
||||
export function replaceTextWithCustomDictionary(text: string, customDictionary: CustomDictionarySettings['customDictionary']): string {
|
||||
let newText = text;
|
||||
|
||||
const [caseInsensitiveMatchers, caseSensitiveMatchers] = customDictionary.reduce((acc, entry) => {
|
||||
const source = entry.source;
|
||||
const matcher = entry.matchWord ? `\\b${escapeRegExp(source)}\\b` : escapeRegExp(source);
|
||||
if (entry.matchCase) {
|
||||
acc[1].push(matcher);
|
||||
} else {
|
||||
acc[0].push(matcher);
|
||||
}
|
||||
return acc;
|
||||
}, [[], []] as [string[], string[]]);
|
||||
|
||||
|
||||
newText = newText.replace(new RegExp(caseSensitiveMatchers.join('|'), 'g'), (match) => {
|
||||
const entry = customDictionary.find(entry => entry.source === match);
|
||||
return entry ? entry.replace : match;
|
||||
});
|
||||
|
||||
newText = newText.replace(new RegExp(caseInsensitiveMatchers.join('|'), 'gi'), (match) => {
|
||||
const entry = customDictionary.find(entry => entry.source.toLocaleUpperCase() === match.toLocaleUpperCase());
|
||||
return entry ? entry.replace : match;
|
||||
});
|
||||
|
||||
return newText;
|
||||
}
|
||||
24
src/main.ts
24
src/main.ts
|
|
@ -6,6 +6,7 @@ import { DownloadListModal, UploadListModal } from './FileListModal';
|
|||
import { jsPDF } from 'jspdf';
|
||||
import { SupernoteWorkerMessage, SupernoteWorkerResponse } from './myworker.worker';
|
||||
import Worker from 'myworker.worker';
|
||||
import { replaceTextWithCustomDictionary } from './customDictionary';
|
||||
|
||||
function generateTimestamp(): string {
|
||||
const date = new Date();
|
||||
|
|
@ -34,6 +35,21 @@ function dataUrlToBuffer(dataUrl: string): ArrayBuffer {
|
|||
return bytes.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the Supernote text based on the provided settings.
|
||||
*
|
||||
* @param text - The input text to be processed.
|
||||
* @param settings - The settings for the Supernote plugin.
|
||||
* @returns The processed text.
|
||||
*/
|
||||
function processSupernoteText(text: string, settings: SupernotePluginSettings): string {
|
||||
let processedText = text;
|
||||
if (settings.isCustomDictionaryEnabled) {
|
||||
processedText = replaceTextWithCustomDictionary(processedText, settings.customDictionary);
|
||||
}
|
||||
return processedText;
|
||||
}
|
||||
|
||||
export class WorkerPool {
|
||||
private workers: Worker[];
|
||||
|
||||
|
|
@ -146,7 +162,7 @@ class VaultWriter {
|
|||
for (let i = 0; i < sn.pages.length; i++) {
|
||||
content += `## Page ${i + 1}\n\n`
|
||||
if (sn.pages[i].text !== undefined && sn.pages[i].text.length > 0) {
|
||||
content += `${sn.pages[i].text}\n`;
|
||||
content += `${processSupernoteText(sn.pages[i].text, this.settings)}\n`;
|
||||
}
|
||||
if (imgs) {
|
||||
let subpath = '';
|
||||
|
|
@ -226,7 +242,7 @@ class VaultWriter {
|
|||
if (sn.pages[i].text !== undefined && sn.pages[i].text.length > 0) {
|
||||
pdf.setFontSize(100);
|
||||
pdf.setTextColor(0, 0, 0, 0); // Transparent text
|
||||
pdf.text(sn.pages[i].text, 20, 20, { maxWidth: sn.pageWidth });
|
||||
pdf.text(processSupernoteText(sn.pages[i].text, this.settings), 20, 20, { maxWidth: sn.pageWidth });
|
||||
pdf.setTextColor(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
|
|
@ -343,13 +359,13 @@ export class SupernoteView extends FileView {
|
|||
// If Collapse Text setting is enabled, place the text into an HTML `details` element
|
||||
if (this.settings.collapseRecognizedText) {
|
||||
text = pageContainer.createEl('details', {
|
||||
text: '\n' + sn.pages[i].text,
|
||||
text: '\n' + processSupernoteText(sn.pages[i].text,this.settings),
|
||||
cls: 'page-recognized-text',
|
||||
});
|
||||
text.createEl('summary', { text: `Page ${i + 1} Recognized Text` });
|
||||
} else {
|
||||
text = pageContainer.createEl('div', {
|
||||
text: sn.pages[i].text,
|
||||
text: processSupernoteText(sn.pages[i].text, this.settings),
|
||||
cls: 'page-recognized-text',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { createCustomDictionarySettingsUI, CUSTOM_DICTIONARY_DEFAULT_SETTINGS, CustomDictionarySettings } from "./customDictionary";
|
||||
import SupernotePlugin from "./main";
|
||||
import { App, ExtraButtonComponent, PluginSettingTab, Setting } from 'obsidian';
|
||||
|
||||
export const IP_VALIDATION_PATTERN = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/;
|
||||
|
||||
|
||||
export interface SupernotePluginSettings {
|
||||
export interface SupernotePluginSettings extends CustomDictionarySettings {
|
||||
directConnectIP: string;
|
||||
invertColorsWhenDark: boolean;
|
||||
showTOC: boolean;
|
||||
|
|
@ -20,6 +21,7 @@ export const DEFAULT_SETTINGS: SupernotePluginSettings = {
|
|||
showExportButtons: true,
|
||||
collapseRecognizedText: false,
|
||||
noteImageMaxDim: 800, // Sensible default for Nomad pages to be legible but not too big. Unit: px
|
||||
...CUSTOM_DICTIONARY_DEFAULT_SETTINGS,
|
||||
}
|
||||
|
||||
export class SupernoteSettingTab extends PluginSettingTab {
|
||||
|
|
@ -123,5 +125,8 @@ export class SupernoteSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
// Add custom dictionary settings to the settings tab
|
||||
createCustomDictionarySettingsUI(containerEl, this.plugin);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
styles.css
45
styles.css
|
|
@ -28,4 +28,47 @@ div.page-container {
|
|||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom dictionary settings styles */
|
||||
.supernote-settings-custom-dictionary-subtitle {
|
||||
padding: 0 0 0.75em;
|
||||
}
|
||||
|
||||
.supernote-settings-custom-dictionary-entries {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.supernote-settings-custom-dictionary-entries.disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.supernote-settings-custom-dictionary table {
|
||||
margin-block-start: var(--size-4-2);
|
||||
margin-block-end: var(--size-2-1);
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.supernote-settings-custom-dictionary table, .supernote-settings-custom-dictionary table input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.supernote-settings-custom-dictionary table th {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.supernote-settings-custom-dictionary-entry-options {
|
||||
display: grid;
|
||||
grid-gap: var(--size-4-1);
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
.supernote-settings-custom-dictionary button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.supernote-settings-custom-dictionary-entry-options button.active {
|
||||
color: var(--text-on-accent);
|
||||
--icon-color: var(--text-on-accent);
|
||||
background-color: var(--interactive-accent);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue