Move Anki note generation functionality into a new class

This commit is contained in:
toasted-nutbread 2020-03-07 15:14:05 -05:00
parent d022d61b1a
commit 69cce49b0d
6 changed files with 122 additions and 95 deletions

View File

@ -22,6 +22,7 @@
<script src="/mixed/js/dom.js"></script> <script src="/mixed/js/dom.js"></script>
<script src="/bg/js/anki.js"></script> <script src="/bg/js/anki.js"></script>
<script src="/bg/js/anki-note-builder.js"></script>
<script src="/bg/js/api.js"></script> <script src="/bg/js/api.js"></script>
<script src="/bg/js/mecab.js"></script> <script src="/bg/js/mecab.js"></script>
<script src="/bg/js/audio.js"></script> <script src="/bg/js/audio.js"></script>

View File

@ -0,0 +1,110 @@
/*
* Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* 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 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}`;
}
});
}
}

View File

@ -20,10 +20,10 @@
conditionsTestValue, profileConditionsDescriptor conditionsTestValue, profileConditionsDescriptor
handlebarsRenderDynamic handlebarsRenderDynamic
requestText, requestJson, optionsLoad requestText, requestJson, optionsLoad
dictConfigured, dictTermsSort, dictEnabledSet, dictNoteFormat dictConfigured, dictTermsSort, dictEnabledSet
audioGetUrl, audioInject audioGetUrl, audioInject
jpConvertReading, jpDistributeFuriganaInflected, jpKatakanaToHiragana jpConvertReading, jpDistributeFuriganaInflected, jpKatakanaToHiragana
AudioSystem, Translator, AnkiConnect, AnkiNull, Mecab, BackendApiForwarder, JsonSchema, ClipboardMonitor*/ AnkiNoteBuilder, AudioSystem, Translator, AnkiConnect, AnkiNull, Mecab, BackendApiForwarder, JsonSchema, ClipboardMonitor*/
class Backend { class Backend {
constructor() { constructor() {
@ -31,6 +31,7 @@ class Backend {
this.anki = new AnkiNull(); this.anki = new AnkiNull();
this.mecab = new Mecab(); this.mecab = new Mecab();
this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)});
this.ankiNoteBuilder = new AnkiNoteBuilder();
this.options = null; this.options = null;
this.optionsSchema = null; this.optionsSchema = null;
this.defaultAnkiFieldTemplates = 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); return this.anki.addNote(note);
} }
@ -463,7 +464,7 @@ class Backend {
const notes = []; const notes = [];
for (const definition of definitions) { for (const definition of definitions) {
for (const mode of modes) { 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); notes.push(note);
} }
} }

View File

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/*global apiTemplateRender*/
function dictEnabledSet(options) { function dictEnabledSet(options) {
const enabledDictionaryMap = new Map(); const enabledDictionaryMap = new Map();
for (const [title, {enabled, priority, allowSecondarySearches}] of Object.entries(options.dictionaries)) { for (const [title, {enabled, priority, allowSecondarySearches}] of Object.entries(options.dictionaries)) {
@ -333,89 +331,3 @@ function dictTagsSort(tags) {
function dictFieldSplit(field) { function dictFieldSplit(field) {
return field.length === 0 ? [] : field.split(' '); 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;
}

View File

@ -17,8 +17,9 @@
*/ */
/*global getOptionsContext, getOptionsMutable, settingsSaveOptions /*global getOptionsContext, getOptionsMutable, settingsSaveOptions
ankiGetFieldMarkers, ankiGetFieldMarkersHtml, dictFieldFormat ankiGetFieldMarkers, ankiGetFieldMarkersHtml
apiOptionsGet, apiTermsFind, apiGetDefaultAnkiFieldTemplates*/ apiOptionsGet, apiTermsFind, apiGetDefaultAnkiFieldTemplates,
AnkiNoteBuilder*/
function onAnkiFieldTemplatesReset(e) { function onAnkiFieldTemplatesReset(e) {
e.preventDefault(); e.preventDefault();
@ -92,7 +93,8 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i
const options = await apiOptionsGet(optionsContext); const options = await apiOptionsGet(optionsContext);
let templates = options.anki.fieldTemplates; let templates = options.anki.fieldTemplates;
if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); } 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) { } catch (e) {
exceptions.push(e); exceptions.push(e);

View File

@ -1090,6 +1090,7 @@
<script src="/mixed/js/api.js"></script> <script src="/mixed/js/api.js"></script>
<script src="/bg/js/anki.js"></script> <script src="/bg/js/anki.js"></script>
<script src="/bg/js/anki-note-builder.js"></script>
<script src="/bg/js/conditions.js"></script> <script src="/bg/js/conditions.js"></script>
<script src="/bg/js/dictionary.js"></script> <script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script> <script src="/bg/js/handlebars.js"></script>