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
This commit is contained in:
toasted-nutbread 2021-01-12 22:47:07 -05:00 committed by GitHub
parent b7c9fa1057
commit 5ae3acf6ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 268 additions and 71 deletions

View File

@ -96,6 +96,7 @@
"excludedFiles": [ "excludedFiles": [
"ext/mixed/js/core.js", "ext/mixed/js/core.js",
"ext/bg/js/template-renderer.js", "ext/bg/js/template-renderer.js",
"ext/bg/js/anki-note-data.js",
"ext/mixed/js/dictionary-data-util.js" "ext/mixed/js/dictionary-data-util.js"
], ],
"globals": { "globals": {

View File

@ -16,7 +16,6 @@
*/ */
/* global /* global
* DictionaryDataUtil
* TemplateRendererProxy * TemplateRendererProxy
*/ */
@ -35,6 +34,7 @@ class AnkiNoteBuilder {
modelName, modelName,
fields, fields,
tags=[], tags=[],
injectedMedia=null,
checkForDuplicates=true, checkForDuplicates=true,
duplicateScope='collection', duplicateScope='collection',
resultOutputMode='split', resultOutputMode='split',
@ -50,7 +50,15 @@ class AnkiNoteBuilder {
duplicateScopeCheckChildren = true; duplicateScopeCheckChildren = true;
} }
const data = this._createNoteData(definition, mode, context, resultOutputMode, glossaryLayoutMode, compactTags); const data = {
definition,
mode,
context,
resultOutputMode,
glossaryLayoutMode,
compactTags,
injectedMedia
};
const formattedFieldValuePromises = []; const formattedFieldValuePromises = [];
for (const [, fieldValue] of fields) { for (const [, fieldValue] of fields) {
const formattedFieldValuePromise = this._formatField(fieldValue, data, templates, errors); const formattedFieldValuePromise = this._formatField(fieldValue, data, templates, errors);
@ -104,36 +112,6 @@ class AnkiNoteBuilder {
// Private // 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) { async _formatField(field, data, templates, errors=null) {
return await this._stringReplaceAsync(field, this._markerPattern, async (g0, marker) => { return await this._stringReplaceAsync(field, this._markerPattern, async (g0, marker) => {
try { try {

240
ext/bg/js/anki-note-data.js Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
/* 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)
});
}
}

View File

@ -16,6 +16,7 @@
*/ */
/* globals /* globals
* AnkiNoteData
* JapaneseUtil * JapaneseUtil
* TemplateRenderer * TemplateRenderer
* TemplateRendererFrameApi * TemplateRendererFrameApi
@ -25,10 +26,7 @@
const japaneseUtil = new JapaneseUtil(null); const japaneseUtil = new JapaneseUtil(null);
const templateRenderer = new TemplateRenderer(japaneseUtil); const templateRenderer = new TemplateRenderer(japaneseUtil);
templateRenderer.registerDataType('ankiNote', { templateRenderer.registerDataType('ankiNote', {
modifier: ({data, marker}) => { modifier: ({data, marker}) => new AnkiNoteData(data, marker).createPublic()
data.marker = marker;
return data;
}
}); });
const api = new TemplateRendererFrameApi(templateRenderer); const api = new TemplateRendererFrameApi(templateRenderer);
api.prepare(); api.prepare();

View File

@ -14,7 +14,9 @@
</head> </head>
<body> <body>
<script src="/mixed/lib/handlebars.min.js"></script> <script src="/mixed/lib/handlebars.min.js"></script>
<script src="/mixed/js/dictionary-data-util.js"></script>
<script src="/mixed/js/japanese.js"></script> <script src="/mixed/js/japanese.js"></script>
<script src="/bg/js/anki-note-data.js"></script>
<script src="/bg/js/template-renderer.js"></script> <script src="/bg/js/template-renderer.js"></script>
<script src="/bg/js/template-renderer-frame-api.js"></script> <script src="/bg/js/template-renderer-frame-api.js"></script>
<script src="/bg/js/template-renderer-frame-main.js"></script> <script src="/bg/js/template-renderer-frame-main.js"></script>

View File

@ -915,18 +915,14 @@ class Display extends EventDispatcher {
focusEntry=null, focusEntry=null,
scrollX=null, scrollX=null,
scrollY=null, scrollY=null,
optionsContext=null, optionsContext=null
sentence=null,
url
} = state; } = state;
if (typeof focusEntry !== 'number') { focusEntry = 0; } if (typeof focusEntry !== 'number') { focusEntry = 0; }
if (typeof url !== 'string') { url = window.location.href; }
if (!(typeof optionsContext === 'object' && optionsContext !== null)) { if (!(typeof optionsContext === 'object' && optionsContext !== null)) {
optionsContext = this.getOptionsContext(); optionsContext = this.getOptionsContext();
state.optionsContext = optionsContext; state.optionsContext = optionsContext;
changeHistory = true; changeHistory = true;
} }
sentence = this._getValidSentenceData(sentence);
this._setFullQuery(queryFull); this._setFullQuery(queryFull);
this._setTitleText(query); this._setTitleText(query);
@ -957,11 +953,6 @@ class Display extends EventDispatcher {
this._definitions = definitions; 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._updateNavigation(this._history.hasPrevious(), this._history.hasNext());
this._setNoContentVisible(definitions.length === 0); this._setNoContentVisible(definitions.length === 0);
@ -1318,15 +1309,6 @@ class Display extends EventDispatcher {
return {text, offset}; 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) { _getClosestDefinitionIndex(element) {
return this._getClosestIndex(element, '.entry'); return this._getClosestIndex(element, '.entry');
} }
@ -1382,17 +1364,18 @@ class Display extends EventDispatcher {
_getNoteContext() { _getNoteContext() {
const {state} = this._history; const {state} = this._history;
let documentTitle = null; let {documentTitle, url, sentence} = (isObject(state) ? state : {});
if (typeof state === 'object' && state !== null) {
({documentTitle} = state);
}
if (typeof documentTitle !== 'string') { if (typeof documentTitle !== 'string') {
documentTitle = ''; documentTitle = '';
} }
return { if (typeof url !== 'string') {
document: { url = window.location.href;
title: documentTitle
} }
sentence = this._getValidSentenceData(sentence);
return {
url,
sentence,
documentTitle
}; };
} }
@ -1534,9 +1517,7 @@ class Display extends EventDispatcher {
const {deck: deckName, model: modelName} = modeOptions; const {deck: deckName, model: modelName} = modeOptions;
const fields = Object.entries(modeOptions.fields); const fields = Object.entries(modeOptions.fields);
if (injectMedia) { const injectedMedia = (injectMedia ? await this._injectAnkiNoteMedia(definition, mode, options, fields) : null);
await this._injectAnkiNoteMedia(definition, mode, options, fields);
}
return await this._ankiNoteBuilder.createNote({ return await this._ankiNoteBuilder.createNote({
definition, definition,
@ -1551,7 +1532,8 @@ class Display extends EventDispatcher {
duplicateScope, duplicateScope,
resultOutputMode, resultOutputMode,
glossaryLayoutMode, glossaryLayoutMode,
compactTags compactTags,
injectedMedia
}); });
} }
@ -1570,17 +1552,13 @@ class Display extends EventDispatcher {
image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'), image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'),
text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text') text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text')
}; };
const {screenshotFileName, clipboardImageFileName, clipboardText, audioFileName} = await api.injectAnkiNoteMedia( return await api.injectAnkiNoteMedia(
timestamp, timestamp,
definitionDetails, definitionDetails,
audioDetails, audioDetails,
screenshotDetails, screenshotDetails,
clipboardDetails 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) { _getDefinitionDetailsForNote(definition) {