summaryrefslogtreecommitdiff
path: root/ext/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js')
-rw-r--r--ext/js/display/display-generator.js182
-rw-r--r--ext/js/display/structured-content-generator.js213
2 files changed, 218 insertions, 177 deletions
diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js
index 7de65f78..74b3fdca 100644
--- a/ext/js/display/display-generator.js
+++ b/ext/js/display/display-generator.js
@@ -18,6 +18,7 @@
/* global
* DictionaryDataUtil
* HtmlTemplateCollection
+ * StructuredContentGenerator
*/
class DisplayGenerator {
@@ -26,12 +27,14 @@ class DisplayGenerator {
this._mediaLoader = mediaLoader;
this._hotkeyHelpController = hotkeyHelpController;
this._templates = null;
+ this._structuredContentGenerator = null;
this._termPitchAccentStaticTemplateIsSetup = false;
}
async prepare() {
const html = await yomichan.api.getDisplayTemplatesHtml();
this._templates = new HtmlTemplateCollection(html);
+ this._structuredContentGenerator = new StructuredContentGenerator(this._templates, this._mediaLoader, document);
this.updateHotkeys();
}
@@ -316,7 +319,7 @@ class DisplayGenerator {
const node = this._templates.instantiate('gloss-item');
const contentContainer = node.querySelector('.gloss-content');
- const image = this._createDefinitionImage(data, dictionary);
+ const image = this._structuredContentGenerator.createDefinitionImage(data, dictionary);
contentContainer.appendChild(image);
if (typeof description === 'string') {
@@ -331,7 +334,7 @@ class DisplayGenerator {
_createTermDefinitionEntryStructuredContent(content, dictionary) {
const node = this._templates.instantiate('gloss-item');
- const child = this._createStructuredContent(content, dictionary);
+ const child = this._structuredContentGenerator.createStructuredContent(content, dictionary);
if (child !== null) {
const contentContainer = node.querySelector('.gloss-content');
contentContainer.appendChild(child);
@@ -339,181 +342,6 @@ class DisplayGenerator {
return node;
}
- _createDefinitionImage(data, dictionary) {
- const {
- path,
- width,
- height,
- preferredWidth,
- preferredHeight,
- title,
- pixelated,
- imageRendering,
- appearance,
- background,
- collapsed,
- collapsible,
- verticalAlign,
- sizeUnits
- } = data;
-
- const hasPreferredWidth = (typeof preferredWidth === 'number');
- const hasPreferredHeight = (typeof preferredHeight === 'number');
- const aspectRatio = (
- hasPreferredWidth && hasPreferredHeight ?
- preferredWidth / preferredHeight :
- width / height
- );
- const usedWidth = (
- hasPreferredWidth ?
- preferredWidth :
- (hasPreferredHeight ? preferredHeight * aspectRatio : width)
- );
-
- const node = this._templates.instantiate('gloss-item-image');
- const imageContainer = node.querySelector('.gloss-image-container');
- const aspectRatioSizer = node.querySelector('.gloss-image-aspect-ratio-sizer');
- const image = node.querySelector('.gloss-image');
- const imageBackground = node.querySelector('.gloss-image-background');
-
- node.dataset.path = path;
- node.dataset.dictionary = dictionary;
- node.dataset.imageLoadState = 'not-loaded';
- node.dataset.hasAspectRatio = 'true';
- node.dataset.imageRendering = typeof imageRendering === 'string' ? imageRendering : (pixelated ? 'pixelated' : 'auto');
- node.dataset.appearance = typeof appearance === 'string' ? appearance : 'auto';
- node.dataset.background = typeof background === 'boolean' ? `${background}` : 'true';
- node.dataset.collapsed = typeof collapsed === 'boolean' ? `${collapsed}` : 'false';
- node.dataset.collapsible = typeof collapsible === 'boolean' ? `${collapsible}` : 'true';
- if (typeof verticalAlign === 'string') {
- node.dataset.verticalAlign = verticalAlign;
- }
- if (typeof sizeUnits === 'string' && (hasPreferredWidth || hasPreferredHeight)) {
- node.dataset.sizeUnits = sizeUnits;
- }
-
- imageContainer.style.width = `${usedWidth}em`;
- if (typeof title === 'string') {
- imageContainer.title = title;
- }
-
- aspectRatioSizer.style.paddingTop = `${aspectRatio * 100.0}%`;
-
- if (this._mediaLoader !== null) {
- this._mediaLoader.loadMedia(
- path,
- dictionary,
- (url) => this._setImageData(node, image, imageBackground, url, false),
- () => this._setImageData(node, image, imageBackground, null, true)
- );
- }
-
- return node;
- }
-
- _setImageData(node, image, imageBackground, url, unloaded) {
- if (url !== null) {
- image.src = url;
- node.href = url;
- node.dataset.imageLoadState = 'loaded';
- imageBackground.style.setProperty('--image', `url("${url}")`);
- } else {
- image.removeAttribute('src');
- node.removeAttribute('href');
- node.dataset.imageLoadState = unloaded ? 'unloaded' : 'load-error';
- imageBackground.style.removeProperty('--image');
- }
- }
-
- _createStructuredContent(content, dictionary) {
- if (typeof content === 'string') {
- return document.createTextNode(content);
- }
- if (!(typeof content === 'object' && content !== null)) {
- return null;
- }
- if (Array.isArray(content)) {
- const fragment = document.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':
- return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, true);
- case 'img':
- return this._createDefinitionImage(content, dictionary);
- }
- return null;
- }
-
- _createStructuredContentTableElement(tag, content, dictionary) {
- const container = document.createElement('div');
- container.classList = 'gloss-sc-table-container';
- const table = this._createStructuredContentElement(tag, content, dictionary, 'table', true, false);
- container.appendChild(table);
- return container;
- }
-
- _createStructuredContentElement(tag, content, dictionary, type, hasChildren, hasStyle) {
- const node = document.createElement(tag);
- node.className = `gloss-sc-${tag}`;
- switch (type) {
- case 'table-cell':
- {
- const {colSpan, rowSpan} = content;
- if (typeof colSpan === 'number') { node.colSpan = colSpan; }
- if (typeof rowSpan === 'number') { node.rowSpan = rowSpan; }
- }
- break;
- }
- if (hasStyle) {
- const {style} = content;
- if (typeof style === 'object' && style !== null) {
- this._setStructuredContentElementStyle(node, style);
- }
- }
- if (hasChildren) {
- const child = this._createStructuredContent(content.content, dictionary);
- if (child !== null) { node.appendChild(child); }
- }
- return node;
- }
-
- _setStructuredContentElementStyle(node, contentStyle) {
- const {style} = node;
- const {fontStyle, fontWeight, fontSize, textDecorationLine, verticalAlign} = contentStyle;
- if (typeof fontStyle === 'string') { style.fontStyle = fontStyle; }
- if (typeof fontWeight === 'string') { style.fontWeight = fontWeight; }
- if (typeof fontSize === 'string') { style.fontSize = fontSize; }
- if (typeof verticalAlign === 'string') { style.verticalAlign = verticalAlign; }
- if (typeof textDecorationLine === 'string') {
- style.textDecoration = textDecorationLine;
- } else if (Array.isArray(textDecorationLine)) {
- style.textDecoration = textDecorationLine.join(' ');
- }
- }
-
_createTermDisambiguation(disambiguation) {
const node = this._templates.instantiate('definition-disambiguation');
node.dataset.term = disambiguation;
diff --git a/ext/js/display/structured-content-generator.js b/ext/js/display/structured-content-generator.js
new file mode 100644
index 00000000..cd4ea091
--- /dev/null
+++ b/ext/js/display/structured-content-generator.js
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+class StructuredContentGenerator {
+ constructor(templates, mediaLoader, document) {
+ this._templates = templates;
+ this._mediaLoader = mediaLoader;
+ this._document = document;
+ }
+
+ 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':
+ return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, true);
+ case 'img':
+ return this.createDefinitionImage(content, dictionary);
+ }
+ return null;
+ }
+
+ createDefinitionImage(data, dictionary) {
+ const {
+ path,
+ width,
+ height,
+ preferredWidth,
+ preferredHeight,
+ title,
+ pixelated,
+ imageRendering,
+ appearance,
+ background,
+ collapsed,
+ collapsible,
+ verticalAlign,
+ sizeUnits
+ } = data;
+
+ const hasPreferredWidth = (typeof preferredWidth === 'number');
+ const hasPreferredHeight = (typeof preferredHeight === 'number');
+ const aspectRatio = (
+ hasPreferredWidth && hasPreferredHeight ?
+ preferredWidth / preferredHeight :
+ width / height
+ );
+ const usedWidth = (
+ hasPreferredWidth ?
+ preferredWidth :
+ (hasPreferredHeight ? preferredHeight * aspectRatio : width)
+ );
+
+ const node = this._templates.instantiate('gloss-item-image');
+ const imageContainer = node.querySelector('.gloss-image-container');
+ const aspectRatioSizer = node.querySelector('.gloss-image-aspect-ratio-sizer');
+ const image = node.querySelector('.gloss-image');
+ const imageBackground = node.querySelector('.gloss-image-background');
+
+ node.dataset.path = path;
+ node.dataset.dictionary = dictionary;
+ node.dataset.imageLoadState = 'not-loaded';
+ node.dataset.hasAspectRatio = 'true';
+ node.dataset.imageRendering = typeof imageRendering === 'string' ? imageRendering : (pixelated ? 'pixelated' : 'auto');
+ node.dataset.appearance = typeof appearance === 'string' ? appearance : 'auto';
+ node.dataset.background = typeof background === 'boolean' ? `${background}` : 'true';
+ node.dataset.collapsed = typeof collapsed === 'boolean' ? `${collapsed}` : 'false';
+ node.dataset.collapsible = typeof collapsible === 'boolean' ? `${collapsible}` : 'true';
+ if (typeof verticalAlign === 'string') {
+ node.dataset.verticalAlign = verticalAlign;
+ }
+ if (typeof sizeUnits === 'string' && (hasPreferredWidth || hasPreferredHeight)) {
+ node.dataset.sizeUnits = sizeUnits;
+ }
+
+ imageContainer.style.width = `${usedWidth}em`;
+ if (typeof title === 'string') {
+ imageContainer.title = title;
+ }
+
+ aspectRatioSizer.style.paddingTop = `${aspectRatio * 100.0}%`;
+
+ if (this._mediaLoader !== null) {
+ this._mediaLoader.loadMedia(
+ path,
+ dictionary,
+ (url) => this._setImageData(node, image, imageBackground, url, false),
+ () => this._setImageData(node, image, imageBackground, null, true)
+ );
+ }
+
+ return node;
+ }
+
+ // Private
+
+ _createElement(tagName) {
+ return this._document.createElement(tagName);
+ }
+
+ _createTextNode(data) {
+ return this._document.createTextNode(data);
+ }
+
+ _createDocumentFragment() {
+ return this._document.createDocumentFragment();
+ }
+
+ _setImageData(node, image, imageBackground, url, unloaded) {
+ if (url !== null) {
+ image.src = url;
+ node.href = url;
+ node.dataset.imageLoadState = 'loaded';
+ imageBackground.style.setProperty('--image', `url("${url}")`);
+ } else {
+ image.removeAttribute('src');
+ node.removeAttribute('href');
+ node.dataset.imageLoadState = unloaded ? 'unloaded' : 'load-error';
+ imageBackground.style.removeProperty('--image');
+ }
+ }
+
+ _createStructuredContentTableElement(tag, content, dictionary) {
+ const container = this._createElement('div');
+ container.classList = 'gloss-sc-table-container';
+ const table = this._createStructuredContentElement(tag, content, dictionary, 'table', true, false);
+ container.appendChild(table);
+ return container;
+ }
+
+ _createStructuredContentElement(tag, content, dictionary, type, hasChildren, hasStyle) {
+ const node = this._createElement(tag);
+ node.className = `gloss-sc-${tag}`;
+ switch (type) {
+ case 'table-cell':
+ {
+ const {colSpan, rowSpan} = content;
+ if (typeof colSpan === 'number') { node.colSpan = colSpan; }
+ if (typeof rowSpan === 'number') { node.rowSpan = rowSpan; }
+ }
+ break;
+ }
+ if (hasStyle) {
+ const {style} = content;
+ if (typeof style === 'object' && style !== null) {
+ this._setStructuredContentElementStyle(node, style);
+ }
+ }
+ if (hasChildren) {
+ const child = this.createStructuredContent(content.content, dictionary);
+ if (child !== null) { node.appendChild(child); }
+ }
+ return node;
+ }
+
+ _setStructuredContentElementStyle(node, contentStyle) {
+ const {style} = node;
+ const {fontStyle, fontWeight, fontSize, textDecorationLine, verticalAlign} = contentStyle;
+ if (typeof fontStyle === 'string') { style.fontStyle = fontStyle; }
+ if (typeof fontWeight === 'string') { style.fontWeight = fontWeight; }
+ if (typeof fontSize === 'string') { style.fontSize = fontSize; }
+ if (typeof verticalAlign === 'string') { style.verticalAlign = verticalAlign; }
+ if (typeof textDecorationLine === 'string') {
+ style.textDecoration = textDecorationLine;
+ } else if (Array.isArray(textDecorationLine)) {
+ style.textDecoration = textDecorationLine.join(' ');
+ }
+ }
+}