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
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`_ <br>
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`.
**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>
### `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
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);
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';

View File

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

View File

@ -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)) {

View File

@ -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 '';
}
}
}

View File

@ -18,6 +18,7 @@
<script src="/lib/handlebars.min.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/dom/sandbox/css-style-applier.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-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);