Refactor template rendering (#1583)

* Update _errorToJson to _serializeError

* Remove async

* Refactor render

* Simplify _getModifiedData

* Rename data => commonData

* Rename templates => template for consistency

* Improve errors check

* Update tests
This commit is contained in:
toasted-nutbread 2021-04-02 12:42:06 -04:00 committed by GitHub
parent 36b7e34cce
commit 8179846e38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 53 additions and 48 deletions

View File

@ -30,7 +30,7 @@ class AnkiNoteBuilder {
definition, definition,
mode, mode,
context, context,
templates, template,
deckName, deckName,
modelName, modelName,
fields, fields,
@ -51,10 +51,10 @@ class AnkiNoteBuilder {
duplicateScopeCheckChildren = true; duplicateScopeCheckChildren = true;
} }
const data = this._createData(definition, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, injectedMedia); const commonData = this._createData(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, commonData, template, errors);
formattedFieldValuePromises.push(formattedFieldValuePromise); formattedFieldValuePromises.push(formattedFieldValuePromise);
} }
@ -110,12 +110,12 @@ class AnkiNoteBuilder {
}; };
} }
async _formatField(field, data, templates, errors=null) { async _formatField(field, commonData, template, errors=null) {
return await this._stringReplaceAsync(field, this._markerPattern, async (g0, marker) => { return await this._stringReplaceAsync(field, this._markerPattern, async (g0, marker) => {
try { try {
return await this._renderTemplate(templates, data, marker); return await this._renderTemplate(template, marker, commonData);
} catch (e) { } catch (e) {
if (errors) { if (Array.isArray(errors)) {
const error = new Error(`Template render error for {${marker}}`); const error = new Error(`Template render error for {${marker}}`);
error.data = {error: e}; error.data = {error: e};
errors.push(error); errors.push(error);
@ -140,7 +140,7 @@ class AnkiNoteBuilder {
return (await Promise.all(parts)).join(''); return (await Promise.all(parts)).join('');
} }
async _renderTemplate(template, data, marker) { async _renderTemplate(template, marker, commonData) {
return await this._templateRenderer.render(template, {data, marker}, 'ankiNote'); return await this._templateRenderer.render(template, {marker, commonData}, 'ankiNote');
} }
} }

View File

@ -1463,7 +1463,7 @@ class Display extends EventDispatcher {
async _createNote(definition, mode, context, injectMedia, errors) { async _createNote(definition, mode, context, injectMedia, errors) {
const options = this._options; const options = this._options;
const templates = this._ankiFieldTemplates; const template = this._ankiFieldTemplates;
const { const {
general: {resultOutputMode, glossaryLayoutMode, compactTags}, general: {resultOutputMode, glossaryLayoutMode, compactTags},
anki: ankiOptions anki: ankiOptions
@ -1488,7 +1488,7 @@ class Display extends EventDispatcher {
definition, definition,
mode, mode,
context, context,
templates, template,
deckName, deckName,
modelName, modelName,
fields, fields,

View File

@ -176,14 +176,14 @@ class AnkiTemplatesController {
sentence: {text: definition.rawSource, offset: 0}, sentence: {text: definition.rawSource, offset: 0},
documentTitle: document.title documentTitle: document.title
}; };
let templates = options.anki.fieldTemplates; let template = options.anki.fieldTemplates;
if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } if (typeof template !== 'string') { template = this._defaultFieldTemplates; }
const {general: {resultOutputMode, glossaryLayoutMode, compactTags}} = options; const {general: {resultOutputMode, glossaryLayoutMode, compactTags}} = options;
const note = await this._ankiNoteBuilder.createNote({ const note = await this._ankiNoteBuilder.createNote({
definition, definition,
mode, mode,
context, context,
templates, template,
deckName: '', deckName: '',
modelName: '', modelName: '',
fields: [ fields: [

View File

@ -19,8 +19,8 @@ class TemplateRendererFrameApi {
constructor(templateRenderer) { constructor(templateRenderer) {
this._templateRenderer = templateRenderer; this._templateRenderer = templateRenderer;
this._windowMessageHandlers = new Map([ this._windowMessageHandlers = new Map([
['render', {async: true, handler: this._onRender.bind(this)}], ['render', {async: false, handler: this._onRender.bind(this)}],
['getModifiedData', {async: true, handler: this._onGetModifiedData.bind(this)}] ['getModifiedData', {async: false, handler: this._onGetModifiedData.bind(this)}]
]); ]);
} }
@ -47,25 +47,25 @@ class TemplateRendererFrameApi {
} }
response = {result}; response = {result};
} catch (error) { } catch (error) {
response = {error: this._errorToJson(error)}; response = {error: this._serializeError(error)};
} }
if (typeof id === 'undefined') { return; } if (typeof id === 'undefined') { return; }
source.postMessage({action: `${action}.response`, params: response, id}, '*'); source.postMessage({action: `${action}.response`, params: response, id}, '*');
} }
async _onRender({template, data, type}) { _onRender({template, data, type}) {
return await this._templateRenderer.render(template, data, type); return this._templateRenderer.render(template, data, type);
} }
async _onGetModifiedData({data, type}) { _onGetModifiedData({data, type}) {
const result = await this._templateRenderer.getModifiedData(data, type); const result = this._templateRenderer.getModifiedData(data, type);
return this._clone(result); return this._clone(result);
} }
_errorToJson(error) { _serializeError(error) {
try { try {
if (error !== null && typeof error === 'object') { if (typeof error === 'object' && error !== null) {
return { return {
name: error.name, name: error.name,
message: error.message, message: error.message,

View File

@ -27,7 +27,7 @@
const templateRenderer = new TemplateRenderer(japaneseUtil); const templateRenderer = new TemplateRenderer(japaneseUtil);
const ankiNoteDataCreator = new AnkiNoteDataCreator(japaneseUtil); const ankiNoteDataCreator = new AnkiNoteDataCreator(japaneseUtil);
templateRenderer.registerDataType('ankiNote', { templateRenderer.registerDataType('ankiNote', {
modifier: ({data, marker}) => ankiNoteDataCreator.create(marker, data) modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData)
}); });
const templateRendererFrameApi = new TemplateRendererFrameApi(templateRenderer); const templateRendererFrameApi = new TemplateRendererFrameApi(templateRenderer);
templateRendererFrameApi.prepare(); templateRendererFrameApi.prepare();

View File

@ -33,10 +33,21 @@ class TemplateRenderer {
this._dataTypes.set(name, {modifier}); this._dataTypes.set(name, {modifier});
} }
async render(template, data, type) { render(template, data, type) {
const instance = this._getTemplateInstance(template);
data = this._getModifiedData(data, type);
return this._renderTemplate(instance, data);
}
getModifiedData(data, type) {
return this._getModifiedData(data, type);
}
// Private
_getTemplateInstance(template) {
if (!this._helpersRegistered) { if (!this._helpersRegistered) {
this._registerHelpers(); this._registerHelpers();
this._helpersRegistered = true;
} }
const cache = this._cache; const cache = this._cache;
@ -47,8 +58,11 @@ class TemplateRenderer {
cache.set(template, instance); cache.set(template, instance);
} }
return instance;
}
_renderTemplate(instance, data) {
try { try {
data = this._getModifiedData(data, type);
this._stateStack = [new Map()]; this._stateStack = [new Map()];
return instance(data).trim(); return instance(data).trim();
} finally { } finally {
@ -56,27 +70,16 @@ class TemplateRenderer {
} }
} }
async getModifiedData(data, type) { _getModifiedData(data, type) {
return this._getModifiedData(data, type);
}
// Private
_getModifier(type) {
if (typeof type === 'string') { if (typeof type === 'string') {
const typeInfo = this._dataTypes.get(type); const typeInfo = this._dataTypes.get(type);
if (typeof typeInfo !== 'undefined') { if (typeof typeInfo !== 'undefined') {
return typeInfo.modifier; const {modifier} = typeInfo;
if (typeof modifier === 'function') {
data = modifier(data);
}
} }
} }
return null;
}
_getModifiedData(data, type) {
const modifier = this._getModifier(type);
if (typeof modifier === 'function') {
data = modifier(data);
}
return data; return data;
} }
@ -122,6 +125,8 @@ class TemplateRenderer {
for (const [name, helper] of helpers) { for (const [name, helper] of helpers) {
this._registerHelper(name, helper); this._registerHelper(name, helper);
} }
this._helpersRegistered = true;
} }
_registerHelper(name, helper) { _registerHelper(name, helper) {

View File

@ -55,7 +55,7 @@ async function createVM() {
const japaneseUtil = new JapaneseUtil(null); const japaneseUtil = new JapaneseUtil(null);
this._templateRenderer = new TemplateRenderer(japaneseUtil); this._templateRenderer = new TemplateRenderer(japaneseUtil);
this._templateRenderer.registerDataType('ankiNote', { this._templateRenderer.registerDataType('ankiNote', {
modifier: ({data, marker}) => ankiNoteDataCreator.create(marker, data) modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData)
}); });
} }
@ -122,7 +122,7 @@ function getFieldMarkers(type) {
} }
} }
async function getRenderResults(dictionaryEntries, type, mode, templates, AnkiNoteBuilder, write) { async function getRenderResults(dictionaryEntries, type, mode, template, AnkiNoteBuilder, write) {
const markers = getFieldMarkers(type); const markers = getFieldMarkers(type);
const fields = []; const fields = [];
for (const marker of markers) { for (const marker of markers) {
@ -158,7 +158,7 @@ async function getRenderResults(dictionaryEntries, type, mode, templates, AnkiNo
definition: dictionaryEntry, definition: dictionaryEntry,
mode: null, mode: null,
context, context,
templates, template,
deckName: 'deckName', deckName: 'deckName',
modelName: 'modelName', modelName: 'modelName',
fields, fields,
@ -193,7 +193,7 @@ async function main() {
const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'})); const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'}));
const actualResults1 = []; const actualResults1 = [];
const templates = fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'}); const template = fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'});
for (let i = 0, ii = tests.length; i < ii; ++i) { for (let i = 0, ii = tests.length; i < ii; ++i) {
const test = tests[i]; const test = tests[i];
@ -204,7 +204,7 @@ async function main() {
const {name, mode, text} = test; const {name, mode, text} = test;
const options = vm.buildOptions(optionsPresets, test.options); const options = vm.buildOptions(optionsPresets, test.options);
const {dictionaryEntries} = clone(await vm.translator.findTerms(mode, text, options)); const {dictionaryEntries} = clone(await vm.translator.findTerms(mode, text, options));
const results = mode !== 'simple' ? clone(await getRenderResults(dictionaryEntries, 'terms', mode, templates, AnkiNoteBuilder, write)) : null; const results = mode !== 'simple' ? clone(await getRenderResults(dictionaryEntries, 'terms', mode, template, AnkiNoteBuilder, write)) : null;
actualResults1.push({name, results}); actualResults1.push({name, results});
if (!write) { if (!write) {
assert.deepStrictEqual(results, expected1.results); assert.deepStrictEqual(results, expected1.results);
@ -216,7 +216,7 @@ async function main() {
const {name, text} = test; const {name, text} = test;
const options = vm.buildOptions(optionsPresets, test.options); const options = vm.buildOptions(optionsPresets, test.options);
const dictionaryEntries = clone(await vm.translator.findKanji(text, options)); const dictionaryEntries = clone(await vm.translator.findKanji(text, options));
const results = clone(await getRenderResults(dictionaryEntries, 'kanji', null, templates, AnkiNoteBuilder, write)); const results = clone(await getRenderResults(dictionaryEntries, 'kanji', null, template, AnkiNoteBuilder, write));
actualResults1.push({name, results}); actualResults1.push({name, results});
if (!write) { if (!write) {
assert.deepStrictEqual(results, expected1.results); assert.deepStrictEqual(results, expected1.results);