diff --git a/.eslintrc.json b/.eslintrc.json index 94551803..0f854e76 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -96,6 +96,7 @@ "excludedFiles": [ "ext/mixed/js/core.js", "ext/bg/js/template-renderer.js", + "ext/bg/js/anki-note-data.js", "ext/mixed/js/dictionary-data-util.js" ], "globals": { diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 632d9f8a..eae5fbe4 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -16,7 +16,6 @@ */ /* global - * DictionaryDataUtil * TemplateRendererProxy */ @@ -35,6 +34,7 @@ class AnkiNoteBuilder { modelName, fields, tags=[], + injectedMedia=null, checkForDuplicates=true, duplicateScope='collection', resultOutputMode='split', @@ -50,7 +50,15 @@ class AnkiNoteBuilder { duplicateScopeCheckChildren = true; } - const data = this._createNoteData(definition, mode, context, resultOutputMode, glossaryLayoutMode, compactTags); + const data = { + definition, + mode, + context, + resultOutputMode, + glossaryLayoutMode, + compactTags, + injectedMedia + }; const formattedFieldValuePromises = []; for (const [, fieldValue] of fields) { const formattedFieldValuePromise = this._formatField(fieldValue, data, templates, errors); @@ -104,36 +112,6 @@ class AnkiNoteBuilder { // Private - _createNoteData(definition, mode, context, resultOutputMode, glossaryLayoutMode, compactTags) { - const pitches = DictionaryDataUtil.getPitchAccentInfos(definition); - const pitchCount = pitches.reduce((i, v) => i + v.pitches.length, 0); - const uniqueExpressions = new Set(); - const uniqueReadings = new Set(); - if (definition.type !== 'kanji') { - for (const {expression, reading} of definition.expressions) { - uniqueExpressions.add(expression); - uniqueReadings.add(reading); - } - } - return { - marker: null, - definition, - uniqueExpressions: [...uniqueExpressions], - uniqueReadings: [...uniqueReadings], - pitches, - pitchCount, - group: resultOutputMode === 'group', - merge: resultOutputMode === 'merge', - modeTermKanji: mode === 'term-kanji', - modeTermKana: mode === 'term-kana', - modeKanji: mode === 'kanji', - compactGlossaries: (glossaryLayoutMode === 'compact'), - glossaryLayoutMode, - compactTags, - context - }; - } - async _formatField(field, data, templates, errors=null) { return await this._stringReplaceAsync(field, this._markerPattern, async (g0, marker) => { try { diff --git a/ext/bg/js/anki-note-data.js b/ext/bg/js/anki-note-data.js new file mode 100644 index 00000000..a7d0f9f6 --- /dev/null +++ b/ext/bg/js/anki-note-data.js @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2021 Yomichan Authors + * + * 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 + * DictionaryDataUtil + */ + +/** + * This class represents the data that is exposed to the Anki template renderer. + * The public properties and data should be backwards compatible. + */ +class AnkiNoteData { + constructor({ + definition, + resultOutputMode, + mode, + glossaryLayoutMode, + compactTags, + context, + injectedMedia=null + }, marker) { + this._definition = definition; + this._resultOutputMode = resultOutputMode; + this._mode = mode; + this._glossaryLayoutMode = glossaryLayoutMode; + this._compactTags = compactTags; + this._context = context; + this._marker = marker; + this._injectedMedia = injectedMedia; + this._pitches = null; + this._pitchCount = null; + this._uniqueExpressions = null; + this._uniqueReadings = null; + this._publicContext = null; + this._cloze = null; + + this._prepareDefinition(definition, injectedMedia, context); + } + + get marker() { + return this._marker; + } + + set marker(value) { + this._marker = value; + } + + get definition() { + return this._definition; + } + + get uniqueExpressions() { + if (this._uniqueExpressions === null) { + this._uniqueExpressions = this._getUniqueExpressions(); + } + return this._uniqueExpressions; + } + + get uniqueReadings() { + if (this._uniqueReadings === null) { + this._uniqueReadings = this._getUniqueReadings(); + } + return this._uniqueReadings; + } + + get pitches() { + if (this._pitches === null) { + this._pitches = DictionaryDataUtil.getPitchAccentInfos(this._definition); + } + return this._pitches; + } + + get pitchCount() { + if (this._pitchCount === null) { + this._pitchCount = this.pitches.reduce((i, v) => i + v.pitches.length, 0); + } + return this._pitchCount; + } + + get group() { + return this._resultOutputMode === 'group'; + } + + get merge() { + return this._resultOutputMode === 'merge'; + } + + get modeTermKanji() { + return this._mode === 'term-kanji'; + } + + get modeTermKana() { + return this._mode === 'term-kana'; + } + + get modeKanji() { + return this._mode === 'kanji'; + } + + get compactGlossaries() { + return this._glossaryLayoutMode === 'compact'; + } + + get glossaryLayoutMode() { + return this._glossaryLayoutMode; + } + + get compactTags() { + return this._compactTags; + } + + get context() { + if (this._publicContext === null) { + this._publicContext = this._getPublicContext(); + } + return this._publicContext; + } + + createPublic() { + const self = this; + return { + get marker() { return self.marker; }, + set marker(value) { self.marker = value; }, + get definition() { return self.definition; }, + get glossaryLayoutMode() { return self.glossaryLayoutMode; }, + get compactTags() { return self.compactTags; }, + get group() { return self.group; }, + get merge() { return self.merge; }, + get modeTermKanji() { return self.modeTermKanji; }, + get modeTermKana() { return self.modeTermKana; }, + get modeKanji() { return self.modeKanji; }, + get compactGlossaries() { return self.compactGlossaries; }, + get uniqueExpressions() { return self.uniqueExpressions; }, + get uniqueReadings() { return self.uniqueReadings; }, + get pitches() { return self.pitches; }, + get pitchCount() { return self.pitchCount; }, + get context() { return self.context; } + }; + } + + // Private + + _asObject(value) { + return (typeof value === 'object' && value !== null ? value : {}); + } + + _getUniqueExpressions() { + const results = new Set(); + const definition = this._definition; + if (definition.type !== 'kanji') { + for (const {expression} of definition.expressions) { + results.add(expression); + } + } + return [...results]; + } + + _getUniqueReadings() { + const results = new Set(); + const definition = this._definition; + if (definition.type !== 'kanji') { + for (const {reading} of definition.expressions) { + results.add(reading); + } + } + return [...results]; + } + + _getPublicContext() { + let {documentTitle} = this._asObject(this._context); + if (typeof documentTitle !== 'string') { documentTitle = ''; } + + return { + document: { + title: documentTitle + } + }; + } + + _getCloze() { + const {sentence} = this._asObject(this._context); + let {text, offset} = this._asObject(sentence); + if (typeof text !== 'string') { text = ''; } + if (typeof offset !== 'number') { offset = 0; } + + const definition = this._definition; + const source = definition.type === 'kanji' ? definition.character : definition.rawSource; + + return { + sentence: text, + prefix: text.substring(0, offset), + body: text.substring(offset, offset + source.length), + suffix: text.substring(offset + source.length) + }; + } + + _getClozeCached() { + if (this._cloze === null) { + this._cloze = this._getCloze(); + } + return this._cloze; + } + + _prepareDefinition(definition, injectedMedia, context) { + const { + screenshotFileName=null, + clipboardImageFileName=null, + clipboardText=null, + audioFileName=null + } = this._asObject(injectedMedia); + + let {url} = this._asObject(context); + if (typeof url !== 'string') { url = ''; } + + definition.screenshotFileName = screenshotFileName; + definition.clipboardImageFileName = clipboardImageFileName; + definition.clipboardText = clipboardText; + definition.audioFileName = audioFileName; + definition.url = url; + Object.defineProperty(definition, 'cloze', { + configurable: true, + enumerable: true, + get: this._getClozeCached.bind(this) + }); + } +} diff --git a/ext/bg/js/template-renderer-frame-main.js b/ext/bg/js/template-renderer-frame-main.js index 92a8095e..d25eb56d 100644 --- a/ext/bg/js/template-renderer-frame-main.js +++ b/ext/bg/js/template-renderer-frame-main.js @@ -16,6 +16,7 @@ */ /* globals + * AnkiNoteData * JapaneseUtil * TemplateRenderer * TemplateRendererFrameApi @@ -25,10 +26,7 @@ const japaneseUtil = new JapaneseUtil(null); const templateRenderer = new TemplateRenderer(japaneseUtil); templateRenderer.registerDataType('ankiNote', { - modifier: ({data, marker}) => { - data.marker = marker; - return data; - } + modifier: ({data, marker}) => new AnkiNoteData(data, marker).createPublic() }); const api = new TemplateRendererFrameApi(templateRenderer); api.prepare(); diff --git a/ext/bg/template-renderer.html b/ext/bg/template-renderer.html index c58e604c..a44205ce 100644 --- a/ext/bg/template-renderer.html +++ b/ext/bg/template-renderer.html @@ -14,7 +14,9 @@ + + diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index a76e2b71..6a1ebba6 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -915,18 +915,14 @@ class Display extends EventDispatcher { focusEntry=null, scrollX=null, scrollY=null, - optionsContext=null, - sentence=null, - url + optionsContext=null } = state; if (typeof focusEntry !== 'number') { focusEntry = 0; } - if (typeof url !== 'string') { url = window.location.href; } if (!(typeof optionsContext === 'object' && optionsContext !== null)) { optionsContext = this.getOptionsContext(); state.optionsContext = optionsContext; changeHistory = true; } - sentence = this._getValidSentenceData(sentence); this._setFullQuery(queryFull); this._setTitleText(query); @@ -957,11 +953,6 @@ class Display extends EventDispatcher { this._definitions = definitions; - for (const definition of definitions) { - definition.cloze = this._clozeBuild(sentence, isTerms ? definition.rawSource : definition.character); - definition.url = url; - } - this._updateNavigation(this._history.hasPrevious(), this._history.hasNext()); this._setNoContentVisible(definitions.length === 0); @@ -1318,15 +1309,6 @@ class Display extends EventDispatcher { return {text, offset}; } - _clozeBuild({text, offset}, source) { - return { - sentence: text.trim(), - prefix: text.substring(0, offset).trim(), - body: text.substring(offset, offset + source.length), - suffix: text.substring(offset + source.length).trim() - }; - } - _getClosestDefinitionIndex(element) { return this._getClosestIndex(element, '.entry'); } @@ -1382,17 +1364,18 @@ class Display extends EventDispatcher { _getNoteContext() { const {state} = this._history; - let documentTitle = null; - if (typeof state === 'object' && state !== null) { - ({documentTitle} = state); - } + let {documentTitle, url, sentence} = (isObject(state) ? state : {}); if (typeof documentTitle !== 'string') { documentTitle = ''; } + if (typeof url !== 'string') { + url = window.location.href; + } + sentence = this._getValidSentenceData(sentence); return { - document: { - title: documentTitle - } + url, + sentence, + documentTitle }; } @@ -1534,9 +1517,7 @@ class Display extends EventDispatcher { const {deck: deckName, model: modelName} = modeOptions; const fields = Object.entries(modeOptions.fields); - if (injectMedia) { - await this._injectAnkiNoteMedia(definition, mode, options, fields); - } + const injectedMedia = (injectMedia ? await this._injectAnkiNoteMedia(definition, mode, options, fields) : null); return await this._ankiNoteBuilder.createNote({ definition, @@ -1551,7 +1532,8 @@ class Display extends EventDispatcher { duplicateScope, resultOutputMode, glossaryLayoutMode, - compactTags + compactTags, + injectedMedia }); } @@ -1570,17 +1552,13 @@ class Display extends EventDispatcher { image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'), text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text') }; - const {screenshotFileName, clipboardImageFileName, clipboardText, audioFileName} = await api.injectAnkiNoteMedia( + return await api.injectAnkiNoteMedia( timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails ); - if (screenshotFileName !== null) { definition.screenshotFileName = screenshotFileName; } - if (clipboardImageFileName !== null) { definition.clipboardImageFileName = clipboardImageFileName; } - if (audioFileName !== null) { definition.audioFileName = audioFileName; } - if (clipboardText !== null) { definition.clipboardText = clipboardText; } } _getDefinitionDetailsForNote(definition) {