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
|
## 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.
|
||||||
|
@ -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';
|
||||||
|
@ -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');
|
||||||
|
@ -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)) {
|
||||||
|
@ -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 '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user