diff --git a/ext/bg/background.html b/ext/bg/background.html index d51858a7..f2d01e85 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -34,12 +34,12 @@ - + diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 0c7a0301..93ba620f 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -27,10 +27,10 @@ * JsonSchema * Mecab * ObjectPropertyAccessor + * TemplateRenderer * Translator * conditionsTestValue * dictTermsSort - * handlebarsRenderDynamic * jp * optionsLoad * optionsSave @@ -63,6 +63,7 @@ class Backend { audioSystem: this.audioSystem, renderTemplate: this._renderTemplate.bind(this) }); + this._templateRenderer = new TemplateRenderer(); const url = (typeof window === 'object' && window !== null ? window.location.href : ''); this.optionsContext = {depth: 0, url}; @@ -1230,7 +1231,7 @@ class Backend { } async _renderTemplate(template, data) { - return handlebarsRenderDynamic(template, data); + return await this._templateRenderer.render(template, data); } _getTemplates(options) { diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js deleted file mode 100644 index 822174e2..00000000 --- a/ext/bg/js/handlebars.js +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (C) 2016-2020 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 - * Handlebars - * jp - */ - -function handlebarsEscape(text) { - return Handlebars.Utils.escapeExpression(text); -} - -function handlebarsDumpObject(options) { - const dump = JSON.stringify(options.fn(this), null, 4); - return handlebarsEscape(dump); -} - -function handlebarsFurigana(options) { - const definition = options.fn(this); - const segs = jp.distributeFurigana(definition.expression, definition.reading); - - let result = ''; - for (const seg of segs) { - if (seg.furigana) { - result += `${seg.text}${seg.furigana}`; - } else { - result += seg.text; - } - } - - return result; -} - -function handlebarsFuriganaPlain(options) { - const definition = options.fn(this); - const segs = jp.distributeFurigana(definition.expression, definition.reading); - - let result = ''; - for (const seg of segs) { - if (seg.furigana) { - result += ` ${seg.text}[${seg.furigana}]`; - } else { - result += seg.text; - } - } - - return result.trimLeft(); -} - -function handlebarsKanjiLinks(options) { - let result = ''; - for (const c of options.fn(this)) { - if (jp.isCodePointKanji(c.codePointAt(0))) { - result += `${c}`; - } else { - result += c; - } - } - - return result; -} - -function handlebarsMultiLine(options) { - return options.fn(this).split('\n').join('
'); -} - -function handlebarsSanitizeCssClass(options) { - return options.fn(this).replace(/[^_a-z0-9\u00a0-\uffff]/ig, '_'); -} - -function handlebarsRegexReplace(...args) { - // Usage: - // {{#regexReplace regex string [flags]}}content{{/regexReplace}} - // regex: regular expression string - // string: string to replace - // flags: optional flags for regular expression - // e.g. "i" for case-insensitive, "g" for replace all - let value = args[args.length - 1].fn(this); - if (args.length >= 3) { - try { - const flags = args.length > 3 ? args[2] : 'g'; - const regex = new RegExp(args[0], flags); - value = value.replace(regex, args[1]); - } catch (e) { - return `${e}`; - } - } - return value; -} - -function handlebarsRegexMatch(...args) { - // Usage: - // {{#regexMatch regex [flags]}}content{{/regexMatch}} - // regex: regular expression string - // flags: optional flags for regular expression - // e.g. "i" for case-insensitive, "g" for match all - let value = args[args.length - 1].fn(this); - if (args.length >= 2) { - try { - const flags = args.length > 2 ? args[1] : ''; - const regex = new RegExp(args[0], flags); - const parts = []; - value.replace(regex, (g0) => parts.push(g0)); - value = parts.join(''); - } catch (e) { - return `${e}`; - } - } - return value; -} - -function handlebarsMergeTags(object, isGroupMode, isMergeMode) { - const tagSources = []; - if (isGroupMode || isMergeMode) { - for (const definition of object.definitions) { - tagSources.push(definition.definitionTags); - } - } else { - tagSources.push(object.definitionTags); - } - - const tags = new Set(); - for (const tagSource of tagSources) { - for (const tag of tagSource) { - tags.add(tag.name); - } - } - - return [...tags].join(', '); -} - -function handlebarsRegisterHelpers() { - if (Handlebars.partials !== Handlebars.templates) { - Handlebars.partials = Handlebars.templates; - Handlebars.registerHelper('dumpObject', handlebarsDumpObject); - Handlebars.registerHelper('furigana', handlebarsFurigana); - Handlebars.registerHelper('furiganaPlain', handlebarsFuriganaPlain); - Handlebars.registerHelper('kanjiLinks', handlebarsKanjiLinks); - Handlebars.registerHelper('multiLine', handlebarsMultiLine); - Handlebars.registerHelper('sanitizeCssClass', handlebarsSanitizeCssClass); - Handlebars.registerHelper('regexReplace', handlebarsRegexReplace); - Handlebars.registerHelper('regexMatch', handlebarsRegexMatch); - Handlebars.registerHelper('mergeTags', handlebarsMergeTags); - } -} - -function handlebarsRenderDynamic(template, data) { - handlebarsRegisterHelpers(); - const cache = handlebarsRenderDynamic._cache; - let instance = cache.get(template); - if (typeof instance === 'undefined') { - instance = Handlebars.compile(template); - cache.set(template, instance); - } - - return instance(data).trim(); -} -handlebarsRenderDynamic._cache = new Map(); diff --git a/ext/bg/js/template-renderer.js b/ext/bg/js/template-renderer.js new file mode 100644 index 00000000..f4b50c3d --- /dev/null +++ b/ext/bg/js/template-renderer.js @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2016-2020 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 + * Handlebars + * jp + */ + +class TemplateRenderer { + constructor() { + this._cache = new Map(); + this._cacheMaxSize = 5; + this._helpersRegistered = false; + } + + async render(template, data) { + if (!this._helpersRegistered) { + this._registerHelpers(); + this._helpersRegistered = true; + } + + const cache = this._cache; + let instance = cache.get(template); + if (typeof instance === 'undefined') { + this._updateCacheSize(this._cacheMaxSize - 1); + instance = Handlebars.compile(template); + cache.set(template, instance); + } + + return instance(data).trim(); + } + + // Private + + _updateCacheSize(maxSize) { + const cache = this._cache; + let removeCount = cache.size - maxSize; + if (removeCount <= 0) { return; } + + for (const key of cache.keys()) { + cache.delete(key); + if (--removeCount <= 0) { break; } + } + } + + _registerHelpers() { + Handlebars.partials = Handlebars.templates; + + const helpers = [ + ['dumpObject', this._dumpObject.bind(this)], + ['furigana', this._furigana.bind(this)], + ['furiganaPlain', this._furiganaPlain.bind(this)], + ['kanjiLinks', this._kanjiLinks.bind(this)], + ['multiLine', this._multiLine.bind(this)], + ['sanitizeCssClass', this._sanitizeCssClass.bind(this)], + ['regexReplace', this._regexReplace.bind(this)], + ['regexMatch', this._regexMatch.bind(this)], + ['mergeTags', this._mergeTags.bind(this)] + ]; + + for (const [name, helper] of helpers) { + Handlebars.registerHelper(name, helper); + } + } + + _escape(text) { + return Handlebars.Utils.escapeExpression(text); + } + + _dumpObject(options) { + const dump = JSON.stringify(options.fn(this), null, 4); + return this._escape(dump); + } + + _furigana(options) { + const definition = options.fn(this); + const segs = jp.distributeFurigana(definition.expression, definition.reading); + + let result = ''; + for (const seg of segs) { + if (seg.furigana) { + result += `${seg.text}${seg.furigana}`; + } else { + result += seg.text; + } + } + + return result; + } + + _furiganaPlain(options) { + const definition = options.fn(this); + const segs = jp.distributeFurigana(definition.expression, definition.reading); + + let result = ''; + for (const seg of segs) { + if (seg.furigana) { + result += ` ${seg.text}[${seg.furigana}]`; + } else { + result += seg.text; + } + } + + return result.trimLeft(); + } + + _kanjiLinks(options) { + let result = ''; + for (const c of options.fn(this)) { + if (jp.isCodePointKanji(c.codePointAt(0))) { + result += `${c}`; + } else { + result += c; + } + } + + return result; + } + + _multiLine(options) { + return options.fn(this).split('\n').join('
'); + } + + _sanitizeCssClass(options) { + return options.fn(this).replace(/[^_a-z0-9\u00a0-\uffff]/ig, '_'); + } + + _regexReplace(...args) { + // Usage: + // {{#regexReplace regex string [flags]}}content{{/regexReplace}} + // regex: regular expression string + // string: string to replace + // flags: optional flags for regular expression + // e.g. "i" for case-insensitive, "g" for replace all + let value = args[args.length - 1].fn(this); + if (args.length >= 3) { + try { + const flags = args.length > 3 ? args[2] : 'g'; + const regex = new RegExp(args[0], flags); + value = value.replace(regex, args[1]); + } catch (e) { + return `${e}`; + } + } + return value; + } + + _regexMatch(...args) { + // Usage: + // {{#regexMatch regex [flags]}}content{{/regexMatch}} + // regex: regular expression string + // flags: optional flags for regular expression + // e.g. "i" for case-insensitive, "g" for match all + let value = args[args.length - 1].fn(this); + if (args.length >= 2) { + try { + const flags = args.length > 2 ? args[1] : ''; + const regex = new RegExp(args[0], flags); + const parts = []; + value.replace(regex, (g0) => parts.push(g0)); + value = parts.join(''); + } catch (e) { + return `${e}`; + } + } + return value; + } + + _mergeTags(object, isGroupMode, isMergeMode) { + const tagSources = []; + if (isGroupMode || isMergeMode) { + for (const definition of object.definitions) { + tagSources.push(definition.definitionTags); + } + } else { + tagSources.push(object.definitionTags); + } + + const tags = new Set(); + for (const tagSource of tagSources) { + for (const tag of tagSource) { + tags.add(tag.name); + } + } + + return [...tags].join(', '); + } +}