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:
parent
10a9da4d31
commit
637d4a2087
@ -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.
|
||||
|
@ -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';
|
||||
|
@ -144,7 +144,7 @@ class PronunciationGenerator {
|
||||
return svg;
|
||||
}
|
||||
|
||||
createPronunciationDownstepNotation(downstepPosition) {
|
||||
createPronunciationDownstepPosition(downstepPosition) {
|
||||
downstepPosition = `${downstepPosition}`;
|
||||
|
||||
const n1 = document.createElement('span');
|
||||
|
@ -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)) {
|
||||
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user