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
This commit is contained in:
toasted-nutbread 2021-07-18 13:43:11 -04:00 committed by GitHub
parent 10a9da4d31
commit 637d4a2087
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 91 additions and 14 deletions

View File

@ -3,7 +3,7 @@
## Helpers ## Helpers
Yomichan supports several custom Handlebars helpers for rendering templates. 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` ### `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. The type of media to check for.
* _`args`_ <br> * _`args`_ <br>
Additional arguments for the media. The arguments depend on the media type. Additional arguments for the media. The arguments depend on the media type.
* _`escape`_ <br> * _`escape`_ _(optional)_ <br>
Whether or not the resulting text should be HTML-escaped. If omitted, defaults to `true`. Whether or not the resulting text should be HTML-escaped. If omitted, defaults to `true`.
**Available media types and arguments** **Available media types and arguments**
@ -742,6 +742,39 @@ These functions are used together in order to request media and other types of o
</details> </details>
### `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.
<details>
<summary>Syntax:</summary>
<code>{{#pronunciation <i>format=string</i> <i>reading=string</i> <i>downstepPosition=integer</i> <i>[nasalPositions=array]</i> <i>[devoicePositions=array]</i>}}{{/pronunciation}}</code><br>
* _`format`_ <br>
The format of the HTML to generate. This can be any of the following values:
* `'text'`
* `'graph'`
* `'position'`
* _`reading`_ <br>
The kana reading of the term.
* _`downstepPosition`_ <br>
The mora position of the downstep in the reading.
* _`nasalPositions`_ _(optional)_ <br>
An array of indices of mora that have a nasal pronunciation.
* _`devoicePositions`_ _(optional)_ <br>
An array of indices of mora that are devoiced.
</details>
<details>
<summary>Example:</summary>
```handlebars
{{~#pronunciation format='text' reading='よむ' downstepPosition=1~}}{{~/pronunciation~}}
```
</details>
## Legacy Helpers ## Legacy Helpers
Yomichan has historically used Handlebars templates to generate the HTML used on the search page and results popup. Yomichan has historically used Handlebars templates to generate the HTML used on the search page and results popup.

View File

@ -477,7 +477,7 @@ class DisplayGenerator {
this._createPitchAccentDisambiguations(n, exclusiveTerms, exclusiveReadings); this._createPitchAccentDisambiguations(n, exclusiveTerms, exclusiveReadings);
n = node.querySelector('.pronunciation-downstep-notation-container'); 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 = node.querySelector('.pronunciation-text-container');
n.lang = 'ja'; n.lang = 'ja';

View File

@ -144,7 +144,7 @@ class PronunciationGenerator {
return svg; return svg;
} }
createPronunciationDownstepNotation(downstepPosition) { createPronunciationDownstepPosition(downstepPosition) {
downstepPosition = `${downstepPosition}`; downstepPosition = `${downstepPosition}`;
const n1 = document.createElement('span'); const n1 = document.createElement('span');

View File

@ -54,7 +54,7 @@ class CssStyleApplier {
applyClassStyles(elements) { applyClassStyles(elements) {
const elementStyles = []; const elementStyles = [];
for (const element of elements) { for (const element of elements) {
const {className} = element; const className = element.getAttribute('class');
if (className.length === 0) { continue; } if (className.length === 0) { continue; }
let cssTextNew = ''; let cssTextNew = '';
for (const {selectorText, styles} of this._getRulesForClass(className)) { for (const {selectorText, styles} of this._getRulesForClass(className)) {

View File

@ -21,6 +21,7 @@
* DictionaryDataUtil * DictionaryDataUtil
* Handlebars * Handlebars
* JapaneseUtil * JapaneseUtil
* PronunciationGenerator
* StructuredContentGenerator * StructuredContentGenerator
* TemplateRenderer * TemplateRenderer
* TemplateRendererMediaProvider * TemplateRendererMediaProvider
@ -35,11 +36,13 @@ class AnkiTemplateRenderer {
* Creates a new instance of the class. * Creates a new instance of the class.
*/ */
constructor() { 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._japaneseUtil = new JapaneseUtil(null);
this._templateRenderer = new TemplateRenderer(); this._templateRenderer = new TemplateRenderer();
this._ankiNoteDataCreator = new AnkiNoteDataCreator(this._japaneseUtil); this._ankiNoteDataCreator = new AnkiNoteDataCreator(this._japaneseUtil);
this._mediaProvider = new TemplateRendererMediaProvider(); this._mediaProvider = new TemplateRendererMediaProvider();
this._pronunciationGenerator = new PronunciationGenerator(this._japaneseUtil);
this._stateStack = null; this._stateStack = null;
this._requirements = null; this._requirements = null;
this._cleanupCallbacks = null; this._cleanupCallbacks = null;
@ -83,7 +86,8 @@ class AnkiTemplateRenderer {
['pitchCategories', this._pitchCategories.bind(this)], ['pitchCategories', this._pitchCategories.bind(this)],
['formatGlossary', this._formatGlossary.bind(this)], ['formatGlossary', this._formatGlossary.bind(this)],
['hasMedia', this._hasMedia.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', { this._templateRenderer.registerDataType('ankiNote', {
modifier: ({marker, commonData}) => this._ankiNoteDataCreator.create(marker, commonData), modifier: ({marker, commonData}) => this._ankiNoteDataCreator.create(marker, commonData),
@ -93,7 +97,10 @@ class AnkiTemplateRenderer {
this._onRenderSetup.bind(this), this._onRenderSetup.bind(this),
this._onRenderCleanup.bind(this) this._onRenderCleanup.bind(this)
); );
await this._cssStyleApplier.prepare(); await Promise.all([
this._structuredContentStyleApplier.prepare(),
this._pronunciationStyleApplier.prepare()
]);
} }
// Private // Private
@ -453,16 +460,16 @@ class AnkiTemplateRenderer {
return element; return element;
} }
_getHtml(node) { _getHtml(node, styleApplier) {
const container = this._getTemporaryElement(); const container = this._getTemporaryElement();
container.appendChild(node); container.appendChild(node);
this._normalizeHtml(container); this._normalizeHtml(container, styleApplier);
const result = container.innerHTML; const result = container.innerHTML;
container.textContent = ''; container.textContent = '';
return result; return result;
} }
_normalizeHtml(root) { _normalizeHtml(root, styleApplier) {
const {ELEMENT_NODE, TEXT_NODE} = Node; const {ELEMENT_NODE, TEXT_NODE} = Node;
const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
const elements = []; const elements = [];
@ -479,7 +486,7 @@ class AnkiTemplateRenderer {
break; break;
} }
} }
this._cssStyleApplier.applyClassStyles(elements); styleApplier.applyClassStyles(elements);
for (const element of elements) { for (const element of elements) {
const {dataset} = element; const {dataset} = element;
for (const key of Object.keys(dataset)) { for (const key of Object.keys(dataset)) {
@ -532,13 +539,13 @@ class AnkiTemplateRenderer {
_formatGlossaryImage(content, dictionary, data) { _formatGlossaryImage(content, dictionary, data) {
const structuredContentGenerator = this._createStructuredContentGenerator(data); const structuredContentGenerator = this._createStructuredContentGenerator(data);
const node = structuredContentGenerator.createDefinitionImage(content, dictionary); const node = structuredContentGenerator.createDefinitionImage(content, dictionary);
return this._getHtml(node); return this._getHtml(node, this._structuredContentStyleApplier);
} }
_formatStructuredContent(content, dictionary, data) { _formatStructuredContent(content, dictionary, data) {
const structuredContentGenerator = this._createStructuredContentGenerator(data); const structuredContentGenerator = this._createStructuredContentGenerator(data);
const node = structuredContentGenerator.createStructuredContent(content.content, dictionary); const node = structuredContentGenerator.createStructuredContent(content.content, dictionary);
return node !== null ? this._getHtml(node) : ''; return node !== null ? this._getHtml(node, this._structuredContentStyleApplier) : '';
} }
_hasMedia(context, ...args) { _hasMedia(context, ...args) {
@ -552,4 +559,36 @@ class AnkiTemplateRenderer {
const options = args[ii]; const options = args[ii];
return this._mediaProvider.getMedia(options.data.root, args.slice(0, ii), options.hash); 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 '';
}
}
} }

View File

@ -18,6 +18,7 @@
<script src="/lib/handlebars.min.js"></script> <script src="/lib/handlebars.min.js"></script>
<script src="/js/data/sandbox/anki-note-data-creator.js"></script> <script src="/js/data/sandbox/anki-note-data-creator.js"></script>
<script src="/js/display/sandbox/pronunciation-generator.js"></script>
<script src="/js/display/sandbox/structured-content-generator.js"></script> <script src="/js/display/sandbox/structured-content-generator.js"></script>
<script src="/js/dom/sandbox/css-style-applier.js"></script> <script src="/js/dom/sandbox/css-style-applier.js"></script>
<script src="/js/language/sandbox/dictionary-data-util.js"></script> <script src="/js/language/sandbox/dictionary-data-util.js"></script>

View File

@ -44,6 +44,7 @@ async function createVM() {
'js/data/anki-note-builder.js', 'js/data/anki-note-builder.js',
'js/data/anki-util.js', 'js/data/anki-util.js',
'js/dom/sandbox/css-style-applier.js', 'js/dom/sandbox/css-style-applier.js',
'js/display/sandbox/pronunciation-generator.js',
'js/display/sandbox/structured-content-generator.js', 'js/display/sandbox/structured-content-generator.js',
'js/templates/sandbox/anki-template-renderer.js', 'js/templates/sandbox/anki-template-renderer.js',
'js/templates/sandbox/template-renderer.js', 'js/templates/sandbox/template-renderer.js',
@ -234,6 +235,9 @@ async function getRenderResults(dictionaryEntries, type, mode, template, AnkiNot
compactTags: false compactTags: false
}); });
if (!write) { if (!write) {
for (const error of errors) {
console.error(error);
}
assert.strictEqual(errors.length, 0); assert.strictEqual(errors.length, 0);
} }
results.push(noteFields); results.push(noteFields);