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 <span>

* 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
This commit is contained in:
toasted-nutbread 2022-05-14 18:12:57 -04:00 committed by GitHub
parent 6a74746113
commit 5dcc2315d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 66 deletions

View File

@ -28,7 +28,7 @@ class DisplayGenerator {
this._contentManager = contentManager; this._contentManager = contentManager;
this._hotkeyHelpController = hotkeyHelpController; this._hotkeyHelpController = hotkeyHelpController;
this._templates = null; this._templates = null;
this._structuredContentGenerator = new StructuredContentGenerator(this._contentManager, document); this._structuredContentGenerator = new StructuredContentGenerator(this._contentManager, japaneseUtil, document);
this._pronunciationGenerator = new PronunciationGenerator(japaneseUtil); this._pronunciationGenerator = new PronunciationGenerator(japaneseUtil);
} }
@ -347,11 +347,8 @@ class DisplayGenerator {
_createTermDefinitionEntryStructuredContent(content, dictionary) { _createTermDefinitionEntryStructuredContent(content, dictionary) {
const node = this._templates.instantiate('gloss-item'); const node = this._templates.instantiate('gloss-item');
const child = this._structuredContentGenerator.createStructuredContent(content, dictionary);
if (child !== null) {
const contentContainer = node.querySelector('.gloss-content'); const contentContainer = node.querySelector('.gloss-content');
contentContainer.appendChild(child); this._structuredContentGenerator.appendStructuredContent(contentContainer, content, dictionary);
}
return node; return node;
} }

View File

@ -16,56 +16,21 @@
*/ */
class StructuredContentGenerator { class StructuredContentGenerator {
constructor(contentManager, document) { constructor(contentManager, japaneseUtil, document) {
this._contentManager = contentManager; this._contentManager = contentManager;
this._japaneseUtil = japaneseUtil;
this._document = document; this._document = document;
} }
appendStructuredContent(node, content, dictionary) {
node.classList.add('structured-content');
this._appendStructuredContent(node, content, dictionary, null);
}
createStructuredContent(content, dictionary) { createStructuredContent(content, dictionary) {
if (typeof content === 'string') { const node = this._createElement('span', 'structured-content');
return this._createTextNode(content); this._appendStructuredContent(node, content, dictionary, null);
} return node;
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;
} }
createDefinitionImage(data, dictionary) { createDefinitionImage(data, dictionary) {
@ -160,6 +125,31 @@ class StructuredContentGenerator {
// Private // 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) { _createElement(tagName, className) {
const node = this._document.createElement(tagName); const node = this._document.createElement(tagName);
node.className = className; node.className = className;
@ -170,10 +160,6 @@ class StructuredContentGenerator {
return this._document.createTextNode(data); return this._document.createTextNode(data);
} }
_createDocumentFragment() {
return this._document.createDocumentFragment();
}
_setElementDataset(element, data) { _setElementDataset(element, data) {
for (let [key, value] of Object.entries(data)) { for (let [key, value] of Object.entries(data)) {
if (key.length > 0) { 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 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); container.appendChild(table);
return container; 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 node = this._createElement(tag, `gloss-sc-${tag}`);
const {data, lang} = content; const {data, lang} = content;
if (typeof data === 'object' && data !== null) { this._setElementDataset(node, data); } 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) { switch (type) {
case 'table-cell': case 'table-cell':
{ {
@ -226,8 +248,7 @@ class StructuredContentGenerator {
} }
} }
if (hasChildren) { if (hasChildren) {
const child = this.createStructuredContent(content.content, dictionary); this._appendStructuredContent(node, content.content, dictionary, language);
if (child !== null) { node.appendChild(child); }
} }
return node; return node;
} }
@ -262,7 +283,7 @@ class StructuredContentGenerator {
if (typeof listStyleType === 'string') { style.listStyleType = listStyleType; } if (typeof listStyleType === 'string') { style.listStyleType = listStyleType; }
} }
_createLinkElement(content, dictionary) { _createLinkElement(content, dictionary, language) {
let {href} = content; let {href} = content;
const internal = href.startsWith('?'); const internal = href.startsWith('?');
if (internal) { if (internal) {
@ -276,10 +297,12 @@ class StructuredContentGenerator {
node.appendChild(text); node.appendChild(text);
const {lang} = content; 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); this._appendStructuredContent(text, content.content, dictionary, language);
if (child !== null) { text.appendChild(child); }
if (!internal) { if (!internal) {
const icon = this._createElement('span', 'gloss-link-external-icon icon'); const icon = this._createElement('span', 'gloss-link-external-icon icon');

View File

@ -536,7 +536,7 @@ class AnkiTemplateRenderer {
_createStructuredContentGenerator(data) { _createStructuredContentGenerator(data) {
const contentManager = new AnkiTemplateRendererContentManager(this._mediaProvider, 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()); this._cleanupCallbacks.push(() => contentManager.unloadAll());
return instance; return instance;
} }

View File

@ -243,6 +243,7 @@
]} ]}
]}, ]},
{"type": "structured-content", "content": [ {"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": "?????", "style": {"fontSize": "xxx-large"}, "content": "直次茶冷 (invalid lang)"},
{"tag": "div", "lang": "ja-JP", "style": {"fontSize": "xxx-large"}, "content": "直次茶冷 (lang=ja-JP)"}, {"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)"}, {"tag": "div", "lang": "zh-CN", "style": {"fontSize": "xxx-large"}, "content": "直次茶冷 (lang=zh-CN)"},