From 69cce49b0d5d9f11f4ffb529ae3d060536297c07 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 7 Mar 2020 15:14:05 -0500 Subject: [PATCH 1/5] Move Anki note generation functionality into a new class --- ext/bg/background.html | 1 + ext/bg/js/anki-note-builder.js | 110 +++++++++++++++++++++++++++ ext/bg/js/backend.js | 9 ++- ext/bg/js/dictionary.js | 88 --------------------- ext/bg/js/settings/anki-templates.js | 8 +- ext/bg/settings.html | 1 + 6 files changed, 122 insertions(+), 95 deletions(-) create mode 100644 ext/bg/js/anki-note-builder.js diff --git a/ext/bg/background.html b/ext/bg/background.html index f2f70d4d..8db017f1 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -22,6 +22,7 @@ + diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js new file mode 100644 index 00000000..f7555280 --- /dev/null +++ b/ext/bg/js/anki-note-builder.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 Alex Yatskov + * Author: Alex Yatskov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/*global apiTemplateRender*/ + +class AnkiNoteBuilder { + constructor() { + this._markers = new Set([ + 'audio', + 'character', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'dictionary', + 'expression', + 'furigana', + 'furigana-plain', + 'glossary', + 'glossary-brief', + 'kunyomi', + 'onyomi', + 'reading', + 'screenshot', + 'sentence', + 'tags', + 'url' + ]); + } + + async createNote(definition, mode, options, templates) { + const isKanji = (mode === 'kanji'); + const tags = options.anki.tags; + const modeOptions = isKanji ? options.anki.kanji : options.anki.terms; + const modeOptionsFieldEntries = Object.entries(modeOptions.fields); + + const note = { + fields: {}, + tags, + deckName: modeOptions.deck, + modelName: modeOptions.model + }; + + for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { + note.fields[fieldName] = await this.formatField(fieldValue, definition, mode, options, templates, null); + } + + if (!isKanji && definition.audio) { + const audioFields = []; + + for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { + if (fieldValue.includes('{audio}')) { + audioFields.push(fieldName); + } + } + + if (audioFields.length > 0) { + note.audio = { + url: definition.audio.url, + filename: definition.audio.filename, + skipHash: '7e2c2f954ef6051373ba916f000168dc', // hash of audio data that should be skipped + fields: audioFields + }; + } + } + + return note; + } + + async formatField(field, definition, mode, options, templates, errors=null) { + const data = { + marker: null, + definition, + group: options.general.resultOutputMode === 'group', + merge: options.general.resultOutputMode === 'merge', + modeTermKanji: mode === 'term-kanji', + modeTermKana: mode === 'term-kana', + modeKanji: mode === 'kanji', + compactGlossaries: options.general.compactGlossaries + }; + const markers = this._markers; + const pattern = /\{([\w-]+)\}/g; + return await stringReplaceAsync(field, pattern, async (g0, marker) => { + if (!markers.has(marker)) { + return g0; + } + data.marker = marker; + try { + return await apiTemplateRender(templates, data); + } catch (e) { + if (errors) { errors.push(e); } + return `{${marker}-render-error}`; + } + }); + } +} diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 60a87916..929281da 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -20,10 +20,10 @@ conditionsTestValue, profileConditionsDescriptor handlebarsRenderDynamic requestText, requestJson, optionsLoad -dictConfigured, dictTermsSort, dictEnabledSet, dictNoteFormat +dictConfigured, dictTermsSort, dictEnabledSet audioGetUrl, audioInject jpConvertReading, jpDistributeFuriganaInflected, jpKatakanaToHiragana -AudioSystem, Translator, AnkiConnect, AnkiNull, Mecab, BackendApiForwarder, JsonSchema, ClipboardMonitor*/ +AnkiNoteBuilder, AudioSystem, Translator, AnkiConnect, AnkiNull, Mecab, BackendApiForwarder, JsonSchema, ClipboardMonitor*/ class Backend { constructor() { @@ -31,6 +31,7 @@ class Backend { this.anki = new AnkiNull(); this.mecab = new Mecab(); this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); + this.ankiNoteBuilder = new AnkiNoteBuilder(); this.options = null; this.optionsSchema = null; this.defaultAnkiFieldTemplates = null; @@ -450,7 +451,7 @@ class Backend { ); } - const note = await dictNoteFormat(definition, mode, options, templates); + const note = await this.ankiNoteBuilder.createNote(definition, mode, options, templates); return this.anki.addNote(note); } @@ -463,7 +464,7 @@ class Backend { const notes = []; for (const definition of definitions) { for (const mode of modes) { - const note = await dictNoteFormat(definition, mode, options, templates); + const note = await this.ankiNoteBuilder.createNote(definition, mode, options, templates); notes.push(note); } } diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index ffeac80a..3dd1d0c1 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -/*global apiTemplateRender*/ - function dictEnabledSet(options) { const enabledDictionaryMap = new Map(); for (const [title, {enabled, priority, allowSecondarySearches}] of Object.entries(options.dictionaries)) { @@ -333,89 +331,3 @@ function dictTagsSort(tags) { function dictFieldSplit(field) { return field.length === 0 ? [] : field.split(' '); } - -async function dictFieldFormat(field, definition, mode, options, templates, exceptions) { - const data = { - marker: null, - definition, - group: options.general.resultOutputMode === 'group', - merge: options.general.resultOutputMode === 'merge', - modeTermKanji: mode === 'term-kanji', - modeTermKana: mode === 'term-kana', - modeKanji: mode === 'kanji', - compactGlossaries: options.general.compactGlossaries - }; - const markers = dictFieldFormat.markers; - const pattern = /\{([\w-]+)\}/g; - return await stringReplaceAsync(field, pattern, async (g0, marker) => { - if (!markers.has(marker)) { - return g0; - } - data.marker = marker; - try { - return await apiTemplateRender(templates, data); - } catch (e) { - if (exceptions) { exceptions.push(e); } - return `{${marker}-render-error}`; - } - }); -} -dictFieldFormat.markers = new Set([ - 'audio', - 'character', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'dictionary', - 'expression', - 'furigana', - 'furigana-plain', - 'glossary', - 'glossary-brief', - 'kunyomi', - 'onyomi', - 'reading', - 'screenshot', - 'sentence', - 'tags', - 'url' -]); - -async function dictNoteFormat(definition, mode, options, templates) { - const isKanji = (mode === 'kanji'); - const tags = options.anki.tags; - const modeOptions = isKanji ? options.anki.kanji : options.anki.terms; - const modeOptionsFieldEntries = Object.entries(modeOptions.fields); - - const note = { - fields: {}, - tags, - deckName: modeOptions.deck, - modelName: modeOptions.model - }; - - for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { - note.fields[fieldName] = await dictFieldFormat(fieldValue, definition, mode, options, templates); - } - - if (!isKanji && definition.audio) { - const audioFields = []; - - for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { - if (fieldValue.includes('{audio}')) { - audioFields.push(fieldName); - } - } - - if (audioFields.length > 0) { - note.audio = { - url: definition.audio.url, - filename: definition.audio.filename, - skipHash: '7e2c2f954ef6051373ba916f000168dc', - fields: audioFields - }; - } - } - - return note; -} diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index 244ec42e..32a990f9 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -17,8 +17,9 @@ */ /*global getOptionsContext, getOptionsMutable, settingsSaveOptions -ankiGetFieldMarkers, ankiGetFieldMarkersHtml, dictFieldFormat -apiOptionsGet, apiTermsFind, apiGetDefaultAnkiFieldTemplates*/ +ankiGetFieldMarkers, ankiGetFieldMarkersHtml +apiOptionsGet, apiTermsFind, apiGetDefaultAnkiFieldTemplates, +AnkiNoteBuilder*/ function onAnkiFieldTemplatesReset(e) { e.preventDefault(); @@ -92,7 +93,8 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i const options = await apiOptionsGet(optionsContext); let templates = options.anki.fieldTemplates; if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); } - result = await dictFieldFormat(field, definition, mode, options, templates, exceptions); + const ankiNoteBuilder = new AnkiNoteBuilder(); + result = await ankiNoteBuilder.formatField(field, definition, mode, options, templates, exceptions); } } catch (e) { exceptions.push(e); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index e9fc6be5..0db76d71 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1090,6 +1090,7 @@ + From 7ac1c843a92cbefd0a625f06b5093217b585f7cf Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 7 Mar 2020 15:20:45 -0500 Subject: [PATCH 2/5] Use dependency injection for apiTemplateRender --- ext/bg/js/anki-note-builder.js | 7 +++---- ext/bg/js/api.js | 4 ---- ext/bg/js/backend.js | 8 ++++++-- ext/bg/js/settings/anki-templates.js | 4 ++-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index f7555280..be39ff43 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -16,10 +16,9 @@ * along with this program. If not, see . */ -/*global apiTemplateRender*/ - class AnkiNoteBuilder { - constructor() { + constructor({renderTemplate}) { + this._renderTemplate = renderTemplate; this._markers = new Set([ 'audio', 'character', @@ -100,7 +99,7 @@ class AnkiNoteBuilder { } data.marker = marker; try { - return await apiTemplateRender(templates, data); + return await this._renderTemplate(templates, data); } catch (e) { if (errors) { errors.push(e); } return `{${marker}-render-error}`; diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 4e5d81db..9a05023e 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -17,10 +17,6 @@ */ -function apiTemplateRender(template, data) { - return _apiInvoke('templateRender', {data, template}); -} - function _apiInvoke(action, params={}) { const data = {action, params}; return new Promise((resolve, reject) => { diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 929281da..6e5235ed 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -31,7 +31,7 @@ class Backend { this.anki = new AnkiNull(); this.mecab = new Mecab(); this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); - this.ankiNoteBuilder = new AnkiNoteBuilder(); + this.ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: this._renderTemplate.bind(this)}); this.options = null; this.optionsSchema = null; this.defaultAnkiFieldTemplates = null; @@ -507,7 +507,7 @@ class Backend { } async _onApiTemplateRender({template, data}) { - return handlebarsRenderDynamic(template, data); + return this._renderTemplate(template, data); } async _onApiCommandExec({command, params}) { @@ -811,6 +811,10 @@ class Backend { definition.screenshotFileName = filename; } + async _renderTemplate(template, data) { + return handlebarsRenderDynamic(template, data); + } + static _getTabUrl(tab) { return new Promise((resolve) => { chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => { diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index 32a990f9..b1665048 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -18,7 +18,7 @@ /*global getOptionsContext, getOptionsMutable, settingsSaveOptions ankiGetFieldMarkers, ankiGetFieldMarkersHtml -apiOptionsGet, apiTermsFind, apiGetDefaultAnkiFieldTemplates, +apiOptionsGet, apiTermsFind, apiGetDefaultAnkiFieldTemplates, apiTemplateRender AnkiNoteBuilder*/ function onAnkiFieldTemplatesReset(e) { @@ -93,7 +93,7 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i const options = await apiOptionsGet(optionsContext); let templates = options.anki.fieldTemplates; if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); } - const ankiNoteBuilder = new AnkiNoteBuilder(); + const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: apiTemplateRender}); result = await ankiNoteBuilder.formatField(field, definition, mode, options, templates, exceptions); } } catch (e) { From 8f9b6534c64dd871db92729714b46d400a52e30e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 7 Mar 2020 15:23:32 -0500 Subject: [PATCH 3/5] Move stringReplaceAsync It is only used in AnkiNoteBuilder and it was originally created for this purpose. --- .eslintrc.json | 1 - ext/bg/js/anki-note-builder.js | 17 ++++++++++++++++- ext/mixed/js/core.js | 15 --------------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 4ee1f982..2730acb5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -86,7 +86,6 @@ "toIterable": "readonly", "stringReverse": "readonly", "promiseTimeout": "readonly", - "stringReplaceAsync": "readonly", "parseUrl": "readonly", "EventDispatcher": "readonly", "EventListenerCollection": "readonly", diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index be39ff43..2a15b20d 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -93,7 +93,7 @@ class AnkiNoteBuilder { }; const markers = this._markers; const pattern = /\{([\w-]+)\}/g; - return await stringReplaceAsync(field, pattern, async (g0, marker) => { + return await AnkiNoteBuilder.stringReplaceAsync(field, pattern, async (g0, marker) => { if (!markers.has(marker)) { return g0; } @@ -106,4 +106,19 @@ class AnkiNoteBuilder { } }); } + + static stringReplaceAsync(str, regex, replacer) { + let match; + let index = 0; + const parts = []; + while ((match = regex.exec(str)) !== null) { + parts.push(str.substring(index, match.index), replacer(...match, match.index, str)); + index = regex.lastIndex; + } + if (parts.length === 0) { + return Promise.resolve(str); + } + parts.push(str.substring(index)); + return Promise.all(parts).then((v) => v.join('')); + } } diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 0e22b9ac..0d50e915 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -175,21 +175,6 @@ function promiseTimeout(delay, resolveValue) { return promise; } -function stringReplaceAsync(str, regex, replacer) { - let match; - let index = 0; - const parts = []; - while ((match = regex.exec(str)) !== null) { - parts.push(str.substring(index, match.index), replacer(...match, match.index, str)); - index = regex.lastIndex; - } - if (parts.length === 0) { - return Promise.resolve(str); - } - parts.push(str.substring(index)); - return Promise.all(parts).then((v) => v.join('')); -} - /* * Common events From 79eb4bdc167409cda6cebf3c0939189ab4cbbaa0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 7 Mar 2020 21:49:04 -0500 Subject: [PATCH 4/5] Remove bg/js/api.js --- ext/bg/background.html | 1 - ext/bg/js/api.js | 43 ------------------------------------------ 2 files changed, 44 deletions(-) delete mode 100644 ext/bg/js/api.js diff --git a/ext/bg/background.html b/ext/bg/background.html index 8db017f1..f6e00bf5 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -23,7 +23,6 @@ - diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js deleted file mode 100644 index 9a05023e..00000000 --- a/ext/bg/js/api.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2019-2020 Alex Yatskov - * Author: Alex Yatskov - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - - -function _apiInvoke(action, params={}) { - const data = {action, params}; - return new Promise((resolve, reject) => { - try { - const callback = (response) => { - if (response !== null && typeof response === 'object') { - if (typeof response.error !== 'undefined') { - reject(jsonToError(response.error)); - } else { - resolve(response.result); - } - } else { - const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`; - reject(new Error(`${message} (${JSON.stringify(data)})`)); - } - }; - const backend = window.yomichanBackend; - backend.onMessage({action, params}, null, callback); - } catch (e) { - reject(e); - yomichan.triggerOrphaned(e); - } - }); -} From 426c1534e7d740fa2c30488a64ad4fa6a382deed Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 7 Mar 2020 21:51:56 -0500 Subject: [PATCH 5/5] Remove marker restrictions --- ext/bg/js/anki-note-builder.js | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 2a15b20d..d0ff8205 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -19,26 +19,6 @@ class AnkiNoteBuilder { constructor({renderTemplate}) { this._renderTemplate = renderTemplate; - this._markers = new Set([ - 'audio', - 'character', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'dictionary', - 'expression', - 'furigana', - 'furigana-plain', - 'glossary', - 'glossary-brief', - 'kunyomi', - 'onyomi', - 'reading', - 'screenshot', - 'sentence', - 'tags', - 'url' - ]); } async createNote(definition, mode, options, templates) { @@ -91,12 +71,8 @@ class AnkiNoteBuilder { modeKanji: mode === 'kanji', compactGlossaries: options.general.compactGlossaries }; - const markers = this._markers; const pattern = /\{([\w-]+)\}/g; return await AnkiNoteBuilder.stringReplaceAsync(field, pattern, async (g0, marker) => { - if (!markers.has(marker)) { - return g0; - } data.marker = marker; try { return await this._renderTemplate(templates, data);