diff options
| -rw-r--r-- | ext/css/display.css | 13 | ||||
| -rw-r--r-- | ext/data/schemas/dictionary-term-bank-v3-schema.json | 111 | ||||
| -rw-r--r-- | ext/js/display/display-generator.js | 51 | ||||
| -rw-r--r-- | ext/js/language/dictionary-importer.js | 50 | ||||
| -rw-r--r-- | test/data/dictionaries/valid-dictionary1/term_bank_1.json | 27 | ||||
| -rw-r--r-- | test/test-database.js | 4 | 
6 files changed, 245 insertions, 11 deletions
| diff --git a/ext/css/display.css b/ext/css/display.css index 529e4249..b8f67ff9 100644 --- a/ext/css/display.css +++ b/ext/css/display.css @@ -1643,6 +1643,19 @@ button.definition-item-expansion-button:focus:focus-visible+.definition-item-con      white-space: pre-line;  } +.gloss-image-link[data-vertical-align=baseline] { vertical-align: baseline; } +.gloss-image-link[data-vertical-align=sub] { vertical-align: sub; } +.gloss-image-link[data-vertical-align=super] { vertical-align: super; } +.gloss-image-link[data-vertical-align=text-top] { vertical-align: top; } +.gloss-image-link[data-vertical-align=text-bottom] { vertical-align: bottom; } +.gloss-image-link[data-vertical-align=middle] { vertical-align: middle; } +.gloss-image-link[data-vertical-align=top] { vertical-align: top; } +.gloss-image-link[data-vertical-align=bottom] { vertical-align: bottom; } +.gloss-image-link[data-collapsed=true], +:root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true] { +    vertical-align: baseline; +} +  .gloss-image-link[data-collapsed=true] .gloss-image-container,  :root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true] .gloss-image-container {      display: none; diff --git a/ext/data/schemas/dictionary-term-bank-v3-schema.json b/ext/data/schemas/dictionary-term-bank-v3-schema.json index 2289dfd6..0ab01edb 100644 --- a/ext/data/schemas/dictionary-term-bank-v3-schema.json +++ b/ext/data/schemas/dictionary-term-bank-v3-schema.json @@ -1,5 +1,97 @@  {      "$schema": "http://json-schema.org/draft-07/schema#", +    "definitions": { +        "structuredContent": { +            "oneOf": [ +                { +                    "type": "string", +                    "description": "Represents a text node." +                }, +                { +                    "type": "array", +                    "items": { +                        "$ref": "#/definitions/structuredContent", +                        "description": "An array of child content." +                    } +                }, +                { +                    "type": "object", +                    "oneOf": [ +                        { +                            "type": "object", +                            "description": "Generic container tags.", +                            "required": [ +                                "tag" +                            ], +                            "additionalProperties": false, +                            "properties": { +                                "tag": { +                                    "type": "string", +                                    "enum": ["ruby", "rt", "rp"] +                                }, +                                "content": { +                                    "$ref": "#/definitions/structuredContent" +                                } +                            } +                        }, +                        { +                            "type": "object", +                            "description": "Image tag.", +                            "required": [ +                                "tag", +                                "path" +                            ], +                            "additionalProperties": false, +                            "properties": { +                                "tag": { +                                    "type": "string", +                                    "const": "img" +                                }, +                                "path": { +                                    "type": "string", +                                    "description": "Path to the image file in the archive." +                                }, +                                "width": { +                                    "type": "integer", +                                    "description": "Preferred width of the image.", +                                    "minimum": 1 +                                }, +                                "height": { +                                    "type": "integer", +                                    "description": "Preferred width of the image.", +                                    "minimum": 1 +                                }, +                                "title": { +                                    "type": "string", +                                    "description": "Hover text for the image." +                                }, +                                "pixelated": { +                                    "type": "boolean", +                                    "description": "Whether or not the image should appear pixelated at sizes larger than the image's native resolution.", +                                    "default": false +                                }, +                                "collapsed": { +                                    "type": "boolean", +                                    "description": "Whether or not the image is collapsed by default.", +                                    "default": false +                                }, +                                "collapsible": { +                                    "type": "boolean", +                                    "description": "Whether or not the image can be collapsed.", +                                    "default": false +                                }, +                                "verticalAlign": { +                                    "type": "string", +                                    "description": "The vertical alignment of the image.", +                                    "enum": ["baseline", "sub", "super", "text-top", "text-bottom", "middle", "top", "bottom"] +                                } +                            } +                        } +                    ] +                } +            ] +        } +    },      "type": "array",      "description": "Data file containing term and expression information.",      "additionalItems": { @@ -46,7 +138,7 @@                                  "type": {                                      "type": "string",                                      "description": "The type of the data for this definition.", -                                    "enum": ["text", "image"] +                                    "enum": ["text", "image", "structured-content"]                                  }                              },                              "oneOf": [ @@ -70,6 +162,23 @@                                  {                                      "required": [                                          "type", +                                        "content" +                                    ], +                                    "additionalProperties": false, +                                    "properties": { +                                        "type": { +                                            "type": "string", +                                            "enum": ["structured-content"] +                                        }, +                                        "content": { +                                            "$ref": "#/definitions/structuredContent", +                                            "description": "Single definition for the term/expression using a structured content object." +                                        } +                                    } +                                }, +                                { +                                    "required": [ +                                        "type",                                          "path"                                      ],                                      "additionalProperties": false, diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js index 299d730b..2131d805 100644 --- a/ext/js/display/display-generator.js +++ b/ext/js/display/display-generator.js @@ -295,6 +295,8 @@ class DisplayGenerator {              switch (entry.type) {                  case 'image':                      return this._createTermDefinitionEntryImage(entry, dictionary); +                case 'structured-content': +                    return this._createTermDefinitionEntryStructuredContent(entry.content, dictionary);              }          } @@ -327,8 +329,18 @@ class DisplayGenerator {          return node;      } +    _createTermDefinitionEntryStructuredContent(content, dictionary) { +        const node = this._templates.instantiate('gloss-item'); +        const child = this._createStructuredContent(content, dictionary); +        if (child !== null) { +            const contentContainer = node.querySelector('.gloss-content'); +            contentContainer.appendChild(child); +        } +        return node; +    } +      _createDefinitionImage(data, dictionary) { -        const {path, width, height, preferredWidth, preferredHeight, title, pixelated, collapsed, collapsible} = data; +        const {path, width, height, preferredWidth, preferredHeight, title, pixelated, collapsed, collapsible, verticalAlign} = data;          const usedWidth = (              typeof preferredWidth === 'number' ? @@ -349,6 +361,9 @@ class DisplayGenerator {          node.dataset.hasAspectRatio = '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; +        }          const imageContainer = node.querySelector('.gloss-image-container');          imageContainer.style.width = `${usedWidth}em`; @@ -386,6 +401,40 @@ class DisplayGenerator {          }      } +    _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 'ruby': +            case 'rt': +            case 'rp': +            { +                const node = document.createElement(tag); +                const child = this._createStructuredContent(content.content, dictionary); +                if (child !== null) { +                    node.appendChild(child); +                } +                return node; +            } +            case 'img': +                return this._createDefinitionImage(content, dictionary); +        } +        return null; +    } +      _createTermDisambiguation(disambiguation) {          const node = this._templates.instantiate('definition-disambiguation');          node.dataset.term = disambiguation; diff --git a/ext/js/language/dictionary-importer.js b/ext/js/language/dictionary-importer.js index 051375a0..4d885a74 100644 --- a/ext/js/language/dictionary-importer.js +++ b/ext/js/language/dictionary-importer.js @@ -300,20 +300,58 @@ class DictionaryImporter {                  return data.text;              case 'image':                  return await this._formatDictionaryTermGlossaryImage(data, context, entry); +            case 'structured-content': +                return await this._formatStructuredContent(data, context, entry);              default:                  throw new Error(`Unhandled data type: ${data.type}`);          }      }      async _formatDictionaryTermGlossaryImage(data, context, entry) { +        return await this._createImageData(data, context, entry, {type: 'image'}); +    } + +    async _formatStructuredContent(data, context, entry) { +        const content = await this._prepareStructuredContent(data.content, context, entry); +        return { +            type: 'structured-content', +            content +        }; +    } + +    async _prepareStructuredContent(content, context, entry) { +        if (typeof content === 'string' || !(typeof content === 'object' && content !== null)) { +            return content; +        } +        if (Array.isArray(content)) { +            for (let i = 0, ii = content.length; i < ii; ++i) { +                content[i] = await this._prepareStructuredContent(content[i], context, entry); +            } +            return content; +        } +        const {tag} = content; +        switch (tag) { +            case 'img': +                return await this._prepareStructuredContentImage(content, context, entry); +        } +        const childContent = content.content; +        if (typeof childContent !== 'undefined') { +            content.content = await this._prepareStructuredContent(childContent, context, entry); +        } +        return content; +    } + +    async _prepareStructuredContentImage(content, context, entry) { +        const {verticalAlign} = content; +        const result = await this._createImageData(content, context, entry, {tag: 'img'}); +        if (typeof verticalAlign === 'string') { result.verticalAlign = verticalAlign; } +        return result; +    } + +    async _createImageData(data, context, entry, attributes) {          const {path, width: preferredWidth, height: preferredHeight, title, description, pixelated, collapsed, collapsible} = data;          const {width, height} = await this._getImageMedia(path, context, entry); -        const newData = { -            type: 'image', -            path, -            width, -            height -        }; +        const newData = Object.assign({}, attributes, {path, width, height});          if (typeof preferredWidth === 'number') { newData.preferredWidth = preferredWidth; }          if (typeof preferredHeight === 'number') { newData.preferredHeight = preferredHeight; }          if (typeof title === 'string') { newData.title = title; } diff --git a/test/data/dictionaries/valid-dictionary1/term_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_bank_1.json index 5094dd0f..a2c9a216 100644 --- a/test/data/dictionaries/valid-dictionary1/term_bank_1.json +++ b/test/data/dictionaries/valid-dictionary1/term_bank_1.json @@ -12,5 +12,30 @@      ["画像", "がぞう", "n", "n", 1, ["gazou definition 1", {"type": "image", "path": "image.gif", "width": 350, "height": 350, "description": "gazou definition 2", "pixelated": true}], 5, "P E1"],      ["読む", "よむ", "vt", "v5", 100, ["to read"], 6, "P E1"],      ["強み", "つよみ", "n", "n", 90, ["strong point"], 7, "P E1"], -    ["テキスト", "テキスト", "n", "n", 1, ["text definition 1", {"type": "text", "text": "text definition 2"}], 8, "P E1"] +    ["テキスト", "テキスト", "n", "n", 1, ["text definition 1", {"type": "text", "text": "text definition 2"}], 8, "P E1"], +    [ +        "内容", "ないよう", "n", "n", 35, +        [ +            "naiyou definition 1", +            {"type": "structured-content", "content": "naiyou definition 2"}, +            {"type": "structured-content", "content": ["naiyou definition 3"]}, +            {"type": "structured-content", "content": {"tag": "img", "path": "image.gif", "width": 35, "height": 35, "pixelated": true}}, +            {"type": "structured-content", "content": [ +                "naiyou definition 5: ", +                {"tag": "img", "path": "image.gif", "width": 35, "height": 35, "pixelated": true, "collapsible": false}, +                "\nmore content 1: ", +                {"tag": "img", "path": "image.gif", "width": 35, "height": 35, "pixelated": true, "collapsible": true}, +                "\nmore content 2: ", +                {"tag": "img", "path": "image.gif", "width": 35, "height": 35, "pixelated": true, "collapsible": false, "collapsed": false, "verticalAlign": "middle"}, +                " and ", +                {"tag": "img", "path": "image.gif", "width": 35, "height": 35, "pixelated": true, "collapsible": false, "collapsed": true} +            ]}, +            {"type": "structured-content", "content": [ +                "naiyou definition 6: ", +                {"tag": "ruby", "content": ["内", {"tag": "rp", "content": "("}, {"tag": "rt", "content": "ない"}, {"tag": "rp", "content": ")"}]}, +                {"tag": "ruby", "content": ["容", {"tag": "rp", "content": "("}, {"tag": "rt", "content": "よう"}, {"tag": "rp", "content": ")"}]} +            ]} +        ], +        9, "P E1" +    ]  ]
\ No newline at end of file diff --git a/test/test-database.js b/test/test-database.js index da4cc6d3..e68a39ba 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -162,8 +162,8 @@ async function testDatabase1() {              true          );          vm.assert.deepStrictEqual(counts, { -            counts: [{kanji: 2, kanjiMeta: 2, terms: 14, termMeta: 12, tagMeta: 15, media: 1}], -            total: {kanji: 2, kanjiMeta: 2, terms: 14, termMeta: 12, tagMeta: 15, media: 1} +            counts: [{kanji: 2, kanjiMeta: 2, terms: 15, termMeta: 12, tagMeta: 15, media: 1}], +            total: {kanji: 2, kanjiMeta: 2, terms: 15, termMeta: 12, tagMeta: 15, media: 1}          });          // Test find* functions |