From 637d4a2087b9e93ccd47d689411887b6c40c3992 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 18 Jul 2021 13:43:11 -0400 Subject: [PATCH] Pronunciation template helper (#1840) * Rename field * Set up pronunication components * Fix documentation * Rename function * Update test dependencies * Fix constructor * Log errors * Add pronunciation helper * Add styleApplier argument to _getHtml/_normalizeHtml * Use getAttribute for 'class' to support namespaced elements (e.g. svg) * Update format name * Add optional tag * Update docs --- docs/templates.md | 37 +++++++++++- ext/js/display/display-generator.js | 2 +- .../sandbox/pronunciation-generator.js | 2 +- ext/js/dom/sandbox/css-style-applier.js | 2 +- .../sandbox/anki-template-renderer.js | 57 ++++++++++++++++--- ext/template-renderer.html | 1 + test/test-anki-note-builder.js | 4 ++ 7 files changed, 91 insertions(+), 14 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index 8ee3bb0c..f1777fde 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -3,7 +3,7 @@ ## Helpers Yomichan supports several custom Handlebars helpers for rendering templates. -The source code for these templates can be found [here](../ext/js/templates/sandbox/template-renderer.js). +The source code for these templates can be found [here](../ext/js/templates/sandbox/anki-template-renderer.js). ### `dumpObject` @@ -689,7 +689,7 @@ These functions are used together in order to request media and other types of o The type of media to check for. * _`args`_
Additional arguments for the media. The arguments depend on the media type. - * _`escape`_
+ * _`escape`_ _(optional)_
Whether or not the resulting text should be HTML-escaped. If omitted, defaults to `true`. **Available media types and arguments** @@ -742,6 +742,39 @@ These functions are used together in order to request media and other types of o +### `pronunciation` + +Converts pronunciation information into a formatted HTML content string. The display layout is the +same as the system used for generating popup and search page dictionary entries. + +
+ Syntax: + + {{#pronunciation format=string reading=string downstepPosition=integer [nasalPositions=array] [devoicePositions=array]}}{{/pronunciation}}
+ + * _`format`_
+ The format of the HTML to generate. This can be any of the following values: + * `'text'` + * `'graph'` + * `'position'` + * _`reading`_
+ The kana reading of the term. + * _`downstepPosition`_
+ The mora position of the downstep in the reading. + * _`nasalPositions`_ _(optional)_
+ An array of indices of mora that have a nasal pronunciation. + * _`devoicePositions`_ _(optional)_
+ An array of indices of mora that are devoiced. +
+
+ Example: + + ```handlebars + {{~#pronunciation format='text' reading='よむ' downstepPosition=1~}}{{~/pronunciation~}} + ``` +
+ + ## Legacy Helpers Yomichan has historically used Handlebars templates to generate the HTML used on the search page and results popup. diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js index 2421b88a..11a0a9d3 100644 --- a/ext/js/display/display-generator.js +++ b/ext/js/display/display-generator.js @@ -477,7 +477,7 @@ class DisplayGenerator { this._createPitchAccentDisambiguations(n, exclusiveTerms, exclusiveReadings); n = node.querySelector('.pronunciation-downstep-notation-container'); - n.appendChild(this._pronunciationGenerator.createPronunciationDownstepNotation(position)); + n.appendChild(this._pronunciationGenerator.createPronunciationDownstepPosition(position)); n = node.querySelector('.pronunciation-text-container'); n.lang = 'ja'; diff --git a/ext/js/display/sandbox/pronunciation-generator.js b/ext/js/display/sandbox/pronunciation-generator.js index bab36add..3739e716 100644 --- a/ext/js/display/sandbox/pronunciation-generator.js +++ b/ext/js/display/sandbox/pronunciation-generator.js @@ -144,7 +144,7 @@ class PronunciationGenerator { return svg; } - createPronunciationDownstepNotation(downstepPosition) { + createPronunciationDownstepPosition(downstepPosition) { downstepPosition = `${downstepPosition}`; const n1 = document.createElement('span'); diff --git a/ext/js/dom/sandbox/css-style-applier.js b/ext/js/dom/sandbox/css-style-applier.js index c617fead..84b8450b 100644 --- a/ext/js/dom/sandbox/css-style-applier.js +++ b/ext/js/dom/sandbox/css-style-applier.js @@ -54,7 +54,7 @@ class CssStyleApplier { applyClassStyles(elements) { const elementStyles = []; for (const element of elements) { - const {className} = element; + const className = element.getAttribute('class'); if (className.length === 0) { continue; } let cssTextNew = ''; for (const {selectorText, styles} of this._getRulesForClass(className)) { diff --git a/ext/js/templates/sandbox/anki-template-renderer.js b/ext/js/templates/sandbox/anki-template-renderer.js index 907ab0fa..43092ec2 100644 --- a/ext/js/templates/sandbox/anki-template-renderer.js +++ b/ext/js/templates/sandbox/anki-template-renderer.js @@ -21,6 +21,7 @@ * DictionaryDataUtil * Handlebars * JapaneseUtil + * PronunciationGenerator * StructuredContentGenerator * TemplateRenderer * TemplateRendererMediaProvider @@ -35,11 +36,13 @@ class AnkiTemplateRenderer { * Creates a new instance of the class. */ constructor() { - this._cssStyleApplier = new CssStyleApplier('/data/structured-content-style.json'); + this._structuredContentStyleApplier = new CssStyleApplier('/data/structured-content-style.json'); + this._pronunciationStyleApplier = new CssStyleApplier('/data/pronunciation-style.json'); this._japaneseUtil = new JapaneseUtil(null); this._templateRenderer = new TemplateRenderer(); this._ankiNoteDataCreator = new AnkiNoteDataCreator(this._japaneseUtil); this._mediaProvider = new TemplateRendererMediaProvider(); + this._pronunciationGenerator = new PronunciationGenerator(this._japaneseUtil); this._stateStack = null; this._requirements = null; this._cleanupCallbacks = null; @@ -83,7 +86,8 @@ class AnkiTemplateRenderer { ['pitchCategories', this._pitchCategories.bind(this)], ['formatGlossary', this._formatGlossary.bind(this)], ['hasMedia', this._hasMedia.bind(this)], - ['getMedia', this._getMedia.bind(this)] + ['getMedia', this._getMedia.bind(this)], + ['pronunciation', this._pronunciation.bind(this)] ]); this._templateRenderer.registerDataType('ankiNote', { modifier: ({marker, commonData}) => this._ankiNoteDataCreator.create(marker, commonData), @@ -93,7 +97,10 @@ class AnkiTemplateRenderer { this._onRenderSetup.bind(this), this._onRenderCleanup.bind(this) ); - await this._cssStyleApplier.prepare(); + await Promise.all([ + this._structuredContentStyleApplier.prepare(), + this._pronunciationStyleApplier.prepare() + ]); } // Private @@ -453,16 +460,16 @@ class AnkiTemplateRenderer { return element; } - _getHtml(node) { + _getHtml(node, styleApplier) { const container = this._getTemporaryElement(); container.appendChild(node); - this._normalizeHtml(container); + this._normalizeHtml(container, styleApplier); const result = container.innerHTML; container.textContent = ''; return result; } - _normalizeHtml(root) { + _normalizeHtml(root, styleApplier) { const {ELEMENT_NODE, TEXT_NODE} = Node; const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); const elements = []; @@ -479,7 +486,7 @@ class AnkiTemplateRenderer { break; } } - this._cssStyleApplier.applyClassStyles(elements); + styleApplier.applyClassStyles(elements); for (const element of elements) { const {dataset} = element; for (const key of Object.keys(dataset)) { @@ -532,13 +539,13 @@ class AnkiTemplateRenderer { _formatGlossaryImage(content, dictionary, data) { const structuredContentGenerator = this._createStructuredContentGenerator(data); const node = structuredContentGenerator.createDefinitionImage(content, dictionary); - return this._getHtml(node); + return this._getHtml(node, this._structuredContentStyleApplier); } _formatStructuredContent(content, dictionary, data) { const structuredContentGenerator = this._createStructuredContentGenerator(data); const node = structuredContentGenerator.createStructuredContent(content.content, dictionary); - return node !== null ? this._getHtml(node) : ''; + return node !== null ? this._getHtml(node, this._structuredContentStyleApplier) : ''; } _hasMedia(context, ...args) { @@ -552,4 +559,36 @@ class AnkiTemplateRenderer { const options = args[ii]; return this._mediaProvider.getMedia(options.data.root, args.slice(0, ii), options.hash); } + + _pronunciation(context, ...args) { + const ii = args.length - 1; + const options = args[ii]; + let {format, reading, downstepPosition, nasalPositions, devoicePositions} = options.hash; + + if (typeof reading !== 'string' || reading.length === 0) { return ''; } + if (typeof downstepPosition !== 'number') { return ''; } + if (!Array.isArray(nasalPositions)) { nasalPositions = []; } + if (!Array.isArray(devoicePositions)) { devoicePositions = []; } + const morae = this._japaneseUtil.getKanaMorae(reading); + + switch (format) { + case 'text': + return this._getHtml( + this._pronunciationGenerator.createPronunciationText(morae, downstepPosition, nasalPositions, devoicePositions), + this._pronunciationStyleApplier + ); + case 'graph': + return this._getHtml( + this._pronunciationGenerator.createPronunciationGraph(morae, downstepPosition), + this._pronunciationStyleApplier + ); + case 'position': + return this._getHtml( + this._pronunciationGenerator.createPronunciationDownstepPosition(downstepPosition), + this._pronunciationStyleApplier + ); + default: + return ''; + } + } } diff --git a/ext/template-renderer.html b/ext/template-renderer.html index 2db61d05..19595ec4 100644 --- a/ext/template-renderer.html +++ b/ext/template-renderer.html @@ -18,6 +18,7 @@ + diff --git a/test/test-anki-note-builder.js b/test/test-anki-note-builder.js index b8002c4b..45087134 100644 --- a/test/test-anki-note-builder.js +++ b/test/test-anki-note-builder.js @@ -44,6 +44,7 @@ async function createVM() { 'js/data/anki-note-builder.js', 'js/data/anki-util.js', 'js/dom/sandbox/css-style-applier.js', + 'js/display/sandbox/pronunciation-generator.js', 'js/display/sandbox/structured-content-generator.js', 'js/templates/sandbox/anki-template-renderer.js', 'js/templates/sandbox/template-renderer.js', @@ -234,6 +235,9 @@ async function getRenderResults(dictionaryEntries, type, mode, template, AnkiNot compactTags: false }); if (!write) { + for (const error of errors) { + console.error(error); + } assert.strictEqual(errors.length, 0); } results.push(noteFields);