From 5ae3acf6ff4d68379d9ea73c6ec90b8dfa69c6ad Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 12 Jan 2021 22:47:07 -0500 Subject: Anki note data abstraction (#1228) * Create AnkiNoteData * Create AnkiNoteDataDefinitionProxyHandler * Update media injection * Create AnkiNoteDataDefinitionSecondaryProperties * Update note context format * Expose url and cloze on definition * Simplify for understandability * Remove unused _createNoteData * Update public object * Remove trims on sentence, since it should already be trimmed * Fix unused global --- ext/bg/js/anki-note-builder.js | 42 ++---- ext/bg/js/anki-note-data.js | 240 ++++++++++++++++++++++++++++++ ext/bg/js/template-renderer-frame-main.js | 6 +- ext/bg/template-renderer.html | 2 + 4 files changed, 254 insertions(+), 36 deletions(-) create mode 100644 ext/bg/js/anki-note-data.js (limited to 'ext/bg') 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 @@ + + -- cgit v1.2.3