Merge pull request #400 from toasted-nutbread/template-render-refactor

Template render refactor
This commit is contained in:
toasted-nutbread 2020-03-07 21:52:47 -05:00 committed by GitHub
commit a0d8caffb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 117 additions and 160 deletions

View File

@ -86,7 +86,6 @@
"toIterable": "readonly", "toIterable": "readonly",
"stringReverse": "readonly", "stringReverse": "readonly",
"promiseTimeout": "readonly", "promiseTimeout": "readonly",
"stringReplaceAsync": "readonly",
"parseUrl": "readonly", "parseUrl": "readonly",
"EventDispatcher": "readonly", "EventDispatcher": "readonly",
"EventListenerCollection": "readonly", "EventListenerCollection": "readonly",

View File

@ -22,7 +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/api.js"></script> <script src="/bg/js/anki-note-builder.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>
<script src="/bg/js/backend-api-forwarder.js"></script> <script src="/bg/js/backend-api-forwarder.js"></script>

View File

@ -0,0 +1,100 @@
/*
* 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/>.
*/
class AnkiNoteBuilder {
constructor({renderTemplate}) {
this._renderTemplate = renderTemplate;
}
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 pattern = /\{([\w-]+)\}/g;
return await AnkiNoteBuilder.stringReplaceAsync(field, pattern, async (g0, marker) => {
data.marker = marker;
try {
return await this._renderTemplate(templates, data);
} catch (e) {
if (errors) { errors.push(e); }
return `{${marker}-render-error}`;
}
});
}
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(''));
}
}

View File

@ -1,47 +0,0 @@
/*
* Copyright (C) 2019-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/>.
*/
function apiTemplateRender(template, data) {
return _apiInvoke('templateRender', {data, template});
}
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);
}
});
}

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({renderTemplate: this._renderTemplate.bind(this)});
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);
} }
} }
@ -506,7 +507,7 @@ class Backend {
} }
async _onApiTemplateRender({template, data}) { async _onApiTemplateRender({template, data}) {
return handlebarsRenderDynamic(template, data); return this._renderTemplate(template, data);
} }
async _onApiCommandExec({command, params}) { async _onApiCommandExec({command, params}) {
@ -810,6 +811,10 @@ class Backend {
definition.screenshotFileName = filename; definition.screenshotFileName = filename;
} }
async _renderTemplate(template, data) {
return handlebarsRenderDynamic(template, data);
}
static _getTabUrl(tab) { static _getTabUrl(tab) {
return new Promise((resolve) => { return new Promise((resolve) => {
chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => { chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => {

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, apiTemplateRender
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({renderTemplate: apiTemplateRender});
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>

View File

@ -175,21 +175,6 @@ function promiseTimeout(delay, resolveValue) {
return promise; 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 * Common events