diff options
Diffstat (limited to 'ext/js/display')
| -rw-r--r-- | ext/js/display/display-generator.js | 182 | ||||
| -rw-r--r-- | ext/js/display/structured-content-generator.js | 213 | 
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(' '); +        } +    } +} |