diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-07-02 22:46:38 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-07-02 22:46:38 -0400 | 
| commit | ca97e38bd22875e23cfe6f70d1803ea31d6f0e89 (patch) | |
| tree | deb2d4c63f8ffd463031f907687c8a416a26553c | |
| parent | a4715935cb4d910f5b13b398ca4742cf30c8f784 (diff) | |
Anki support for structured-content (#1786)
* Update how glossary text is formatted
* Update structured content and image generation
* Pass root data to _createStructuredContentGenerator
* Implement media URLs
* Update documentation
* Update options util
* Apply styles to content
* Improve HTML normalization
* Update DatabaseVM.fetch function
* Update test
* Update test data
| -rw-r--r-- | dev/database-vm.js | 8 | ||||
| -rw-r--r-- | docs/templates.md | 29 | ||||
| -rw-r--r-- | ext/data/templates/anki-field-templates-upgrade-v13.handlebars | 17 | ||||
| -rw-r--r-- | ext/data/templates/default-anki-field-templates.handlebars | 6 | ||||
| -rw-r--r-- | ext/js/data/options-util.js | 10 | ||||
| -rw-r--r-- | ext/js/dom/css-style-applier.js | 6 | ||||
| -rw-r--r-- | ext/js/templates/template-renderer-frame-main.js | 7 | ||||
| -rw-r--r-- | ext/js/templates/template-renderer.js | 140 | ||||
| -rw-r--r-- | ext/template-renderer.html | 2 | ||||
| -rw-r--r-- | test/data/anki-note-builder-test-results.json | 12 | ||||
| -rw-r--r-- | test/test-anki-note-builder.js | 21 | ||||
| -rw-r--r-- | test/test-options-util.js | 32 | 
12 files changed, 257 insertions, 33 deletions
diff --git a/dev/database-vm.js b/dev/database-vm.js index 014c989f..0127bc8f 100644 --- a/dev/database-vm.js +++ b/dev/database-vm.js @@ -84,7 +84,13 @@ class Blob {  }  async function fetch(url2) { -    const filePath = url.fileURLToPath(url2); +    const extDir = path.join(__dirname, '..', 'ext'); +    let filePath; +    try { +        filePath = url.fileURLToPath(url2); +    } catch (e) { +        filePath = path.resolve(extDir, url2.replace(/^[/\\]/, '')); +    }      await Promise.resolve();      const content = fs.readFileSync(filePath, {encoding: null});      return { diff --git a/docs/templates.md b/docs/templates.md index 81259a3f..95155bea 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -645,6 +645,35 @@ Returns an array representing the different pitch categories for a specific term  </details> +### `formatGlossary` + +Formats a glossary entry to a HTML content string. This helper handles image and +structured-content generation. + +<details> +  <summary>Syntax:</summary> + +  <code>{{#formatGlossary <i>dictionary</i>}}{{{definitionEntry}}}{{/pitchCategories}}</code><br> + +  * _`@dictionary`_ <br> +    The dictionary that the glossary entry belongs to. +  * _`@definitionEntry`_ <br> +    The definition entry object in raw form. +</details> +<details> +  <summary>Example:</summary> + +  ```handlebars +  {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} +  ``` + +  Output: +  ```html +  Here is the content of a gloss, which may include formatted HTML. +  ``` +</details> + +  ## Legacy Helpers  Yomichan has historically used Handlebars templates to generate the HTML used on the search page and results popup. diff --git a/ext/data/templates/anki-field-templates-upgrade-v13.handlebars b/ext/data/templates/anki-field-templates-upgrade-v13.handlebars new file mode 100644 index 00000000..04cc855a --- /dev/null +++ b/ext/data/templates/anki-field-templates-upgrade-v13.handlebars @@ -0,0 +1,17 @@ +{{<<<<<<<}} +{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} +{{=======}} +{{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} +{{=======}} +{{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}} +{{=======}} +{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}} +{{>>>>>>>}} diff --git a/ext/data/templates/default-anki-field-templates.handlebars b/ext/data/templates/default-anki-field-templates.handlebars index 97359aa0..67547732 100644 --- a/ext/data/templates/default-anki-field-templates.handlebars +++ b/ext/data/templates/default-anki-field-templates.handlebars @@ -21,11 +21,11 @@          {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}      {{~/unless~}}      {{~#if (op "<=" glossary.length 1)~}} -        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}}      {{~else if @root.compactGlossaries~}} -        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}}      {{~else~}} -        <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> +        <ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul>      {{~/if~}}      {{~#set "previousDictionary" dictionary~}}{{~/set~}}  {{/inline}} diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index eb29dae4..42175d35 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -461,7 +461,8 @@ class OptionsUtil {              {async: false, update: this._updateVersion9.bind(this)},              {async: true,  update: this._updateVersion10.bind(this)},              {async: false, update: this._updateVersion11.bind(this)}, -            {async: true,  update: this._updateVersion12.bind(this)} +            {async: true,  update: this._updateVersion12.bind(this)}, +            {async: true,  update: this._updateVersion13.bind(this)}          ];      } @@ -844,4 +845,11 @@ class OptionsUtil {          }          return options;      } + +    async _updateVersion13(options) { +        // Version 13 changes: +        //  Handlebars templates updated to use formatGlossary. +        await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v13.handlebars'); +        return options; +    }  } diff --git a/ext/js/dom/css-style-applier.js b/ext/js/dom/css-style-applier.js index 593e7a46..c617fead 100644 --- a/ext/js/dom/css-style-applier.js +++ b/ext/js/dom/css-style-applier.js @@ -72,12 +72,6 @@ class CssStyleApplier {                  element.removeAttribute('style');              }          } -        for (const element of elements) { -            const {dataset} = element; -            for (const key of Object.keys(dataset)) { -                delete dataset[key]; -            } -        }      }      // Private diff --git a/ext/js/templates/template-renderer-frame-main.js b/ext/js/templates/template-renderer-frame-main.js index 3d61295d..bb9cac3a 100644 --- a/ext/js/templates/template-renderer-frame-main.js +++ b/ext/js/templates/template-renderer-frame-main.js @@ -17,14 +17,17 @@  /* globals   * AnkiNoteDataCreator + * CssStyleApplier   * JapaneseUtil   * TemplateRenderer   * TemplateRendererFrameApi   */ -(() => { +(async () => { +    const cssStyleApplier = new CssStyleApplier('/data/structured-content-style.json'); +    await cssStyleApplier.prepare();      const japaneseUtil = new JapaneseUtil(null); -    const templateRenderer = new TemplateRenderer(japaneseUtil); +    const templateRenderer = new TemplateRenderer(japaneseUtil, cssStyleApplier);      const ankiNoteDataCreator = new AnkiNoteDataCreator(japaneseUtil);      templateRenderer.registerDataType('ankiNote', {          modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData), diff --git a/ext/js/templates/template-renderer.js b/ext/js/templates/template-renderer.js index a06298aa..928ec3c4 100644 --- a/ext/js/templates/template-renderer.js +++ b/ext/js/templates/template-renderer.js @@ -18,11 +18,13 @@  /* global   * DictionaryDataUtil   * Handlebars + * StructuredContentGenerator   */  class TemplateRenderer { -    constructor(japaneseUtil) { +    constructor(japaneseUtil, cssStyleApplier) {          this._japaneseUtil = japaneseUtil; +        this._cssStyleApplier = cssStyleApplier;          this._cache = new Map();          this._cacheMaxSize = 5;          this._helpersRegistered = false; @@ -31,6 +33,7 @@ class TemplateRenderer {          this._requirements = null;          this._cleanupCallbacks = null;          this._customData = null; +        this._temporaryElement = null;      }      registerDataType(name, {modifier=null, composeData=null}) { @@ -161,7 +164,8 @@ class TemplateRenderer {              ['typeof',           this._getTypeof.bind(this)],              ['join',             this._join.bind(this)],              ['concat',           this._concat.bind(this)], -            ['pitchCategories',  this._pitchCategories.bind(this)] +            ['pitchCategories',  this._pitchCategories.bind(this)], +            ['formatGlossary',   this._formatGlossary.bind(this)]          ];          for (const [name, helper] of helpers) { @@ -244,8 +248,12 @@ class TemplateRenderer {          return result;      } +    _stringToMultiLineHtml(string) { +        return string.split('\n').join('<br>'); +    } +      _multiLine(context, options) { -        return options.fn(context).split('\n').join('<br>'); +        return this._stringToMultiLineHtml(options.fn(context));      }      _sanitizeCssClass(context, options) { @@ -497,4 +505,130 @@ class TemplateRenderer {          }          return [...categories];      } + +    _getTemporaryElement() { +        let element = this._temporaryElement; +        if (element === null) { +            element = document.createElement('div'); +            this._temporaryElement = element; +        } +        return element; +    } + +    _getHtml(node) { +        const container = this._getTemporaryElement(); +        container.appendChild(node); +        this._normalizeHtml(container); +        const result = container.innerHTML; +        container.textContent = ''; +        return result; +    } + +    _normalizeHtml(root) { +        const {ELEMENT_NODE, TEXT_NODE} = Node; +        const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); +        const elements = []; +        const textNodes = []; +        while (true) { +            const node = treeWalker.nextNode(); +            if (node === null) { break; } +            switch (node.nodeType) { +                case ELEMENT_NODE: +                    elements.push(node); +                    break; +                case TEXT_NODE: +                    textNodes.push(node); +                    break; +            } +        } +        this._cssStyleApplier.applyClassStyles(elements); +        for (const element of elements) { +            const {dataset} = element; +            for (const key of Object.keys(dataset)) { +                delete dataset[key]; +            } +        } +        for (const textNode of textNodes) { +            this._replaceNewlines(textNode); +        } +    } + +    _replaceNewlines(textNode) { +        const parts = textNode.nodeValue.split('\n'); +        if (parts.length <= 1) { return; } +        const {parentNode} = textNode; +        if (parentNode === null) { return; } +        const fragment = document.createDocumentFragment(); +        for (let i = 0, ii = parts.length; i < ii; ++i) { +            if (i > 0) { fragment.appendChild(document.createElement('br')); } +            fragment.appendChild(document.createTextNode(parts[i])); +        } +        parentNode.replaceChild(fragment, textNode); +    } + +    _getDictionaryMedia(data, dictionary, path) { +        const {media} = data; +        if (typeof media === 'object' && media !== null && Object.prototype.hasOwnProperty.call(media, 'dictionaryMedia')) { +            const {dictionaryMedia} = media; +            if (typeof dictionaryMedia === 'object' && dictionaryMedia !== null && Object.prototype.hasOwnProperty.call(dictionaryMedia, dictionary)) { +                const dictionaryMedia2 = dictionaryMedia[dictionary]; +                if (Object.prototype.hasOwnProperty.call(dictionaryMedia2, path)) { +                    return dictionaryMedia2[path]; +                } +            } +        } +        return null; +    } + +    _createStructuredContentGenerator(data) { +        const mediaLoader = { +            loadMedia: async (path, dictionary, onLoad, onUnload) => { +                const imageUrl = this._getDictionaryMedia(data, dictionary, path); +                if (imageUrl !== null) { +                    onLoad(imageUrl); +                    this._cleanupCallbacks.push(() => onUnload(true)); +                } else { +                    let set = this._customData.requiredDictionaryMedia; +                    if (typeof set === 'undefined') { +                        set = new Set(); +                        this._customData.requiredDictionaryMedia = set; +                    } +                    const key = JSON.stringify([dictionary, path]); +                    if (!set.has(key)) { +                        set.add(key); +                        this._requirements.push({ +                            type: 'dictionaryMedia', +                            dictionary, +                            path +                        }); +                    } +                } +            } +        }; +        return new StructuredContentGenerator(mediaLoader, document); +    } + +    _formatGlossary(context, dictionary, options) { +        const data = options.data.root; +        const content = options.fn(context); +        if (typeof content === 'string') { return this._stringToMultiLineHtml(content); } +        if (!(typeof content === 'object' && content !== null)) { return ''; } +        switch (content.type) { +            case 'image': return this._formatGlossaryImage(content, dictionary, data); +            case 'structured-content': return this._formatStructuredContent(content, dictionary, data); +        } +        return ''; +    } + +    _formatGlossaryImage(content, dictionary, data) { +        const structuredContentGenerator = this._createStructuredContentGenerator(data); +        const node = structuredContentGenerator.createDefinitionImage(content, dictionary); +        return this._getHtml(node); +    } + +    _formatStructuredContent(content, dictionary, data) { +        const structuredContentGenerator = this._createStructuredContentGenerator(data); +        const node = structuredContentGenerator.createStructuredContent(content.content, dictionary); +        return node !== null ? this._getHtml(node) : ''; +    }  } diff --git a/ext/template-renderer.html b/ext/template-renderer.html index f9667acd..f01b5b9a 100644 --- a/ext/template-renderer.html +++ b/ext/template-renderer.html @@ -18,6 +18,8 @@  <script src="/lib/handlebars.min.js"></script>  <script src="/js/data/anki-note-data-creator.js"></script> +<script src="/js/display/structured-content-generator.js"></script> +<script src="/js/dom/css-style-applier.js"></script>  <script src="/js/language/dictionary-data-util.js"></script>  <script src="/js/language/japanese-util.js"></script>  <script src="/js/templates/template-renderer.js"></script> diff --git a/test/data/anki-note-builder-test-results.json b/test/data/anki-note-builder-test-results.json index c6405439..c8357cb3 100644 --- a/test/data/anki-note-builder-test-results.json +++ b/test/data/anki-note-builder-test-results.json @@ -587,9 +587,9 @@          "frequencies": "",          "furigana": "<ruby>画像<rt>がぞう</rt></ruby>",          "furigana-plain": "画像[がぞう]", -        "glossary": "<div style=\"text-align: left;\"><i>(n, Test Dictionary 2)</i> <ul><li>gazou definition 1</li><li>[object Object]</li></ul></div>", -        "glossary-brief": "<div style=\"text-align: left;\"><ul><li>gazou definition 1</li><li>[object Object]</li></ul></div>", -        "glossary-no-dictionary": "<div style=\"text-align: left;\"><i>(n)</i> <ul><li>gazou definition 1</li><li>[object Object]</li></ul></div>", +        "glossary": "<div style=\"text-align: left;\"><i>(n, Test Dictionary 2)</i> <ul><li>gazou definition 1</li><li><a target=\"_blank\" rel=\"noreferrer noopener\" style=\"cursor:inherit;display:inline-block;position:relative;line-height:1;max-width:100%;color:inherit;\"><span style=\"display:inline-block;white-space:nowrap;max-width:100%;max-height:100vh;position:relative;vertical-align:top;line-height:0;overflow:hidden;font-size:1px;width: 350em;\"><span style=\"display:inline-block;width:0;vertical-align:top;font-size:0;padding-top: 100%;\"></span><span style=\"--image:none;position:absolute;left:0;top:0;width:100%;height:100%;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center center;-webkit-mask-mode:alpha;-webkit-mask-size:contain;-webkit-mask-image:var(--image);mask-repeat:no-repeat;mask-position:center center;mask-mode:alpha;mask-size:contain;mask-image:var(--image);background-color:currentColor;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;display:none;\"></span><img alt=\"\" style=\"display:inline-block;vertical-align:top;object-fit:contain;border:none;outline:none;position:absolute;left:0;top:0;width:100%;height:100%;display:none;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;\"><span style=\"position:absolute;left:0;top:0;width:100%;height:100%;display:table;table-layout:fixed;white-space:normal;font-size:initial;line-height:initial;color:initial;\"></span></span><span style=\"display:none;line-height:initial;\">Image</span></a></li></ul></div>", +        "glossary-brief": "<div style=\"text-align: left;\"><ul><li>gazou definition 1</li><li><a target=\"_blank\" rel=\"noreferrer noopener\" style=\"cursor:inherit;display:inline-block;position:relative;line-height:1;max-width:100%;color:inherit;\"><span style=\"display:inline-block;white-space:nowrap;max-width:100%;max-height:100vh;position:relative;vertical-align:top;line-height:0;overflow:hidden;font-size:1px;width: 350em;\"><span style=\"display:inline-block;width:0;vertical-align:top;font-size:0;padding-top: 100%;\"></span><span style=\"--image:none;position:absolute;left:0;top:0;width:100%;height:100%;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center center;-webkit-mask-mode:alpha;-webkit-mask-size:contain;-webkit-mask-image:var(--image);mask-repeat:no-repeat;mask-position:center center;mask-mode:alpha;mask-size:contain;mask-image:var(--image);background-color:currentColor;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;display:none;\"></span><img alt=\"\" style=\"display:inline-block;vertical-align:top;object-fit:contain;border:none;outline:none;position:absolute;left:0;top:0;width:100%;height:100%;display:none;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;\"><span style=\"position:absolute;left:0;top:0;width:100%;height:100%;display:table;table-layout:fixed;white-space:normal;font-size:initial;line-height:initial;color:initial;\"></span></span><span style=\"display:none;line-height:initial;\">Image</span></a></li></ul></div>", +        "glossary-no-dictionary": "<div style=\"text-align: left;\"><i>(n)</i> <ul><li>gazou definition 1</li><li><a target=\"_blank\" rel=\"noreferrer noopener\" style=\"cursor:inherit;display:inline-block;position:relative;line-height:1;max-width:100%;color:inherit;\"><span style=\"display:inline-block;white-space:nowrap;max-width:100%;max-height:100vh;position:relative;vertical-align:top;line-height:0;overflow:hidden;font-size:1px;width: 350em;\"><span style=\"display:inline-block;width:0;vertical-align:top;font-size:0;padding-top: 100%;\"></span><span style=\"--image:none;position:absolute;left:0;top:0;width:100%;height:100%;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center center;-webkit-mask-mode:alpha;-webkit-mask-size:contain;-webkit-mask-image:var(--image);mask-repeat:no-repeat;mask-position:center center;mask-mode:alpha;mask-size:contain;mask-image:var(--image);background-color:currentColor;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;display:none;\"></span><img alt=\"\" style=\"display:inline-block;vertical-align:top;object-fit:contain;border:none;outline:none;position:absolute;left:0;top:0;width:100%;height:100%;display:none;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;\"><span style=\"position:absolute;left:0;top:0;width:100%;height:100%;display:table;table-layout:fixed;white-space:normal;font-size:initial;line-height:initial;color:initial;\"></span></span><span style=\"display:none;line-height:initial;\">Image</span></a></li></ul></div>",          "part-of-speech": "Noun",          "pitch-accents": "No pitch accent data",          "pitch-accent-graphs": "No pitch accent data", @@ -1042,9 +1042,9 @@          "frequencies": "",          "furigana": "<ruby>画像<rt>がぞう</rt></ruby>",          "furigana-plain": "画像[がぞう]", -        "glossary": "<div style=\"text-align: left;\"><i>(n, Test Dictionary 2)</i> <ul><li>gazou definition 1</li><li>[object Object]</li></ul></div>", -        "glossary-brief": "<div style=\"text-align: left;\"><ul><li>gazou definition 1</li><li>[object Object]</li></ul></div>", -        "glossary-no-dictionary": "<div style=\"text-align: left;\"><i>(n)</i> <ul><li>gazou definition 1</li><li>[object Object]</li></ul></div>", +        "glossary": "<div style=\"text-align: left;\"><i>(n, Test Dictionary 2)</i> <ul><li>gazou definition 1</li><li><a target=\"_blank\" rel=\"noreferrer noopener\" style=\"cursor:inherit;display:inline-block;position:relative;line-height:1;max-width:100%;color:inherit;\"><span style=\"display:inline-block;white-space:nowrap;max-width:100%;max-height:100vh;position:relative;vertical-align:top;line-height:0;overflow:hidden;font-size:1px;width: 350em;\"><span style=\"display:inline-block;width:0;vertical-align:top;font-size:0;padding-top: 100%;\"></span><span style=\"--image:none;position:absolute;left:0;top:0;width:100%;height:100%;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center center;-webkit-mask-mode:alpha;-webkit-mask-size:contain;-webkit-mask-image:var(--image);mask-repeat:no-repeat;mask-position:center center;mask-mode:alpha;mask-size:contain;mask-image:var(--image);background-color:currentColor;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;display:none;\"></span><img alt=\"\" style=\"display:inline-block;vertical-align:top;object-fit:contain;border:none;outline:none;position:absolute;left:0;top:0;width:100%;height:100%;display:none;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;\"><span style=\"position:absolute;left:0;top:0;width:100%;height:100%;display:table;table-layout:fixed;white-space:normal;font-size:initial;line-height:initial;color:initial;\"></span></span><span style=\"display:none;line-height:initial;\">Image</span></a></li></ul></div>", +        "glossary-brief": "<div style=\"text-align: left;\"><ul><li>gazou definition 1</li><li><a target=\"_blank\" rel=\"noreferrer noopener\" style=\"cursor:inherit;display:inline-block;position:relative;line-height:1;max-width:100%;color:inherit;\"><span style=\"display:inline-block;white-space:nowrap;max-width:100%;max-height:100vh;position:relative;vertical-align:top;line-height:0;overflow:hidden;font-size:1px;width: 350em;\"><span style=\"display:inline-block;width:0;vertical-align:top;font-size:0;padding-top: 100%;\"></span><span style=\"--image:none;position:absolute;left:0;top:0;width:100%;height:100%;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center center;-webkit-mask-mode:alpha;-webkit-mask-size:contain;-webkit-mask-image:var(--image);mask-repeat:no-repeat;mask-position:center center;mask-mode:alpha;mask-size:contain;mask-image:var(--image);background-color:currentColor;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;display:none;\"></span><img alt=\"\" style=\"display:inline-block;vertical-align:top;object-fit:contain;border:none;outline:none;position:absolute;left:0;top:0;width:100%;height:100%;display:none;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;\"><span style=\"position:absolute;left:0;top:0;width:100%;height:100%;display:table;table-layout:fixed;white-space:normal;font-size:initial;line-height:initial;color:initial;\"></span></span><span style=\"display:none;line-height:initial;\">Image</span></a></li></ul></div>", +        "glossary-no-dictionary": "<div style=\"text-align: left;\"><i>(n)</i> <ul><li>gazou definition 1</li><li><a target=\"_blank\" rel=\"noreferrer noopener\" style=\"cursor:inherit;display:inline-block;position:relative;line-height:1;max-width:100%;color:inherit;\"><span style=\"display:inline-block;white-space:nowrap;max-width:100%;max-height:100vh;position:relative;vertical-align:top;line-height:0;overflow:hidden;font-size:1px;width: 350em;\"><span style=\"display:inline-block;width:0;vertical-align:top;font-size:0;padding-top: 100%;\"></span><span style=\"--image:none;position:absolute;left:0;top:0;width:100%;height:100%;-webkit-mask-repeat:no-repeat;-webkit-mask-position:center center;-webkit-mask-mode:alpha;-webkit-mask-size:contain;-webkit-mask-image:var(--image);mask-repeat:no-repeat;mask-position:center center;mask-mode:alpha;mask-size:contain;mask-image:var(--image);background-color:currentColor;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;display:none;\"></span><img alt=\"\" style=\"display:inline-block;vertical-align:top;object-fit:contain;border:none;outline:none;position:absolute;left:0;top:0;width:100%;height:100%;display:none;image-rendering:auto;image-rendering:-moz-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;image-rendering:crisp-edges;\"><span style=\"position:absolute;left:0;top:0;width:100%;height:100%;display:table;table-layout:fixed;white-space:normal;font-size:initial;line-height:initial;color:initial;\"></span></span><span style=\"display:none;line-height:initial;\">Image</span></a></li></ul></div>",          "part-of-speech": "Noun",          "pitch-accents": "No pitch accent data",          "pitch-accent-graphs": "No pitch accent data", diff --git a/test/test-anki-note-builder.js b/test/test-anki-note-builder.js index d7045254..2517e521 100644 --- a/test/test-anki-note-builder.js +++ b/test/test-anki-note-builder.js @@ -29,9 +29,13 @@ function clone(value) {  async function createVM() {      const dom = new JSDOM(); -    const {document} = dom.window; +    const {Node, NodeFilter, document} = dom.window; -    const vm = new TranslatorVM({document}); +    const vm = new TranslatorVM({ +        Node, +        NodeFilter, +        document +    });      const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1');      await vm.prepare(dictionaryDirectory, 'Test Dictionary 2'); @@ -39,6 +43,8 @@ async function createVM() {      vm.execute([          'js/data/anki-note-builder.js',          'js/data/anki-util.js', +        'js/dom/css-style-applier.js', +        'js/display/structured-content-generator.js',          'js/templates/template-renderer.js',          'lib/handlebars.min.js'      ]); @@ -46,11 +52,13 @@ async function createVM() {      const [          JapaneseUtil,          TemplateRenderer, -        AnkiNoteBuilder +        AnkiNoteBuilder, +        CssStyleApplier      ] = vm.get([          'JapaneseUtil',          'TemplateRenderer', -        'AnkiNoteBuilder' +        'AnkiNoteBuilder', +        'CssStyleApplier'      ]);      const ankiNoteDataCreator = vm.ankiNoteDataCreator; @@ -58,7 +66,8 @@ async function createVM() {          constructor() {              this._preparePromise = null;              this._japaneseUtil = new JapaneseUtil(null); -            this._templateRenderer = new TemplateRenderer(this._japaneseUtil); +            this._cssStyleApplier = new CssStyleApplier('/data/structured-content-style.json'); +            this._templateRenderer = new TemplateRenderer(this._japaneseUtil, this._cssStyleApplier);              this._templateRenderer.registerDataType('ankiNote', {                  modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData),                  composeData: (marker, commonData) => ({marker, commonData}) @@ -83,7 +92,7 @@ async function createVM() {          }          async _prepareInternal() { -            // Empty +            await this._cssStyleApplier.prepare();          }          _serializeError(error) { diff --git a/test/test-options-util.js b/test/test-options-util.js index 47b09b94..7d86743b 100644 --- a/test/test-options-util.js +++ b/test/test-options-util.js @@ -589,7 +589,7 @@ function createOptionsUpdatedTestData1() {              }          ],          profileCurrent: 0, -        version: 12, +        version: 13,          global: {              database: {                  prefixWildcardsSupported: false @@ -655,7 +655,8 @@ async function testFieldTemplatesUpdate(extDir) {          {version: 6,  changes: loadDataFile('data/templates/anki-field-templates-upgrade-v6.handlebars')},          {version: 8,  changes: loadDataFile('data/templates/anki-field-templates-upgrade-v8.handlebars')},          {version: 10, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v10.handlebars')}, -        {version: 12, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v12.handlebars')} +        {version: 12, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v12.handlebars')}, +        {version: 13, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v13.handlebars')}      ];      const getUpdateAdditions = (startVersion=0) => {          let value = ''; @@ -875,11 +876,11 @@ ${getUpdateAdditions()}          {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}      {{~/unless~}}      {{~#if (op "<=" glossary.length 1)~}} -        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}}      {{~else if @root.compactGlossaries~}} -        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}}      {{~else~}} -        <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> +        <ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul>      {{~/if~}}      {{~#set "previousDictionary" dictionary~}}{{~/set~}}  {{/inline}} @@ -920,6 +921,27 @@ ${getUpdateAdditions()}  ${getUpdateAdditions(7)}  {{~> (lookup . "marker") ~}}`.trimStart() +        }, +        // formatGlossary update +        { +            oldVersion: 12, +            old: ` +    {{~#if (op "<=" glossary.length 1)~}} +        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} +    {{~else if @root.compactGlossaries~}} +        {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} +    {{~else~}} +        <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> +    {{~/if~}}`.trimStart(), + +            expected: ` +    {{~#if (op "<=" glossary.length 1)~}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} +    {{~else if @root.compactGlossaries~}} +        {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}} +    {{~else~}} +        <ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul> +    {{~/if~}}`.trimStart()          }      ];  |