aboutsummaryrefslogtreecommitdiff
path: root/ext/js/templates/sandbox
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/templates/sandbox')
-rw-r--r--ext/js/templates/sandbox/anki-template-renderer-content-manager.js73
-rw-r--r--ext/js/templates/sandbox/anki-template-renderer.js827
-rw-r--r--ext/js/templates/sandbox/template-renderer-frame-api.js103
-rw-r--r--ext/js/templates/sandbox/template-renderer-frame-main.js30
-rw-r--r--ext/js/templates/sandbox/template-renderer-media-provider.js189
-rw-r--r--ext/js/templates/sandbox/template-renderer.js206
6 files changed, 0 insertions, 1428 deletions
diff --git a/ext/js/templates/sandbox/anki-template-renderer-content-manager.js b/ext/js/templates/sandbox/anki-template-renderer-content-manager.js
deleted file mode 100644
index 664746bf..00000000
--- a/ext/js/templates/sandbox/anki-template-renderer-content-manager.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2023-2024 Yomitan Authors
- * Copyright (C) 2022 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/>.
- */
-
-/**
- * The content manager which is used when generating content for Anki.
- */
-export class AnkiTemplateRendererContentManager {
- /**
- * Creates a new instance of the class.
- * @param {import('./template-renderer-media-provider.js').TemplateRendererMediaProvider} mediaProvider The media provider for the object.
- * @param {import('anki-templates').NoteData} data The data object passed to the Handlebars template renderer.
- */
- constructor(mediaProvider, data) {
- /** @type {import('./template-renderer-media-provider.js').TemplateRendererMediaProvider} */
- this._mediaProvider = mediaProvider;
- /** @type {import('anki-templates').NoteData} */
- this._data = data;
- /** @type {import('anki-template-renderer-content-manager').OnUnloadCallback[]} */
- this._onUnloadCallbacks = [];
- }
-
- /**
- * Attempts to load the media file from a given dictionary.
- * @param {string} path The path to the media file in the dictionary.
- * @param {string} dictionary The name of the dictionary.
- * @param {import('anki-template-renderer-content-manager').OnLoadCallback} onLoad The callback that is executed if the media was loaded successfully.
- * No assumptions should be made about the synchronicity of this callback.
- * @param {import('anki-template-renderer-content-manager').OnUnloadCallback} onUnload The callback that is executed when the media should be unloaded.
- */
- loadMedia(path, dictionary, onLoad, onUnload) {
- const imageUrl = this._mediaProvider.getMedia(this._data, ['dictionaryMedia', path], {dictionary, format: 'fileName', default: null});
- if (imageUrl === null) { return; }
- onLoad(imageUrl);
- if (typeof onUnload === 'function') {
- this._onUnloadCallbacks.push(onUnload);
- }
- }
-
- /**
- * Unloads all media that has been loaded.
- */
- unloadAll() {
- for (const onUnload of this._onUnloadCallbacks) {
- onUnload(true);
- }
- this._onUnloadCallbacks = [];
- }
-
- /**
- * Sets up attributes and events for a link element.
- * @param {HTMLAnchorElement} element The link element.
- * @param {string} href The URL.
- * @param {boolean} internal Whether or not the URL is an internal or external link.
- */
- prepareLink(element, href, internal) {
- element.href = internal ? '#' : href;
- }
-}
diff --git a/ext/js/templates/sandbox/anki-template-renderer.js b/ext/js/templates/sandbox/anki-template-renderer.js
deleted file mode 100644
index 022716c3..00000000
--- a/ext/js/templates/sandbox/anki-template-renderer.js
+++ /dev/null
@@ -1,827 +0,0 @@
-/*
- * Copyright (C) 2023-2024 Yomitan Authors
- * Copyright (C) 2021-2022 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/>.
- */
-
-import {Handlebars} from '../../../lib/handlebars.js';
-import {createAnkiNoteData} from '../../data/sandbox/anki-note-data-creator.js';
-import {getPronunciationsOfType, isNonNounVerbOrAdjective} from '../../dictionary/dictionary-data-util.js';
-import {createPronunciationDownstepPosition, createPronunciationGraph, createPronunciationText} from '../../display/sandbox/pronunciation-generator.js';
-import {StructuredContentGenerator} from '../../display/sandbox/structured-content-generator.js';
-import {CssStyleApplier} from '../../dom/sandbox/css-style-applier.js';
-import {convertHiraganaToKatakana, convertKatakanaToHiragana, distributeFurigana, getKanaMorae, getPitchCategory, isMoraPitchHigh} from '../../language/ja/japanese.js';
-import {AnkiTemplateRendererContentManager} from './anki-template-renderer-content-manager.js';
-import {TemplateRendererMediaProvider} from './template-renderer-media-provider.js';
-import {TemplateRenderer} from './template-renderer.js';
-
-/**
- * This class contains all Anki-specific template rendering functionality. It is built on
- * the generic TemplateRenderer class and various other Anki-related classes.
- */
-export class AnkiTemplateRenderer {
- /**
- * Creates a new instance of the class.
- */
- constructor() {
- /** @type {CssStyleApplier} */
- this._structuredContentStyleApplier = new CssStyleApplier('/data/structured-content-style.json');
- /** @type {CssStyleApplier} */
- this._pronunciationStyleApplier = new CssStyleApplier('/data/pronunciation-style.json');
- /** @type {RegExp} */
- this._structuredContentDatasetKeyIgnorePattern = /^sc([^a-z]|$)/;
- /** @type {TemplateRenderer} */
- this._templateRenderer = new TemplateRenderer();
- /** @type {TemplateRendererMediaProvider} */
- this._mediaProvider = new TemplateRendererMediaProvider();
- /** @type {?(Map<string, unknown>[])} */
- this._stateStack = null;
- /** @type {?import('anki-note-builder').Requirement[]} */
- this._requirements = null;
- /** @type {(() => void)[]} */
- this._cleanupCallbacks = [];
- /** @type {?HTMLElement} */
- this._temporaryElement = null;
- }
-
- /**
- * Gets the generic TemplateRenderer instance.
- * @type {TemplateRenderer}
- */
- get templateRenderer() {
- return this._templateRenderer;
- }
-
- /**
- * Prepares the data that is necessary before the template renderer can be safely used.
- */
- async prepare() {
- /* eslint-disable @stylistic/no-multi-spaces */
- this._templateRenderer.registerHelpers([
- ['dumpObject', this._dumpObject.bind(this)],
- ['furigana', this._furigana.bind(this)],
- ['furiganaPlain', this._furiganaPlain.bind(this)],
- ['multiLine', this._multiLine.bind(this)],
- ['regexReplace', this._regexReplace.bind(this)],
- ['regexMatch', this._regexMatch.bind(this)],
- ['mergeTags', this._mergeTags.bind(this)],
- ['eachUpTo', this._eachUpTo.bind(this)],
- ['spread', this._spread.bind(this)],
- ['op', this._op.bind(this)],
- ['get', this._get.bind(this)],
- ['set', this._set.bind(this)],
- ['scope', this._scope.bind(this)],
- ['property', this._property.bind(this)],
- ['noop', this._noop.bind(this)],
- ['isMoraPitchHigh', this._isMoraPitchHigh.bind(this)],
- ['getKanaMorae', this._getKanaMorae.bind(this)],
- ['typeof', this._getTypeof.bind(this)],
- ['join', this._join.bind(this)],
- ['concat', this._concat.bind(this)],
- ['pitchCategories', this._pitchCategories.bind(this)],
- ['formatGlossary', this._formatGlossary.bind(this)],
- ['hasMedia', this._hasMedia.bind(this)],
- ['getMedia', this._getMedia.bind(this)],
- ['pronunciation', this._pronunciation.bind(this)],
- ['hiragana', this._hiragana.bind(this)],
- ['katakana', this._katakana.bind(this)]
- ]);
- /* eslint-enable @stylistic/no-multi-spaces */
- this._templateRenderer.registerDataType('ankiNote', {
- modifier: ({marker, commonData}) => createAnkiNoteData(marker, commonData),
- composeData: ({marker}, commonData) => ({marker, commonData})
- });
- this._templateRenderer.setRenderCallbacks(
- this._onRenderSetup.bind(this),
- this._onRenderCleanup.bind(this)
- );
- await Promise.all([
- this._structuredContentStyleApplier.prepare(),
- this._pronunciationStyleApplier.prepare()
- ]);
- }
-
- // Private
-
- /**
- * @returns {{requirements: import('anki-note-builder').Requirement[]}}
- */
- _onRenderSetup() {
- /** @type {import('anki-note-builder').Requirement[]} */
- const requirements = [];
- this._stateStack = [new Map()];
- this._requirements = requirements;
- this._mediaProvider.requirements = requirements;
- return {requirements};
- }
-
- /**
- * @returns {void}
- */
- _onRenderCleanup() {
- for (const callback of this._cleanupCallbacks) { callback(); }
- this._stateStack = null;
- this._requirements = null;
- this._mediaProvider.requirements = null;
- this._cleanupCallbacks.length = 0;
- }
-
- /**
- * @param {string} text
- * @returns {string}
- */
- _safeString(text) {
- return new Handlebars.SafeString(text);
- }
-
- // Template helpers
-
- /** @type {import('template-renderer').HelperFunction<string>} */
- _dumpObject(object) {
- return JSON.stringify(object, null, 4);
- }
-
- /** @type {import('template-renderer').HelperFunction<string>} */
- _furigana(args, context, options) {
- const {expression, reading} = this._getFuriganaExpressionAndReading(args, context, options);
- const segments = distributeFurigana(expression, reading);
-
- let result = '';
- for (const {text, reading: reading2} of segments) {
- result += (
- reading2.length > 0 ?
- `<ruby>${text}<rt>${reading2}</rt></ruby>` :
- text
- );
- }
-
- return this._safeString(result);
- }
-
- /** @type {import('template-renderer').HelperFunction<string>} */
- _furiganaPlain(args, context, options) {
- const {expression, reading} = this._getFuriganaExpressionAndReading(args, context, options);
- const segments = distributeFurigana(expression, reading);
-
- let result = '';
- for (const {text, reading: reading2} of segments) {
- if (reading2.length > 0) {
- if (result.length > 0) { result += ' '; }
- result += `${text}[${reading2}]`;
- } else {
- result += text;
- }
- }
-
- return result;
- }
-
- /**
- * @type {import('template-renderer').HelperFunction<{expression: string, reading: string}>}
- */
- _getFuriganaExpressionAndReading(args) {
- let expression;
- let reading;
- if (args.length >= 2) {
- [expression, reading] = /** @type {[expression?: string, reading?: string]} */ (args);
- } else {
- ({expression, reading} = /** @type {import('core').SerializableObject} */ (args[0]));
- }
- return {
- expression: typeof expression === 'string' ? expression : '',
- reading: typeof reading === 'string' ? reading : ''
- };
- }
-
- /**
- * @param {string} string
- * @returns {string}
- */
- _stringToMultiLineHtml(string) {
- return string.split('\n').join('<br>');
- }
-
- /** @type {import('template-renderer').HelperFunction<string>} */
- _multiLine(_args, context, options) {
- return this._stringToMultiLineHtml(this._computeValueString(options, context));
- }
-
- /**
- * Usage:
- * ```{{#regexReplace regex string [flags] [content]...}}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
- * @type {import('template-renderer').HelperFunction<string>}
- */
- _regexReplace(args, context, options) {
- const argCount = args.length;
- let value = this._computeValueString(options, context);
- if (argCount > 3) {
- value = `${args.slice(3).join('')}${value}`;
- }
- if (argCount > 1) {
- try {
- const [pattern, replacement, flags] = args;
- if (typeof pattern !== 'string') { throw new Error('Invalid pattern'); }
- if (typeof replacement !== 'string') { throw new Error('Invalid replacement'); }
- const regex = new RegExp(pattern, typeof flags === 'string' ? flags : 'g');
- value = value.replace(regex, replacement);
- } catch (e) {
- return `${e}`;
- }
- }
- return value;
- }
-
- /**
- * Usage:
- * {{#regexMatch regex [flags] [content]...}}content{{/regexMatch}}
- * - regex: regular expression string
- * - flags: optional flags for regular expression
- * e.g. "i" for case-insensitive, "g" for match all
- * @type {import('template-renderer').HelperFunction<string>}
- */
- _regexMatch(args, context, options) {
- const argCount = args.length;
- let value = this._computeValueString(options, context);
- if (argCount > 2) {
- value = `${args.slice(2).join('')}${value}`;
- }
- if (argCount > 0) {
- try {
- const [pattern, flags] = args;
- if (typeof pattern !== 'string') { throw new Error('Invalid pattern'); }
- const regex = new RegExp(pattern, typeof flags === 'string' ? flags : '');
- /** @type {string[]} */
- const parts = [];
- value.replace(regex, (g0) => {
- parts.push(g0);
- return g0;
- });
- value = parts.join('');
- } catch (e) {
- return `${e}`;
- }
- }
- return value;
- }
-
- /**
- * @type {import('template-renderer').HelperFunction<string>}
- */
- _mergeTags(args) {
- const [object, isGroupMode, isMergeMode] = /** @type {[object: import('anki-templates').TermDictionaryEntry, isGroupMode: boolean, isMergeMode: boolean]} */ (args);
- const tagSources = [];
- if (isGroupMode || isMergeMode) {
- const {definitions} = object;
- if (Array.isArray(definitions)) {
- for (const definition of definitions) {
- tagSources.push(definition.definitionTags);
- }
- }
- } else {
- tagSources.push(object.definitionTags);
- }
-
- const tags = new Set();
- for (const tagSource of tagSources) {
- if (!Array.isArray(tagSource)) { continue; }
- for (const tag of tagSource) {
- tags.add(tag.name);
- }
- }
-
- return [...tags].join(', ');
- }
-
- /** @type {import('template-renderer').HelperFunction<string>} */
- _eachUpTo(args, context, options) {
- const [iterable, maxCount] = /** @type {[iterable: Iterable<unknown>, maxCount: number]} */ (args);
- if (iterable) {
- const results = [];
- let any = false;
- for (const entry of iterable) {
- any = true;
- if (results.length >= maxCount) { break; }
- const processedEntry = this._computeValue(options, entry);
- results.push(processedEntry);
- }
- if (any) {
- return results.join('');
- }
- }
- return this._computeInverseString(options, context);
- }
-
- /** @type {import('template-renderer').HelperFunction<unknown[]>} */
- _spread(args) {
- const result = [];
- for (const array of /** @type {Iterable<unknown>[]} */ (args)) {
- try {
- result.push(...array);
- } catch (e) {
- // NOP
- }
- }
- return result;
- }
-
- /** @type {import('template-renderer').HelperFunction<unknown>} */
- _op(args) {
- const [operator] = /** @type {[operator: string, operand1: import('core').SafeAny, operand2?: import('core').SafeAny, operand3?: import('core').SafeAny]} */ (args);
- switch (args.length) {
- case 2: return this._evaluateUnaryExpression(operator, args[1]);
- case 3: return this._evaluateBinaryExpression(operator, args[1], args[2]);
- case 4: return this._evaluateTernaryExpression(operator, args[1], args[2], args[3]);
- default: return void 0;
- }
- }
-
- /**
- * @param {string} operator
- * @param {import('core').SafeAny} operand1
- * @returns {unknown}
- */
- _evaluateUnaryExpression(operator, operand1) {
- switch (operator) {
- case '+': return +operand1;
- case '-': return -operand1;
- case '~': return ~operand1;
- case '!': return !operand1;
- default: return void 0;
- }
- }
-
- /**
- * @param {string} operator
- * @param {import('core').SafeAny} operand1
- * @param {import('core').SafeAny} operand2
- * @returns {unknown}
- */
- _evaluateBinaryExpression(operator, operand1, operand2) {
- switch (operator) {
- case '+': return operand1 + operand2;
- case '-': return operand1 - operand2;
- case '/': return operand1 / operand2;
- case '*': return operand1 * operand2;
- case '%': return operand1 % operand2;
- case '**': return operand1 ** operand2;
- case '==': return operand1 == operand2; // eslint-disable-line eqeqeq
- case '!=': return operand1 != operand2; // eslint-disable-line eqeqeq
- case '===': return operand1 === operand2;
- case '!==': return operand1 !== operand2;
- case '<': return operand1 < operand2;
- case '<=': return operand1 <= operand2;
- case '>': return operand1 > operand2;
- case '>=': return operand1 >= operand2;
- case '<<': return operand1 << operand2;
- case '>>': return operand1 >> operand2;
- case '>>>': return operand1 >>> operand2;
- case '&': return operand1 & operand2;
- case '|': return operand1 | operand2;
- case '^': return operand1 ^ operand2;
- case '&&': return operand1 && operand2;
- case '||': return operand1 || operand2;
- default: return void 0;
- }
- }
-
- /**
- * @param {string} operator
- * @param {import('core').SafeAny} operand1
- * @param {import('core').SafeAny} operand2
- * @param {import('core').SafeAny} operand3
- * @returns {unknown}
- */
- _evaluateTernaryExpression(operator, operand1, operand2, operand3) {
- switch (operator) {
- case '?:': return operand1 ? operand2 : operand3;
- default: return void 0;
- }
- }
-
- /** @type {import('template-renderer').HelperFunction<unknown>} */
- _get(args) {
- const [key] = /** @type {[key: string]} */ (args);
- const stateStack = this._stateStack;
- if (stateStack === null) { throw new Error('Invalid state'); }
- for (let i = stateStack.length; --i >= 0;) {
- const map = stateStack[i];
- if (map.has(key)) {
- return map.get(key);
- }
- }
- return void 0;
- }
-
- /** @type {import('template-renderer').HelperFunction<string>} */
- _set(args, context, options) {
- const stateStack = this._stateStack;
- if (stateStack === null) { throw new Error('Invalid state'); }
- switch (args.length) {
- case 1:
- {
- const [key] = /** @type {[key: string]} */ (args);
- const value = this._computeValue(options, context);
- stateStack[stateStack.length - 1].set(key, value);
- }
- break;
- case 2:
- {
- const [key, value] = /** @type {[key: string, value: unknown]} */ (args);
- stateStack[stateStack.length - 1].set(key, value);
- }
- break;
- }
- return '';
- }
-
- /** @type {import('template-renderer').HelperFunction<unknown>} */
- _scope(_args, context, options) {
- const stateStack = this._stateStack;
- if (stateStack === null) { throw new Error('Invalid state'); }
- try {
- stateStack.push(new Map());
- return this._computeValue(options, context);
- } finally {
- if (stateStack.length > 1) {
- stateStack.pop();
- }
- }
- }
-
- /** @type {import('template-renderer').HelperFunction<unknown>} */
- _property(args) {
- const ii = args.length;
- if (ii <= 0) { return void 0; }
-
- try {
- let value = args[0];
- for (let i = 1; i < ii; ++i) {
- if (typeof value !== 'object' || value === null) { throw new Error('Invalid object'); }
- const key = args[i];
- switch (typeof key) {
- case 'number':
- case 'string':
- case 'symbol':
- break;
- default:
- throw new Error('Invalid key');
- }
- value = /** @type {import('core').UnknownObject} */ (value)[key];
- }
- return value;
- } catch (e) {
- return void 0;
- }
- }
-
- /** @type {import('template-renderer').HelperFunction<unknown>} */
- _noop(_args, context, options) {
- return this._computeValue(options, context);
- }
-
- /** @type {import('template-renderer').HelperFunction<boolean>} */
- _isMoraPitchHigh(args) {
- const [index, position] = /** @type {[index: number, position: number]} */ (args);
- return isMoraPitchHigh(index, position);
- }
-
- /** @type {import('template-renderer').HelperFunction<string[]>} */
- _getKanaMorae(args) {
- const [text] = /** @type {[text: string]} */ (args);
- return getKanaMorae(`${text}`);
- }
-
- /** @type {import('template-renderer').HelperFunction<import('core').TypeofResult>} */
- _getTypeof(args, context, options) {
- const ii = args.length;
- const value = (ii > 0 ? args[0] : this._computeValue(options, context));
- return typeof value;
- }
-
- /** @type {import('template-renderer').HelperFunction<string>} */
- _join(args) {
- return args.length > 0 ? args.slice(1, args.length).flat().join(/** @type {string} */ (args[0])) : '';
- }
-
- /** @type {import('template-renderer').HelperFunction<string>} */
- _concat(args) {
- let result = '';
- for (let i = 0, ii = args.length; i < ii; ++i) {
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
- result += args[i];
- }
- return result;
- }
-
- /** @type {import('template-renderer').HelperFunction<string[]>} */
- _pitchCategories(args) {
- const [data] = /** @type {[data: import('anki-templates').NoteData]} */ (args);
- const {dictionaryEntry} = data;
- if (dictionaryEntry.type !== 'term') { return []; }
- const {pronunciations: termPronunciations, headwords} = dictionaryEntry;
- /** @type {Set<string>} */
- const categories = new Set();
- for (const {headwordIndex, pronunciations} of termPronunciations) {
- const {reading, wordClasses} = headwords[headwordIndex];
- const isVerbOrAdjective = isNonNounVerbOrAdjective(wordClasses);
- const pitches = getPronunciationsOfType(pronunciations, 'pitch-accent');
- for (const {position} of pitches) {
- const category = getPitchCategory(reading, position, isVerbOrAdjective);
- if (category !== null) {
- categories.add(category);
- }
- }
- }
- return [...categories];
- }
-
- /**
- * @returns {HTMLElement}
- */
- _getTemporaryElement() {
- let element = this._temporaryElement;
- if (element === null) {
- element = document.createElement('div');
- this._temporaryElement = element;
- }
- return element;
- }
-
- /**
- * @param {Element} node
- * @returns {string}
- */
- _getStructuredContentHtml(node) {
- return this._getHtml(node, this._structuredContentStyleApplier, this._structuredContentDatasetKeyIgnorePattern);
- }
-
- /**
- * @param {Element} node
- * @returns {string}
- */
- _getPronunciationHtml(node) {
- return this._getHtml(node, this._pronunciationStyleApplier, null);
- }
-
- /**
- * @param {Element} node
- * @param {CssStyleApplier} styleApplier
- * @param {?RegExp} datasetKeyIgnorePattern
- * @returns {string}
- */
- _getHtml(node, styleApplier, datasetKeyIgnorePattern) {
- const container = this._getTemporaryElement();
- container.appendChild(node);
- this._normalizeHtml(container, styleApplier, datasetKeyIgnorePattern);
- const result = container.innerHTML;
- container.textContent = '';
- return this._safeString(result);
- }
-
- /**
- * @param {Element} root
- * @param {CssStyleApplier} styleApplier
- * @param {?RegExp} datasetKeyIgnorePattern
- */
- _normalizeHtml(root, styleApplier, datasetKeyIgnorePattern) {
- const {ELEMENT_NODE, TEXT_NODE} = Node;
- const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
- /** @type {HTMLElement[]} */
- const elements = [];
- /** @type {Text[]} */
- const textNodes = [];
- while (true) {
- const node = treeWalker.nextNode();
- if (node === null) { break; }
- switch (node.nodeType) {
- case ELEMENT_NODE:
- elements.push(/** @type {HTMLElement} */ (node));
- break;
- case TEXT_NODE:
- textNodes.push(/** @type {Text} */ (node));
- break;
- }
- }
- styleApplier.applyClassStyles(elements);
- for (const element of elements) {
- const {dataset} = element;
- for (const key of Object.keys(dataset)) {
- if (datasetKeyIgnorePattern !== null && datasetKeyIgnorePattern.test(key)) { continue; }
- delete dataset[key];
- }
- }
- for (const textNode of textNodes) {
- this._replaceNewlines(textNode);
- }
- }
-
- /**
- * @param {Text} textNode
- */
- _replaceNewlines(textNode) {
- const parts = /** @type {string} */ (textNode.nodeValue).split('\n');
- if (parts.length <= 1) { return; }
- const {parentNode} = textNode;
- if (parentNode === null) { return; }
- const fragment = document.createDocumentFragment();
- for (let i = 0, ii = parts.length; i < ii; ++i) {
- if (i > 0) { fragment.appendChild(document.createElement('br')); }
- fragment.appendChild(document.createTextNode(parts[i]));
- }
- parentNode.replaceChild(fragment, textNode);
- }
-
- /**
- * @param {import('anki-templates').NoteData} data
- * @returns {StructuredContentGenerator}
- */
- _createStructuredContentGenerator(data) {
- const contentManager = new AnkiTemplateRendererContentManager(this._mediaProvider, data);
- const instance = new StructuredContentGenerator(contentManager, document);
- this._cleanupCallbacks.push(() => contentManager.unloadAll());
- return instance;
- }
-
- /**
- * @type {import('template-renderer').HelperFunction<string>}
- */
- _formatGlossary(args, _context, options) {
- const [dictionary, content] = /** @type {[dictionary: string, content: import('dictionary-data').TermGlossaryContent]} */ (args);
- /** @type {import('anki-templates').NoteData} */
- const data = options.data.root;
- if (typeof content === 'string') { return this._safeString(this._stringToMultiLineHtml(content)); }
- if (!(typeof content === 'object' && content !== null)) { return ''; }
- switch (content.type) {
- case 'image': return this._formatGlossaryImage(content, dictionary, data);
- case 'structured-content': return this._formatStructuredContent(content, dictionary, data);
- case 'text': return this._safeString(this._stringToMultiLineHtml(content.text));
- }
- return '';
- }
-
- /**
- * @param {import('dictionary-data').TermGlossaryImage} content
- * @param {string} dictionary
- * @param {import('anki-templates').NoteData} data
- * @returns {string}
- */
- _formatGlossaryImage(content, dictionary, data) {
- const structuredContentGenerator = this._createStructuredContentGenerator(data);
- const node = structuredContentGenerator.createDefinitionImage(content, dictionary);
- return this._getStructuredContentHtml(node);
- }
-
- /**
- * @param {import('dictionary-data').TermGlossaryStructuredContent} content
- * @param {string} dictionary
- * @param {import('anki-templates').NoteData} data
- * @returns {string}
- */
- _formatStructuredContent(content, dictionary, data) {
- const structuredContentGenerator = this._createStructuredContentGenerator(data);
- const node = structuredContentGenerator.createStructuredContent(content.content, dictionary);
- return node !== null ? this._getStructuredContentHtml(node) : '';
- }
-
- /**
- * @type {import('template-renderer').HelperFunction<boolean>}
- */
- _hasMedia(args, _context, options) {
- /** @type {import('anki-templates').NoteData} */
- const data = options.data.root;
- return this._mediaProvider.hasMedia(data, args, options.hash);
- }
-
- /**
- * @type {import('template-renderer').HelperFunction<?string>}
- */
- _getMedia(args, _context, options) {
- /** @type {import('anki-templates').NoteData} */
- const data = options.data.root;
- return this._mediaProvider.getMedia(data, args, options.hash);
- }
-
- /**
- * @type {import('template-renderer').HelperFunction<string>}
- */
- _pronunciation(_args, _context, options) {
- const {format, reading, downstepPosition} = options.hash;
-
- if (
- typeof reading !== 'string' ||
- reading.length === 0 ||
- typeof downstepPosition !== 'number'
- ) {
- return '';
- }
- const morae = getKanaMorae(reading);
-
- switch (format) {
- case 'text':
- {
- const nasalPositions = this._getValidNumberArray(options.hash.nasalPositions);
- const devoicePositions = this._getValidNumberArray(options.hash.devoicePositions);
- return this._getPronunciationHtml(createPronunciationText(morae, downstepPosition, nasalPositions, devoicePositions));
- }
- case 'graph':
- return this._getPronunciationHtml(createPronunciationGraph(morae, downstepPosition));
- case 'position':
- return this._getPronunciationHtml(createPronunciationDownstepPosition(downstepPosition));
- default:
- return '';
- }
- }
-
- /**
- * @param {unknown} value
- * @returns {number[]}
- */
- _getValidNumberArray(value) {
- const result = [];
- if (Array.isArray(value)) {
- for (const item of value) {
- if (typeof item === 'number') { result.push(item); }
- }
- }
- return result;
- }
-
- /**
- * @type {import('template-renderer').HelperFunction<string>}
- */
- _hiragana(args, context, options) {
- const ii = args.length;
- const {keepProlongedSoundMarks} = options.hash;
- const value = (ii > 0 ? args[0] : this._computeValue(options, context));
- return typeof value === 'string' ? convertKatakanaToHiragana(value, keepProlongedSoundMarks === true) : '';
- }
-
- /**
- * @type {import('template-renderer').HelperFunction<string>}
- */
- _katakana(args, context, options) {
- const ii = args.length;
- const value = (ii > 0 ? args[0] : this._computeValue(options, context));
- return typeof value === 'string' ? convertHiraganaToKatakana(value) : '';
- }
-
- /**
- * @param {unknown} value
- * @returns {string}
- */
- _asString(value) {
- return typeof value === 'string' ? value : `${value}`;
- }
-
- /**
- * @param {import('template-renderer').HelperOptions} options
- * @param {unknown} context
- * @returns {unknown}
- */
- _computeValue(options, context) {
- return typeof options.fn === 'function' ? options.fn(context) : '';
- }
-
- /**
- * @param {import('template-renderer').HelperOptions} options
- * @param {unknown} context
- * @returns {string}
- */
- _computeValueString(options, context) {
- return this._asString(this._computeValue(options, context));
- }
-
- /**
- * @param {import('template-renderer').HelperOptions} options
- * @param {unknown} context
- * @returns {unknown}
- */
- _computeInverse(options, context) {
- return typeof options.inverse === 'function' ? options.inverse(context) : '';
- }
-
- /**
- * @param {import('template-renderer').HelperOptions} options
- * @param {unknown} context
- * @returns {string}
- */
- _computeInverseString(options, context) {
- return this._asString(this._computeInverse(options, context));
- }
-}
diff --git a/ext/js/templates/sandbox/template-renderer-frame-api.js b/ext/js/templates/sandbox/template-renderer-frame-api.js
deleted file mode 100644
index a0017d70..00000000
--- a/ext/js/templates/sandbox/template-renderer-frame-api.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2023-2024 Yomitan Authors
- * Copyright (C) 2020-2022 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/>.
- */
-
-import {createApiMap, invokeApiMapHandler} from '../../core/api-map.js';
-import {parseJson} from '../../core/json.js';
-
-export class TemplateRendererFrameApi {
- /**
- * @param {import('./template-renderer.js').TemplateRenderer} templateRenderer
- */
- constructor(templateRenderer) {
- /** @type {import('./template-renderer.js').TemplateRenderer} */
- this._templateRenderer = templateRenderer;
- /** @type {import('template-renderer-proxy').FrontendApiMap} */
- this._windowMessageHandlers = createApiMap([
- ['render', this._onRender.bind(this)],
- ['renderMulti', this._onRenderMulti.bind(this)],
- ['getModifiedData', this._onGetModifiedData.bind(this)]
- ]);
- }
-
- /**
- * @returns {void}
- */
- prepare() {
- window.addEventListener('message', this._onWindowMessage.bind(this), false);
- this._postMessage(window.parent, 'ready', void 0, null);
- }
-
- // Private
-
- /**
- * @param {MessageEvent<import('template-renderer-proxy').FrontendMessageAny>} e
- */
- _onWindowMessage(e) {
- const {source, data: {action, params, id}} = e;
- invokeApiMapHandler(this._windowMessageHandlers, action, params, [], (response) => {
- this._postMessage(/** @type {Window} */ (source), 'response', response, id);
- });
- }
-
- /**
- * @param {{template: string, data: import('template-renderer').PartialOrCompositeRenderData, type: import('anki-templates').RenderMode}} event
- * @returns {import('template-renderer').RenderResult}
- */
- _onRender({template, data, type}) {
- return this._templateRenderer.render(template, data, type);
- }
-
- /**
- * @param {{items: import('template-renderer').RenderMultiItem[]}} event
- * @returns {import('core').Response<import('template-renderer').RenderResult>[]}
- */
- _onRenderMulti({items}) {
- return this._templateRenderer.renderMulti(items);
- }
-
- /**
- * @param {{data: import('template-renderer').CompositeRenderData, type: import('anki-templates').RenderMode}} event
- * @returns {import('anki-templates').NoteData}
- */
- _onGetModifiedData({data, type}) {
- const result = this._templateRenderer.getModifiedData(data, type);
- return this._clone(result);
- }
-
- /**
- * @template [T=unknown]
- * @param {T} value
- * @returns {T}
- */
- _clone(value) {
- return parseJson(JSON.stringify(value));
- }
-
- /**
- * @template {import('template-renderer-proxy').BackendApiNames} TName
- * @param {Window} target
- * @param {TName} action
- * @param {import('template-renderer-proxy').BackendApiParams<TName>} params
- * @param {?string} id
- */
- _postMessage(target, action, params, id) {
- /** @type {import('template-renderer-proxy').BackendMessageAny} */
- const data = {action, params, id};
- target.postMessage(data, '*');
- }
-}
diff --git a/ext/js/templates/sandbox/template-renderer-frame-main.js b/ext/js/templates/sandbox/template-renderer-frame-main.js
deleted file mode 100644
index 4ab7d2bc..00000000
--- a/ext/js/templates/sandbox/template-renderer-frame-main.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023-2024 Yomitan Authors
- * Copyright (C) 2020-2022 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/>.
- */
-
-import {AnkiTemplateRenderer} from './anki-template-renderer.js';
-import {TemplateRendererFrameApi} from './template-renderer-frame-api.js';
-
-/** Entry point. */
-async function main() {
- const ankiTemplateRenderer = new AnkiTemplateRenderer();
- await ankiTemplateRenderer.prepare();
- const templateRendererFrameApi = new TemplateRendererFrameApi(ankiTemplateRenderer.templateRenderer);
- templateRendererFrameApi.prepare();
-}
-
-await main();
diff --git a/ext/js/templates/sandbox/template-renderer-media-provider.js b/ext/js/templates/sandbox/template-renderer-media-provider.js
deleted file mode 100644
index 29dd29ae..00000000
--- a/ext/js/templates/sandbox/template-renderer-media-provider.js
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright (C) 2023-2024 Yomitan Authors
- * Copyright (C) 2021-2022 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/>.
- */
-
-import {Handlebars} from '../../../lib/handlebars.js';
-
-export class TemplateRendererMediaProvider {
- constructor() {
- /** @type {?import('anki-note-builder').Requirement[]} */
- this._requirements = null;
- }
-
- /** @type {?import('anki-note-builder').Requirement[]} */
- get requirements() {
- return this._requirements;
- }
-
- set requirements(value) {
- this._requirements = value;
- }
-
- /**
- * @param {import('anki-templates').NoteData} root
- * @param {unknown[]} args
- * @param {import('core').SerializableObject} namedArgs
- * @returns {boolean}
- */
- hasMedia(root, args, namedArgs) {
- const {media} = root;
- const data = this._getMediaData(media, args, namedArgs);
- return (data !== null);
- }
-
- /**
- * @param {import('anki-templates').NoteData} root
- * @param {unknown[]} args
- * @param {import('core').SerializableObject} namedArgs
- * @returns {?string}
- */
- getMedia(root, args, namedArgs) {
- const {media} = root;
- const data = this._getMediaData(media, args, namedArgs);
- if (data !== null) {
- const result = this._getFormattedValue(data, namedArgs);
- if (typeof result === 'string') { return result; }
- }
- const defaultValue = namedArgs.default;
- return defaultValue === null || typeof defaultValue === 'string' ? defaultValue : '';
- }
-
- // Private
-
- /**
- * @param {import('anki-note-builder').Requirement} value
- */
- _addRequirement(value) {
- if (this._requirements === null) { return; }
- this._requirements.push(value);
- }
-
- /**
- * @param {import('anki-templates').MediaObject} data
- * @param {import('core').SerializableObject} namedArgs
- * @returns {string}
- */
- _getFormattedValue(data, namedArgs) {
- let {value} = data;
- const {escape = true} = namedArgs;
- if (escape) {
- value = Handlebars.Utils.escapeExpression(value);
- }
- return value;
- }
-
- /**
- * @param {import('anki-templates').Media} media
- * @param {unknown[]} args
- * @param {import('core').SerializableObject} namedArgs
- * @returns {?(import('anki-templates').MediaObject)}
- */
- _getMediaData(media, args, namedArgs) {
- const type = args[0];
- switch (type) {
- case 'audio': return this._getSimpleMediaData(media, 'audio');
- case 'screenshot': return this._getSimpleMediaData(media, 'screenshot');
- case 'clipboardImage': return this._getSimpleMediaData(media, 'clipboardImage');
- case 'clipboardText': return this._getSimpleMediaData(media, 'clipboardText');
- case 'selectionText': return this._getSimpleMediaData(media, 'selectionText');
- case 'textFurigana': return this._getTextFurigana(media, args[1], namedArgs);
- case 'dictionaryMedia': return this._getDictionaryMedia(media, args[1], namedArgs);
- default: return null;
- }
- }
-
- /**
- * @param {import('anki-templates').Media} media
- * @param {import('anki-templates').MediaSimpleType} type
- * @returns {?import('anki-templates').MediaObject}
- */
- _getSimpleMediaData(media, type) {
- const result = media[type];
- if (typeof result === 'object' && result !== null) { return result; }
- this._addRequirement({type});
- return null;
- }
-
- /**
- * @param {import('anki-templates').Media} media
- * @param {unknown} path
- * @param {import('core').SerializableObject} namedArgs
- * @returns {?import('anki-templates').MediaObject}
- */
- _getDictionaryMedia(media, path, namedArgs) {
- if (typeof path !== 'string') { return null; }
- const {dictionaryMedia} = media;
- const {dictionary} = namedArgs;
- if (typeof dictionary !== 'string') { return null; }
- if (
- typeof dictionaryMedia !== 'undefined' &&
- Object.prototype.hasOwnProperty.call(dictionaryMedia, dictionary)
- ) {
- const dictionaryMedia2 = dictionaryMedia[dictionary];
- if (Object.prototype.hasOwnProperty.call(dictionaryMedia2, path)) {
- const result = dictionaryMedia2[path];
- if (typeof result === 'object' && result !== null) {
- return result;
- }
- }
- }
- this._addRequirement({
- type: 'dictionaryMedia',
- dictionary,
- path
- });
- return null;
- }
-
- /**
- * @param {import('anki-templates').Media} media
- * @param {unknown} text
- * @param {import('core').SerializableObject} namedArgs
- * @returns {?import('anki-templates').MediaObject}
- */
- _getTextFurigana(media, text, namedArgs) {
- if (typeof text !== 'string') { return null; }
- const readingMode = this._normalizeReadingMode(namedArgs.readingMode);
- const {textFurigana} = media;
- if (Array.isArray(textFurigana)) {
- for (const entry of textFurigana) {
- if (entry.text !== text || entry.readingMode !== readingMode) { continue; }
- return entry.details;
- }
- }
- this._addRequirement({
- type: 'textFurigana',
- text,
- readingMode
- });
- return null;
- }
-
- /**
- * @param {unknown} value
- * @returns {?import('anki-templates').TextFuriganaReadingMode}
- */
- _normalizeReadingMode(value) {
- switch (value) {
- case 'hiragana':
- case 'katakana':
- return value;
- default:
- return null;
- }
- }
-}
diff --git a/ext/js/templates/sandbox/template-renderer.js b/ext/js/templates/sandbox/template-renderer.js
deleted file mode 100644
index 84eb6a19..00000000
--- a/ext/js/templates/sandbox/template-renderer.js
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright (C) 2023-2024 Yomitan Authors
- * Copyright (C) 2016-2022 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/>.
- */
-
-import {Handlebars} from '../../../lib/handlebars.js';
-import {ExtensionError} from '../../core/extension-error.js';
-
-export class TemplateRenderer {
- constructor() {
- /** @type {Map<string, import('handlebars').TemplateDelegate<import('anki-templates').NoteData>>} */
- this._cache = new Map();
- /** @type {number} */
- this._cacheMaxSize = 5;
- /** @type {Map<import('anki-templates').RenderMode, import('template-renderer').DataType>} */
- this._dataTypes = new Map();
- /** @type {?((noteData: import('anki-templates').NoteData) => import('template-renderer').SetupCallbackResult)} */
- this._renderSetup = null;
- /** @type {?((noteData: import('anki-templates').NoteData) => import('template-renderer').CleanupCallbackResult)} */
- this._renderCleanup = null;
- }
-
- /**
- * @param {import('template-renderer').HelperFunctionsDescriptor} helpers
- */
- registerHelpers(helpers) {
- for (const [name, helper] of helpers) {
- this._registerHelper(name, helper);
- }
- }
-
- /**
- * @param {import('anki-templates').RenderMode} name
- * @param {import('template-renderer').DataType} details
- */
- registerDataType(name, {modifier, composeData}) {
- this._dataTypes.set(name, {modifier, composeData});
- }
-
- /**
- * @param {?((noteData: import('anki-templates').NoteData) => import('template-renderer').SetupCallbackResult)} setup
- * @param {?((noteData: import('anki-templates').NoteData) => import('template-renderer').CleanupCallbackResult)} cleanup
- */
- setRenderCallbacks(setup, cleanup) {
- this._renderSetup = setup;
- this._renderCleanup = cleanup;
- }
-
- /**
- * @param {string} template
- * @param {import('template-renderer').PartialOrCompositeRenderData} data
- * @param {import('anki-templates').RenderMode} type
- * @returns {import('template-renderer').RenderResult}
- */
- render(template, data, type) {
- const instance = this._getTemplateInstance(template);
- const modifiedData = this._getModifiedData(data, void 0, type);
- return this._renderTemplate(instance, modifiedData);
- }
-
- /**
- * @param {import('template-renderer').RenderMultiItem[]} items
- * @returns {import('core').Response<import('template-renderer').RenderResult>[]}
- */
- renderMulti(items) {
- /** @type {import('core').Response<import('template-renderer').RenderResult>[]} */
- const results = [];
- for (const {template, templateItems} of items) {
- const instance = this._getTemplateInstance(template);
- for (const {type, commonData, datas} of templateItems) {
- for (const data of datas) {
- let result;
- try {
- const data2 = this._getModifiedData(data, commonData, type);
- const renderResult = this._renderTemplate(instance, data2);
- result = {result: renderResult};
- } catch (error) {
- result = {error: ExtensionError.serialize(error)};
- }
- results.push(result);
- }
- }
- }
- return results;
- }
-
- /**
- * @param {import('template-renderer').CompositeRenderData} data
- * @param {import('anki-templates').RenderMode} type
- * @returns {import('anki-templates').NoteData}
- */
- getModifiedData(data, type) {
- return this._getModifiedData(data, void 0, type);
- }
-
- // Private
-
- /**
- * @param {string} template
- * @returns {import('handlebars').TemplateDelegate<import('anki-templates').NoteData>}
- */
- _getTemplateInstance(template) {
- const cache = this._cache;
- let instance = cache.get(template);
- if (typeof instance === 'undefined') {
- this._updateCacheSize(this._cacheMaxSize - 1);
- instance = /** @type {import('handlebars').TemplateDelegate<import('anki-templates').NoteData>} */ (Handlebars.compileAST(template));
- cache.set(template, instance);
- }
-
- return instance;
- }
-
- /**
- * @param {import('handlebars').TemplateDelegate<import('anki-templates').NoteData>} instance
- * @param {import('anki-templates').NoteData} data
- * @returns {import('template-renderer').RenderResult}
- */
- _renderTemplate(instance, data) {
- const renderSetup = this._renderSetup;
- const renderCleanup = this._renderCleanup;
- /** @type {string} */
- let result;
- /** @type {?import('template-renderer').SetupCallbackResult} */
- let additions1;
- /** @type {?import('template-renderer').CleanupCallbackResult} */
- let additions2;
- try {
- additions1 = (typeof renderSetup === 'function' ? renderSetup(data) : null);
- result = instance(data).replace(/^\n+|\n+$/g, '');
- } finally {
- additions2 = (typeof renderCleanup === 'function' ? renderCleanup(data) : null);
- }
- return /** @type {import('template-renderer').RenderResult} */ (Object.assign({result}, additions1, additions2));
- }
-
- /**
- * @param {import('template-renderer').PartialOrCompositeRenderData} data
- * @param {import('anki-note-builder').CommonData|undefined} commonData
- * @param {import('anki-templates').RenderMode} type
- * @returns {import('anki-templates').NoteData}
- * @throws {Error}
- */
- _getModifiedData(data, commonData, type) {
- if (typeof type === 'string') {
- const typeInfo = this._dataTypes.get(type);
- if (typeof typeInfo !== 'undefined') {
- if (typeof commonData !== 'undefined') {
- const {composeData} = typeInfo;
- data = composeData(data, commonData);
- } else if (typeof data.commonData === 'undefined') {
- throw new Error('Incomplete data');
- }
- const {modifier} = typeInfo;
- return modifier(/** @type {import('template-renderer').CompositeRenderData} */ (data));
- }
- }
- throw new Error(`Invalid type: ${type}`);
- }
-
- /**
- * @param {number} maxSize
- */
- _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; }
- }
- }
-
- /**
- * @param {string} name
- * @param {import('template-renderer').HelperFunction} helper
- */
- _registerHelper(name, helper) {
- /**
- * @this {unknown}
- * @param {unknown[]} args
- * @returns {unknown}
- */
- function wrapper(...args) {
- const argCountM1 = Math.max(0, args.length - 1);
- const options = /** @type {import('handlebars').HelperOptions} */ (args[argCountM1]);
- args.length = argCountM1;
- return helper(args, this, options);
- }
- Handlebars.registerHelper(name, wrapper);
- }
-}