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
This commit is contained in:
toasted-nutbread 2021-07-02 22:46:38 -04:00 committed by GitHub
parent a4715935cb
commit ca97e38bd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 257 additions and 33 deletions

View File

@ -84,7 +84,13 @@ class Blob {
} }
async function fetch(url2) { 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(); await Promise.resolve();
const content = fs.readFileSync(filePath, {encoding: null}); const content = fs.readFileSync(filePath, {encoding: null});
return { return {

View File

@ -645,6 +645,35 @@ Returns an array representing the different pitch categories for a specific term
</details> </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 ## Legacy Helpers
Yomichan has historically used Handlebars templates to generate the HTML used on the search page and results popup. Yomichan has historically used Handlebars templates to generate the HTML used on the search page and results popup.

View File

@ -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}}
{{>>>>>>>}}

View File

@ -21,11 +21,11 @@
{{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
{{~/unless~}} {{~/unless~}}
{{~#if (op "<=" glossary.length 1)~}} {{~#if (op "<=" glossary.length 1)~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}}
{{~else if @root.compactGlossaries~}} {{~else if @root.compactGlossaries~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}}
{{~else~}} {{~else~}}
<ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> <ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul>
{{~/if~}} {{~/if~}}
{{~#set "previousDictionary" dictionary~}}{{~/set~}} {{~#set "previousDictionary" dictionary~}}{{~/set~}}
{{/inline}} {{/inline}}

View File

@ -461,7 +461,8 @@ class OptionsUtil {
{async: false, update: this._updateVersion9.bind(this)}, {async: false, update: this._updateVersion9.bind(this)},
{async: true, update: this._updateVersion10.bind(this)}, {async: true, update: this._updateVersion10.bind(this)},
{async: false, update: this._updateVersion11.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; 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;
}
} }

View File

@ -72,12 +72,6 @@ class CssStyleApplier {
element.removeAttribute('style'); element.removeAttribute('style');
} }
} }
for (const element of elements) {
const {dataset} = element;
for (const key of Object.keys(dataset)) {
delete dataset[key];
}
}
} }
// Private // Private

View File

@ -17,14 +17,17 @@
/* globals /* globals
* AnkiNoteDataCreator * AnkiNoteDataCreator
* CssStyleApplier
* JapaneseUtil * JapaneseUtil
* TemplateRenderer * TemplateRenderer
* TemplateRendererFrameApi * TemplateRendererFrameApi
*/ */
(() => { (async () => {
const cssStyleApplier = new CssStyleApplier('/data/structured-content-style.json');
await cssStyleApplier.prepare();
const japaneseUtil = new JapaneseUtil(null); const japaneseUtil = new JapaneseUtil(null);
const templateRenderer = new TemplateRenderer(japaneseUtil); const templateRenderer = new TemplateRenderer(japaneseUtil, cssStyleApplier);
const ankiNoteDataCreator = new AnkiNoteDataCreator(japaneseUtil); const ankiNoteDataCreator = new AnkiNoteDataCreator(japaneseUtil);
templateRenderer.registerDataType('ankiNote', { templateRenderer.registerDataType('ankiNote', {
modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData), modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData),

View File

@ -18,11 +18,13 @@
/* global /* global
* DictionaryDataUtil * DictionaryDataUtil
* Handlebars * Handlebars
* StructuredContentGenerator
*/ */
class TemplateRenderer { class TemplateRenderer {
constructor(japaneseUtil) { constructor(japaneseUtil, cssStyleApplier) {
this._japaneseUtil = japaneseUtil; this._japaneseUtil = japaneseUtil;
this._cssStyleApplier = cssStyleApplier;
this._cache = new Map(); this._cache = new Map();
this._cacheMaxSize = 5; this._cacheMaxSize = 5;
this._helpersRegistered = false; this._helpersRegistered = false;
@ -31,6 +33,7 @@ class TemplateRenderer {
this._requirements = null; this._requirements = null;
this._cleanupCallbacks = null; this._cleanupCallbacks = null;
this._customData = null; this._customData = null;
this._temporaryElement = null;
} }
registerDataType(name, {modifier=null, composeData=null}) { registerDataType(name, {modifier=null, composeData=null}) {
@ -161,7 +164,8 @@ class TemplateRenderer {
['typeof', this._getTypeof.bind(this)], ['typeof', this._getTypeof.bind(this)],
['join', this._join.bind(this)], ['join', this._join.bind(this)],
['concat', this._concat.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) { for (const [name, helper] of helpers) {
@ -244,8 +248,12 @@ class TemplateRenderer {
return result; return result;
} }
_stringToMultiLineHtml(string) {
return string.split('\n').join('<br>');
}
_multiLine(context, options) { _multiLine(context, options) {
return options.fn(context).split('\n').join('<br>'); return this._stringToMultiLineHtml(options.fn(context));
} }
_sanitizeCssClass(context, options) { _sanitizeCssClass(context, options) {
@ -497,4 +505,130 @@ class TemplateRenderer {
} }
return [...categories]; 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) : '';
}
} }

View File

@ -18,6 +18,8 @@
<script src="/lib/handlebars.min.js"></script> <script src="/lib/handlebars.min.js"></script>
<script src="/js/data/anki-note-data-creator.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/dictionary-data-util.js"></script>
<script src="/js/language/japanese-util.js"></script> <script src="/js/language/japanese-util.js"></script>
<script src="/js/templates/template-renderer.js"></script> <script src="/js/templates/template-renderer.js"></script>

View File

@ -587,9 +587,9 @@
"frequencies": "", "frequencies": "",
"furigana": "<ruby>画像<rt>がぞう</rt></ruby>", "furigana": "<ruby>画像<rt>がぞう</rt></ruby>",
"furigana-plain": "画像[がぞう]", "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": "<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>[object Object]</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>[object Object]</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", "part-of-speech": "Noun",
"pitch-accents": "No pitch accent data", "pitch-accents": "No pitch accent data",
"pitch-accent-graphs": "No pitch accent data", "pitch-accent-graphs": "No pitch accent data",
@ -1042,9 +1042,9 @@
"frequencies": "", "frequencies": "",
"furigana": "<ruby>画像<rt>がぞう</rt></ruby>", "furigana": "<ruby>画像<rt>がぞう</rt></ruby>",
"furigana-plain": "画像[がぞう]", "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": "<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>[object Object]</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>[object Object]</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", "part-of-speech": "Noun",
"pitch-accents": "No pitch accent data", "pitch-accents": "No pitch accent data",
"pitch-accent-graphs": "No pitch accent data", "pitch-accent-graphs": "No pitch accent data",

View File

@ -29,9 +29,13 @@ function clone(value) {
async function createVM() { async function createVM() {
const dom = new JSDOM(); 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'); const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1');
await vm.prepare(dictionaryDirectory, 'Test Dictionary 2'); await vm.prepare(dictionaryDirectory, 'Test Dictionary 2');
@ -39,6 +43,8 @@ async function createVM() {
vm.execute([ vm.execute([
'js/data/anki-note-builder.js', 'js/data/anki-note-builder.js',
'js/data/anki-util.js', 'js/data/anki-util.js',
'js/dom/css-style-applier.js',
'js/display/structured-content-generator.js',
'js/templates/template-renderer.js', 'js/templates/template-renderer.js',
'lib/handlebars.min.js' 'lib/handlebars.min.js'
]); ]);
@ -46,11 +52,13 @@ async function createVM() {
const [ const [
JapaneseUtil, JapaneseUtil,
TemplateRenderer, TemplateRenderer,
AnkiNoteBuilder AnkiNoteBuilder,
CssStyleApplier
] = vm.get([ ] = vm.get([
'JapaneseUtil', 'JapaneseUtil',
'TemplateRenderer', 'TemplateRenderer',
'AnkiNoteBuilder' 'AnkiNoteBuilder',
'CssStyleApplier'
]); ]);
const ankiNoteDataCreator = vm.ankiNoteDataCreator; const ankiNoteDataCreator = vm.ankiNoteDataCreator;
@ -58,7 +66,8 @@ async function createVM() {
constructor() { constructor() {
this._preparePromise = null; this._preparePromise = null;
this._japaneseUtil = new JapaneseUtil(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', { this._templateRenderer.registerDataType('ankiNote', {
modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData), modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData),
composeData: (marker, commonData) => ({marker, commonData}) composeData: (marker, commonData) => ({marker, commonData})
@ -83,7 +92,7 @@ async function createVM() {
} }
async _prepareInternal() { async _prepareInternal() {
// Empty await this._cssStyleApplier.prepare();
} }
_serializeError(error) { _serializeError(error) {

View File

@ -589,7 +589,7 @@ function createOptionsUpdatedTestData1() {
} }
], ],
profileCurrent: 0, profileCurrent: 0,
version: 12, version: 13,
global: { global: {
database: { database: {
prefixWildcardsSupported: false prefixWildcardsSupported: false
@ -655,7 +655,8 @@ async function testFieldTemplatesUpdate(extDir) {
{version: 6, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v6.handlebars')}, {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: 8, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v8.handlebars')},
{version: 10, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v10.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) => { const getUpdateAdditions = (startVersion=0) => {
let value = ''; let value = '';
@ -875,11 +876,11 @@ ${getUpdateAdditions()}
{{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
{{~/unless~}} {{~/unless~}}
{{~#if (op "<=" glossary.length 1)~}} {{~#if (op "<=" glossary.length 1)~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}}
{{~else if @root.compactGlossaries~}} {{~else if @root.compactGlossaries~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}}
{{~else~}} {{~else~}}
<ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> <ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul>
{{~/if~}} {{~/if~}}
{{~#set "previousDictionary" dictionary~}}{{~/set~}} {{~#set "previousDictionary" dictionary~}}{{~/set~}}
{{/inline}} {{/inline}}
@ -920,6 +921,27 @@ ${getUpdateAdditions()}
${getUpdateAdditions(7)} ${getUpdateAdditions(7)}
{{~> (lookup . "marker") ~}}`.trimStart() {{~> (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()
} }
]; ];