From 5dcc2315d242bcec29cc478618d448c941f73ab1 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 14 May 2022 18:12:57 -0400 Subject: [PATCH] Structured content auto language (#2131) * Pass JapaneseUtil instance to StructuredContentGenerator * Move body of createStructuredContent to an internal function * Create _createStructuredContentGenericElement * Wrap structured content in a * Change _createStructuredContent to _appendStructuredContent * Add public appendStructuredContent function * Add missing return * Remove unused _createDocumentFragment * Automatically assign lang=ja for content with Japanese characters without an explicit language * Add test --- ext/js/display/display-generator.js | 9 +- .../sandbox/structured-content-generator.js | 141 ++++++++++-------- .../sandbox/anki-template-renderer.js | 2 +- .../valid-dictionary1/term_bank_1.json | 1 + 4 files changed, 87 insertions(+), 66 deletions(-) diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js index 1f485a4f..3fabdbb0 100644 --- a/ext/js/display/display-generator.js +++ b/ext/js/display/display-generator.js @@ -28,7 +28,7 @@ class DisplayGenerator { this._contentManager = contentManager; this._hotkeyHelpController = hotkeyHelpController; this._templates = null; - this._structuredContentGenerator = new StructuredContentGenerator(this._contentManager, document); + this._structuredContentGenerator = new StructuredContentGenerator(this._contentManager, japaneseUtil, document); this._pronunciationGenerator = new PronunciationGenerator(japaneseUtil); } @@ -347,11 +347,8 @@ class DisplayGenerator { _createTermDefinitionEntryStructuredContent(content, dictionary) { const node = this._templates.instantiate('gloss-item'); - const child = this._structuredContentGenerator.createStructuredContent(content, dictionary); - if (child !== null) { - const contentContainer = node.querySelector('.gloss-content'); - contentContainer.appendChild(child); - } + const contentContainer = node.querySelector('.gloss-content'); + this._structuredContentGenerator.appendStructuredContent(contentContainer, content, dictionary); return node; } diff --git a/ext/js/display/sandbox/structured-content-generator.js b/ext/js/display/sandbox/structured-content-generator.js index 6102cfdd..5b11965a 100644 --- a/ext/js/display/sandbox/structured-content-generator.js +++ b/ext/js/display/sandbox/structured-content-generator.js @@ -16,56 +16,21 @@ */ class StructuredContentGenerator { - constructor(contentManager, document) { + constructor(contentManager, japaneseUtil, document) { this._contentManager = contentManager; + this._japaneseUtil = japaneseUtil; this._document = document; } + appendStructuredContent(node, content, dictionary) { + node.classList.add('structured-content'); + this._appendStructuredContent(node, content, dictionary, null); + } + createStructuredContent(content, dictionary) { - if (typeof content === 'string') { - return this._createTextNode(content); - } - if (!(typeof content === 'object' && content !== null)) { - return null; - } - if (Array.isArray(content)) { - const fragment = this._createDocumentFragment(); - for (const item of content) { - const child = this.createStructuredContent(item, dictionary); - if (child !== null) { fragment.appendChild(child); } - } - return fragment; - } - const {tag} = content; - switch (tag) { - case 'br': - return this._createStructuredContentElement(tag, content, dictionary, 'simple', false, false); - case 'ruby': - case 'rt': - case 'rp': - return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, false); - case 'table': - return this._createStructuredContentTableElement(tag, content, dictionary); - case 'thead': - case 'tbody': - case 'tfoot': - case 'tr': - return this._createStructuredContentElement(tag, content, dictionary, 'table', true, false); - case 'th': - case 'td': - return this._createStructuredContentElement(tag, content, dictionary, 'table-cell', true, true); - case 'div': - case 'span': - case 'ol': - case 'ul': - case 'li': - return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, true); - case 'img': - return this.createDefinitionImage(content, dictionary); - case 'a': - return this._createLinkElement(content, dictionary); - } - return null; + const node = this._createElement('span', 'structured-content'); + this._appendStructuredContent(node, content, dictionary, null); + return node; } createDefinitionImage(data, dictionary) { @@ -160,6 +125,31 @@ class StructuredContentGenerator { // Private + _appendStructuredContent(container, content, dictionary, language) { + if (typeof content === 'string') { + if (content.length > 0) { + container.appendChild(this._createTextNode(content)); + if (language === null && this._japaneseUtil.isStringPartiallyJapanese(content)) { + container.lang = 'ja'; + } + } + return; + } + if (!(typeof content === 'object' && content !== null)) { + return; + } + if (Array.isArray(content)) { + for (const item of content) { + this._appendStructuredContent(container, item, dictionary, language); + } + return; + } + const node = this._createStructuredContentGenericElement(content, dictionary, language); + if (node !== null) { + container.appendChild(node); + } + } + _createElement(tagName, className) { const node = this._document.createElement(tagName); node.className = className; @@ -170,10 +160,6 @@ class StructuredContentGenerator { return this._document.createTextNode(data); } - _createDocumentFragment() { - return this._document.createDocumentFragment(); - } - _setElementDataset(element, data) { for (let [key, value] of Object.entries(data)) { if (key.length > 0) { @@ -198,18 +184,54 @@ class StructuredContentGenerator { } } - _createStructuredContentTableElement(tag, content, dictionary) { + _createStructuredContentGenericElement(content, dictionary, language) { + const {tag} = content; + switch (tag) { + case 'br': + return this._createStructuredContentElement(tag, content, dictionary, language, 'simple', false, false); + case 'ruby': + case 'rt': + case 'rp': + return this._createStructuredContentElement(tag, content, dictionary, language, 'simple', true, false); + case 'table': + return this._createStructuredContentTableElement(tag, content, dictionary, language); + case 'thead': + case 'tbody': + case 'tfoot': + case 'tr': + return this._createStructuredContentElement(tag, content, dictionary, language, 'table', true, false); + case 'th': + case 'td': + return this._createStructuredContentElement(tag, content, dictionary, language, 'table-cell', true, true); + case 'div': + case 'span': + case 'ol': + case 'ul': + case 'li': + return this._createStructuredContentElement(tag, content, dictionary, language, 'simple', true, true); + case 'img': + return this.createDefinitionImage(content, dictionary); + case 'a': + return this._createLinkElement(content, dictionary, language); + } + return null; + } + + _createStructuredContentTableElement(tag, content, dictionary, language) { const container = this._createElement('div', 'gloss-sc-table-container'); - const table = this._createStructuredContentElement(tag, content, dictionary, 'table', true, false); + const table = this._createStructuredContentElement(tag, content, dictionary, language, 'table', true, false); container.appendChild(table); return container; } - _createStructuredContentElement(tag, content, dictionary, type, hasChildren, hasStyle) { + _createStructuredContentElement(tag, content, dictionary, language, type, hasChildren, hasStyle) { const node = this._createElement(tag, `gloss-sc-${tag}`); const {data, lang} = content; if (typeof data === 'object' && data !== null) { this._setElementDataset(node, data); } - if (typeof lang === 'string') { node.lang = lang; } + if (typeof lang === 'string') { + node.lang = lang; + language = lang; + } switch (type) { case 'table-cell': { @@ -226,8 +248,7 @@ class StructuredContentGenerator { } } if (hasChildren) { - const child = this.createStructuredContent(content.content, dictionary); - if (child !== null) { node.appendChild(child); } + this._appendStructuredContent(node, content.content, dictionary, language); } return node; } @@ -262,7 +283,7 @@ class StructuredContentGenerator { if (typeof listStyleType === 'string') { style.listStyleType = listStyleType; } } - _createLinkElement(content, dictionary) { + _createLinkElement(content, dictionary, language) { let {href} = content; const internal = href.startsWith('?'); if (internal) { @@ -276,10 +297,12 @@ class StructuredContentGenerator { node.appendChild(text); const {lang} = content; - if (typeof lang === 'string') { node.lang = lang; } + if (typeof lang === 'string') { + node.lang = lang; + language = lang; + } - const child = this.createStructuredContent(content.content, dictionary); - if (child !== null) { text.appendChild(child); } + this._appendStructuredContent(text, content.content, dictionary, language); if (!internal) { const icon = this._createElement('span', 'gloss-link-external-icon icon'); diff --git a/ext/js/templates/sandbox/anki-template-renderer.js b/ext/js/templates/sandbox/anki-template-renderer.js index c75f92d3..45dda06c 100644 --- a/ext/js/templates/sandbox/anki-template-renderer.js +++ b/ext/js/templates/sandbox/anki-template-renderer.js @@ -536,7 +536,7 @@ class AnkiTemplateRenderer { _createStructuredContentGenerator(data) { const contentManager = new AnkiTemplateRendererContentManager(this._mediaProvider, data); - const instance = new StructuredContentGenerator(contentManager, document); + const instance = new StructuredContentGenerator(contentManager, this._japaneseUtil, document); this._cleanupCallbacks.push(() => contentManager.unloadAll()); return instance; } diff --git a/test/data/dictionaries/valid-dictionary1/term_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_bank_1.json index cdff4028..114ec88b 100644 --- a/test/data/dictionaries/valid-dictionary1/term_bank_1.json +++ b/test/data/dictionaries/valid-dictionary1/term_bank_1.json @@ -243,6 +243,7 @@ ]} ]}, {"type": "structured-content", "content": [ + {"tag": "div", "style": {"fontSize": "xxx-large"}, "content": "直次茶冷 (auto lang)"}, {"tag": "div", "lang": "?????", "style": {"fontSize": "xxx-large"}, "content": "直次茶冷 (invalid lang)"}, {"tag": "div", "lang": "ja-JP", "style": {"fontSize": "xxx-large"}, "content": "直次茶冷 (lang=ja-JP)"}, {"tag": "div", "lang": "zh-CN", "style": {"fontSize": "xxx-large"}, "content": "直次茶冷 (lang=zh-CN)"},