From eddd0288643f08d2a2c85f73575bc7ee1c157539 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 19 May 2021 18:24:50 -0400 Subject: [PATCH] Add support for definitions with structured content (#1689) * Add structured content to schema * Add support for generating custom content * Update importer * Update test data * Add verticalAlign property --- ext/css/display.css | 13 ++ .../dictionary-term-bank-v3-schema.json | 111 +++++++++++++++++- ext/js/display/display-generator.js | 51 +++++++- ext/js/language/dictionary-importer.js | 50 +++++++- .../valid-dictionary1/term_bank_1.json | 27 ++++- 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": [ @@ -67,6 +159,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", 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