diff --git a/README.md b/README.md index 20306688c1..1f25c0574e 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ updates automatically. ## Migrating from Yomichan +### Exporting Data + If you are an existing user of Yomichan, you can export your dictionary collection and settings such that they can be imported into Yomitan to reflect your setup exactly as it was. You can export your settings from Yomichan's Settings page. Go to the `Backup` section and click on `Export Settings`. @@ -82,7 +84,26 @@ You can export your settings from Yomichan's Settings page. Go to the `Backup` s Yomichan doesn't have first-class support to export the dictionary collection. Please follow the instructions provided in the following link to export your data: https://github.com/themoeway/yomichan-data-exporter#steps-to-export-the-data -You can them import the exported files into Yomitan from the `Backup` section of the `Settings` page. Please see [the section on importing dictionaries](#importing-dictionaries) further below for more explicit steps. +You can then import the exported files into Yomitan from the `Backup` section of the `Settings` page. Please see [the section on importing dictionaries](#importing-dictionaries) further below for more explicit steps. + +### Custom Templates + +If you do not use custom templates for Anki note creation, this section can be skipped. + +Due to security concerns, an alternate implementation of Handlebars is being used which behaves slightly differently. +This revealed a bug in four of Yomitan's template helpers, which have now been fixed in the default templates. If your +custom templates use the following helpers, please ensure their use matches the corrected forms. + +Helper | Example | Corrected +-------|---------|---------- +`formatGlossary` | `{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}` | `{{formatGlossary ../dictionary .}}` +`furigana` | `{{#furigana}}{{{definition}}}{{/furigana}}` | `{{furigana definition}}` +`furiganaPlain` | `{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}` | `{{~furiganaPlain .~}}` +`dumpObject` | `{{#dumpObject}}{{{.}}}{{/dumpObject}}` | `{{dumpObject .}}` + +Authors of custom templates may be interested to know that other helpers previously used and documented in the block +form (e.g. `{{#set "key" "value"}}{{/set}}`), while not broken by this change, may also be replaced with the less verbose +form (e.g. `{{set "key" "value"}}`). The default templates and helper documentation have been changed to reflect this. ## Dictionaries diff --git a/docs/templates.md b/docs/templates.md index 778b8e2dba..030ca3d247 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -14,7 +14,7 @@ This function can be helpful for debugging values when creating templates.
Syntax: - {{#dumpObject}}<object>{{/dumpObject}} + {{dumpObject object}} * _`object`_
The object to convert. @@ -23,7 +23,7 @@ This function can be helpful for debugging values when creating templates. Example: ```handlebars -
{{#dumpObject}}{{.}}{{/dumpObject}}
+
{{dumpObject .}}
``` Output: @@ -49,8 +49,8 @@ Converts a definition or expression/reading pair to its furigana representation.
Syntax: - {{#furigana}}<definition>{{/furigana}}
- {{#furigana expression reading}}{{/furigana}}
+ {{furigana definition}}
+ {{furigana expression reading}}
* _`definition`_
The definition to convert. @@ -63,8 +63,8 @@ Converts a definition or expression/reading pair to its furigana representation. Example: ```handlebars - {{#furigana}}{{.}}{{/furigana}} - {{#furigana "読む" "よむ"}}{{/furigana}} + {{furigana .}} + {{furigana "読む" "よむ"}} ``` Output: @@ -84,8 +84,8 @@ Converts a definition or expression/reading pair to its simplified furigana repr
Syntax: - {{#furiganaPlain}}<definition>{{/furigana}} - {{#furiganaPlain expression reading}}{{/furiganaPlain}}
+ {{furiganaPlain definition}} + {{furiganaPlain expression reading}}
* _`definition`_
The definition to convert. @@ -98,8 +98,8 @@ Converts a definition or expression/reading pair to its simplified furigana repr Example: ```handlebars - {{~#furiganaPlain~}}{{.}}{{~/furiganaPlain~}} - {{#furiganaPlain "読む" "よむ"}}{{/furiganaPlain}} + {{~furiganaPlain .~}} + {{furiganaPlain "読む" "よむ"}} ``` Output: @@ -122,11 +122,11 @@ Replaces newline characters with a forced HTML line break `
`. Example: ```handlebars - {{#kanjiLinks~}} + {{#multiLine~}} some multiline text - {{~/kanjiLinks}} + {{~/multiLine}} ``` Output: @@ -147,7 +147,7 @@ Uses a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScr Syntax: {{#regexReplace regex replacement [flags]}}text-to-modify{{/regexReplace}}
- {{#regexReplace regex replacement [flags] [text-to-modify]...}}{{/regexReplace}}
+ {{regexReplace regex replacement [flags] [text-to-modify]...}}
* _`regex`_
The raw string used to create the regular expression. This value is passed to the [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) constructor. @@ -181,7 +181,7 @@ Uses a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScr Syntax: {{#regexMatch regex [flags]}}text-to-modify{{/regexMatch}}
- {{#regexMatch regex [flags] [text-to-modify]...}}{{/regexMatch}}
+ {{regexMatch regex [flags] [text-to-modify]...}}
* _`regex`_
The raw string used to create the regular expression. This value is passed to the [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) constructor. @@ -212,7 +212,7 @@ Creates a set of all unique tags for the definition and returns a text represent
Syntax: - {{#mergeTags definition isGroupMode isMergeMode}}{{/mergeTags}} + {{mergeTags definition isGroupMode isMergeMode}} * _`definition`_
The root definition object. @@ -225,7 +225,7 @@ Creates a set of all unique tags for the definition and returns a text represent Example: ```handlebars - {{~#mergeTags definition group merge}}{{/mergeTags~}} + {{~mergeTags definition group merge~}} ``` Output: @@ -235,9 +235,9 @@ Creates a set of all unique tags for the definition and returns a text represent
-### `eachUpTo` +### `#eachUpTo` -Similar to the built-in `each` function, but iterates up to a maximum count. +Similar to the built-in `#each` function, but iterates up to a maximum count. If the iterable is falsy or empty, the `else` condition will be used.
@@ -279,7 +279,7 @@ This allows it to be used similar to an [`Array.concat`](https://developer.mozil
Syntax: - {{#spread iterable1 iterable2 ... iterableN}}{{/spread}} + {{spread iterable1 iterable2 ... iterableN}} * _`iterableN`_
A variable amount of iterable objects to combine into a single array. @@ -314,7 +314,7 @@ If an unknown operator is specified, the `undefined` value is returned.
Syntax: - {{#op operator operand1 [operand2] [operand3]}}{{/op}} + {{op operator operand1 [operand2] [operand3]}} * _`operator`_
One of the unary, binary, or ternary operators. @@ -329,9 +329,9 @@ If an unknown operator is specified, the `undefined` value is returned. Example: ```handlebars - {{#if (op "===" value1 value2)}}Values are equal{{/op~}}
+ {{#if (op "===" value1 value2)}}Values are equal{{/if~}}
{{~#op "-" value1}}{{/op~}}
- {{~#op "?:" value1 "a" "b"}}{{/op}} + {{~op "?:" value1 "a" "b"}} ``` Output: @@ -351,7 +351,7 @@ Gets a value from the custom state stack.
Syntax: - {{#get name}}{{/get}} + {{get name}} * _`name`_
The name of the variable to get. @@ -360,7 +360,7 @@ Gets a value from the custom state stack. Example: ```handlebars - {{#get "some-text"}}{{/get}} + {{get "some-text"}} ``` Output: @@ -378,7 +378,7 @@ Assigns a value to the custom state stack. Syntax: {{#set name}}value{{/get}}
- {{#set name value}}{{/get}}
+ {{set name value}}
* _`name`_
The name of the variable to assign. @@ -390,7 +390,7 @@ Assigns a value to the custom state stack. ```handlebars {{#set "some-text"}}This is the value of some-text!{{/set~}} - {{~#set "some-number" 32}}{{/set}} + {{~set "some-number" 32}} ``` Output: @@ -399,7 +399,7 @@ Assigns a value to the custom state stack.
-### `scope` +### `#scope` Pushes a new variable scope to the custom state stack. Variable assignments are applied to the most recent scope, @@ -419,14 +419,14 @@ and variable lookups will start from the most recent scope and work backwards un Example: ```handlebars - {{~#set "key" 32}}{{/set~}} - {{~#get "key"}}{{/get~}}, + {{~set "key" 32~}} + {{~get "key"~}}, {{~#scope~}} - {{~#get "key"}}{{/get~}}, - {{~#set "key" 64}}{{/set~}} - {{~#get "key"}}{{/get~}}, + {{~#get "key"~}}, + {{~#set "key" 64~}} + {{~#get "key"~}}, {{~/scope~}} - {{~#get "key"}}{{/get~}} + {{~get "key"~}} ``` Output: @@ -443,7 +443,7 @@ Repeatedly gets a property of an object.
Syntax: - {{#property object property1 property2 ... propertyN}}{{/property}} + {{property object property1 property2 ... propertyN}} * _`object`_
The initial object to use. @@ -494,7 +494,7 @@ Returns whether or not a mora will have a high pitch, given the index of the mor
Syntax: - {{#isMoraPitchHigh index position}}{{/isMoraPitchHigh}} + {{isMoraPitchHigh index position}}
Example: @@ -517,7 +517,7 @@ Returns an array of the mora for a kana string.
Syntax: - {{#getKanaMorae kana-string}}{{/getKanaMorae}} + {{getKanaMorae kana-string}}
Example: @@ -538,12 +538,12 @@ Returns an array of the mora for a kana string. ### `typeof` -Returns the type of a value. +Returns the type of a value. Use of `#typeof` in the block form may be nonportable.
Syntax: - {{#typeof value}}{{/typeof}}
+ {{typeof value}}
{{#typeof}}value{{/typeof}}
* _`value`_
@@ -553,8 +553,8 @@ Returns the type of a value. Example: ```handlebars - {{#typeof "よみちゃん"}}{{/typeof}} - {{#typeof 1}}{{/typeof}} + {{typeof "よみちゃん"}} + {{typeof 1}} {{#typeof}}よみちゃん{{/typeof}} ``` @@ -574,7 +574,7 @@ Joins the arguments to a single string with a separator, flattening any argument
Syntax: - {{#join separator value1 value2 valueN...}}{{/join}}
+ {{join separator value1 value2 valueN...}}
* _`separator`_
The separator string to use between values. @@ -585,8 +585,8 @@ Joins the arguments to a single string with a separator, flattening any argument Example: ```handlebars - {{#set "index" 32}}{{/set~}} - {{~#join "_" "yomichan" (get "index") "value"}}{{/join}} + {{set "index" 32~}} + {{~join "_" "yomichan" (get "index") "value"}} ``` Output: @@ -603,7 +603,7 @@ Joins the arguments to a single string, without flattening arguments that are ar
Syntax: - {{#concat value1 value1 valueN...}}{{/concat}}
+ {{concat value1 value1 valueN...}}
* _`valueN`_
A value to join into the resulting string @@ -612,8 +612,8 @@ Joins the arguments to a single string, without flattening arguments that are ar Example: ```handlebars - {{#set "index" 32}}{{/set~}} - {{~#concat "yomichan_" (get "index") "_value"}}{{/concat}} + {{set "index" 32~}} + {{~concat "yomichan_" (get "index") "_value"}} ``` Output: @@ -630,7 +630,7 @@ Returns an array representing the different pitch categories for a specific term
Syntax: - {{#pitchCategories @root}}{{/pitchCategories}}
+ {{pitchCategories @root}}
* _`@root`_
The argument passed should always be the root data object. @@ -657,7 +657,7 @@ structured-content generation.
Syntax: - {{#formatGlossary dictionary}}{{{definitionEntry}}}{{/pitchCategories}}
+ {{formatGlossary dictionary definitionEntry}}
* _`dictionary`_
The dictionary that the glossary entry belongs to. @@ -668,7 +668,7 @@ structured-content generation. Example: ```handlebars - {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} + {{#each glossary}}{{formatGlossary ../dictionary .}}{{/each}} ``` Output: @@ -686,8 +686,8 @@ These functions are used together in order to request media and other types of o
Syntax: - {{#hasMedia type args...}}{{/hasMedia}}
- {{#getMedia type args... [escape=true|false]}}{{/getMedia}}
+ {{hasMedia type args...}}
+ {{getMedia type args... [escape=true|false]}}
* _`type`_
The type of media to check for. @@ -710,19 +710,19 @@ These functions are used together in order to request media and other types of o Examples: ```handlebars - {{#if (hasMedia "audio")}}The audio file name is: {{#getMedia "audio"}}{{/getMedia}}{{/if}} + {{#if (hasMedia "audio")}}The audio file name is: {{getMedia "audio"}}{{/if}} - {{#if (hasMedia "screenshot")}}The screenshot file name is: {{#getMedia "screenshot"}}{{/getMedia}}{{/if}} + {{#if (hasMedia "screenshot")}}The screenshot file name is: {{getMedia "screenshot"}}{{/if}} - {{#if (hasMedia "clipboardImage")}}The clipboard image file name is: {{#getMedia "clipboardImage"}}{{/getMedia}}{{/if}} + {{#if (hasMedia "clipboardImage")}}The clipboard image file name is: {{getMedia "clipboardImage"}}{{/if}} - {{#if (hasMedia "clipboardText")}}The clipboard text is: {{#getMedia "clipboardText"}}{{/getMedia}}{{/if}} + {{#if (hasMedia "clipboardText")}}The clipboard text is: {{getMedia "clipboardText"}}{{/if}} - {{#if (hasMedia "selectionText")}}The selection text is: {{#getMedia "selectionText"}}{{/getMedia}}{{/if}} + {{#if (hasMedia "selectionText")}}The selection text is: {{getMedia "selectionText"}}{{/if}} - {{#if (hasMedia "textFurigana" "日本語")}}This is an example of text with generated furigana: {{#getMedia "textFurigana" "日本語" escape=false}}{{/getMedia}}{{/if}} + {{#if (hasMedia "textFurigana" "日本語")}}This is an example of text with generated furigana: {{getMedia "textFurigana" "日本語" escape=false}}{{/if}} - {{#if (hasMedia "dictionaryMedia" "image.png" dictionary="Example Dictionary")}}The remapped file name for image.png is: {{#getMedia "dictionaryMedia" "image.png" dictionary="Example Dictionary"}}{{/getMedia}}{{/if}} + {{#if (hasMedia "dictionaryMedia" "image.png" dictionary="Example Dictionary")}}The remapped file name for image.png is: {{getMedia "dictionaryMedia" "image.png" dictionary="Example Dictionary"}}{{/if}} ``` Output: @@ -754,7 +754,7 @@ same as the system used for generating popup and search page dictionary entries.
Syntax: - {{#pronunciation format=string reading=string downstepPosition=integer [nasalPositions=array] [devoicePositions=array]}}{{/pronunciation}}
+ {{pronunciation format=string reading=string downstepPosition=integer [nasalPositions=array] [devoicePositions=array]}}
* _`format`_
The format of the HTML to generate. This can be any of the following values: @@ -774,7 +774,7 @@ same as the system used for generating popup and search page dictionary entries. Example: ```handlebars - {{~#pronunciation format='text' reading='よむ' downstepPosition=1~}}{{~/pronunciation~}} + {{~pronunciation format='text' reading='よむ' downstepPosition=1~}} ```
@@ -786,7 +786,7 @@ Converts katakana text to hiragana.
Syntax: - {{#hiragana value [keepProlongedSoundMarks=true|false]}}{{/hiragana}}
+ {{hiragana value [keepProlongedSoundMarks=true|false]}}
{{#hiragana [keepProlongedSoundMarks=true|false]}}value{{/hiragana}}
* _`value`_
@@ -799,7 +799,7 @@ Converts katakana text to hiragana. Example: ```handlebars - {{#hiragana "よみちゃん ヨミちゃん ヨミチャン"}}{{/hiragana}} + {{hiragana "よみちゃん ヨミちゃん ヨミチャン"}} {{#hiragana}}よみちゃん ヨミちゃん ヨミチャン{{/hiragana}} {{#hiragana}}ローマ字{{/hiragana}} {{#hiragana keepProlongedSoundMarks=true}}ローマ字{{/hiragana}} @@ -822,7 +822,7 @@ Converts hiragana text to katakana.
Syntax: - {{#katakana text}}{{/katakana}}
+ {{katakana text}}
{{#katakana}}text{{/katakana}}
* _`text`_
@@ -832,7 +832,7 @@ Converts hiragana text to katakana. Example: ```handlebars - {{#katakana "よみちゃん ヨミちゃん ヨミチャン"}}{{/katakana}} + {{katakana "よみちゃん ヨミちゃん ヨミチャン"}} {{#katakana}}よみちゃん ヨミちゃん ヨミチャン{{/katakana}} ``` @@ -842,60 +842,3 @@ Converts hiragana text to katakana. ヨミチャン ヨミチャン ヨミチャン ```
- - -## Legacy Helpers - -Yomichan has historically used Handlebars templates to generate the HTML used on the search page and results popup. -To simplify the and improve Yomichan's capabilities, the HTML elements are now generated directly using a different process. - -As such, there are several leftover Handlebars helpers that do not have much utility for Anki templates, but are kept for compatibility purposes. - - -### `kanjiLinks` - -Replaces kanji characters in the text with linkified versions. - -
- Syntax: - - {{#kanjiLinks}}text{{/kanjiLinks}} -
-
- Example: - - ```handlebars - {{#kanjiLinks}}読む{{/kanjiLinks}} - ``` - - Output: - ```html - む - ``` - - Preview: -
-
- - -### `sanitizeCssClass` - -Sanitizes text so it can be used as a CSS class name. - -
- Syntax: - - {{#sanitizeCssClass}}text{{/sanitizeCssClass}} -
-
- Example: - - ```handlebars - {{#sanitizeCssClass}}some text with many types of characters !@#$%^ 読む{{/sanitizeCssClass}} - ``` - - Output: - ```html - some_text_with_many_types_of_characters________読む - ``` -
diff --git a/ext/css/settings.css b/ext/css/settings.css index eaebc3af02..a2618d88c8 100644 --- a/ext/css/settings.css +++ b/ext/css/settings.css @@ -2167,6 +2167,13 @@ button.hotkey-list-item-enabled-button[data-scope-count='0'] { display: none; } +.warn-custom-templates-notification { + border: 1px solid var(--danger-color); +} +:root:not([data-warn-custom-templates=true]) .warn-custom-templates-notification { + display: none; +} + .test-anki-note-viewer-container { margin-top: 0.85em; display: flex; diff --git a/ext/data/templates/anki-field-templates-upgrade-v21.handlebars b/ext/data/templates/anki-field-templates-upgrade-v21.handlebars new file mode 100644 index 0000000000..33c4dc6c02 --- /dev/null +++ b/ext/data/templates/anki-field-templates-upgrade-v21.handlebars @@ -0,0 +1,161 @@ +{{<<<<<<<}} +{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}} +{{=======}} +{{formatGlossary ../dictionary .}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#furigana}}{{{.}}}{{/furigana~}} +{{=======}} +{{~furigana .~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#furigana}}{{{definition}}}{{/furigana}} +{{=======}} +{{furigana definition}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#furigana expression reading~}}{{~/furigana~}} +{{=======}} +{{~furigana expression reading~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#furigana expression reading}}{{/furigana~}} +{{=======}} +{{~furigana expression reading~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}} +{{=======}} +{{~furiganaPlain .~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}} +{{=======}} +{{furiganaPlain definition}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#furiganaPlain expression reading~}}{{~/furiganaPlain~}} +{{=======}} +{{~furiganaPlain expression reading~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#furiganaPlain expression reading}}{{/furiganaPlain~}} +{{=======}} +{{~furiganaPlain expression reading~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#getMedia "audio"}}{{/getMedia}} +{{=======}} +{{getMedia "audio"}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#getMedia "screenshot"}}{{/getMedia}} +{{=======}} +{{getMedia "screenshot"}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#getMedia "clipboardImage"}}{{/getMedia}} +{{=======}} +{{getMedia "clipboardImage"}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#getMedia "clipboardText"}}{{/getMedia}} +{{=======}} +{{getMedia "clipboardText"}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#getMedia "selectionText"}}{{/getMedia}} +{{=======}} +{{getMedia "selectionText"}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{#getMedia "textFurigana" definition.cloze.sentence escape=false}}{{/getMedia}} +{{=======}} +{{getMedia "textFurigana" definition.cloze.sentence escape=false}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}} +{{=======}} +{{~pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set "any" false}}{{/set~}} +{{=======}} +{{~set "any" false~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set "any" true}}{{/set~}} +{{=======}} +{{~set "any" true~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set "previousDictionary" dictionary~}}{{~/set~}} +{{=======}} +{{~set "previousDictionary" dictionary~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} +{{=======}} +{{~set "exclusive" (spread exclusiveExpressions exclusiveReadings)~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set "separator" ""~}}{{/set~}} +{{=======}} +{{~set "separator" ""~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#get "separator"}}{{/get~}} +{{=======}} +{{~get "separator"~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set "found" false}}{{/set~}} +{{=======}} +{{~set "found" false~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set "found" true}}{{/set~}} +{{=======}} +{{~set "found" true~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set "first" true}}{{/set~}} +{{=======}} +{{~set "first" true~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set "first" false~}}{{~/set~}} +{{=======}} +{{~set "first" false~}} +{{>>>>>>>}} + +{{<<<<<<<}} +{{~#set (concat "used_" .) true~}}{{~/set~}} +{{=======}} +{{~set (concat "used_" .) true~}} +{{>>>>>>>}} diff --git a/ext/data/templates/default-anki-field-templates.handlebars b/ext/data/templates/default-anki-field-templates.handlebars index 31d5d13fb9..d94f6d709f 100644 --- a/ext/data/templates/default-anki-field-templates.handlebars +++ b/ext/data/templates/default-anki-field-templates.handlebars @@ -1,19 +1,19 @@ {{#*inline "glossary-single"}} {{~#unless brief~}} {{~#scope~}} - {{~#set "any" false}}{{/set~}} + {{~set "any" false~}} {{~#each definitionTags~}} {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} {{~#if (get "any")}}, {{else}}({{/if~}} {{name}} - {{~#set "any" true}}{{/set~}} + {{~set "any" true~}} {{~/if~}} {{~/each~}} {{~#unless noDictionaryTag~}} {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} {{~#if (get "any")}}, {{else}}({{/if~}} {{dictionary}} - {{~#set "any" true}}{{/set~}} + {{~set "any" true~}} {{~/if~}} {{~/unless~}} {{~#if (get "any")}}) {{/if~}} @@ -21,18 +21,18 @@ {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} {{~/unless~}} {{~#if (op "<=" glossary.length 1)~}} - {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} + {{#each glossary}}{{formatGlossary ../dictionary .}}{{/each}} {{~else if @root.compactGlossaries~}} - {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}} + {{#each glossary}}{{formatGlossary ../dictionary .}}{{#unless @last}} | {{/unless}}{{/each}} {{~else~}} -
    {{#each glossary}}
  • {{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}
  • {{/each}}
+
    {{#each glossary}}
  • {{formatGlossary ../dictionary .}}
  • {{/each}}
{{~/if~}} - {{~#set "previousDictionary" dictionary~}}{{~/set~}} + {{~set "previousDictionary" dictionary~}} {{/inline}} {{#*inline "audio"}} {{~#if (hasMedia "audio")~}} - [sound:{{#getMedia "audio"}}{{/getMedia}}] + [sound:{{getMedia "audio"}}] {{~/if~}} {{/inline}} @@ -78,22 +78,22 @@ {{#*inline "furigana"}} {{~#if merge~}} {{~#each definition.expressions~}} - {{~#furigana}}{{{.}}}{{/furigana~}} + {{~furigana .~}} {{~#unless @last}}、{{/unless~}} {{~/each~}} {{~else~}} - {{#furigana}}{{{definition}}}{{/furigana}} + {{furigana definition}} {{~/if~}} {{/inline}} {{#*inline "furigana-plain"}} {{~#if merge~}} {{~#each definition.expressions~}} - {{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}} + {{~furiganaPlain .~}} {{~#unless @last}}、{{/unless~}} {{~/each~}} {{~else~}} - {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}} + {{furiganaPlain definition}} {{~/if~}} {{/inline}} @@ -174,7 +174,7 @@ {{#*inline "screenshot"}} {{~#if (hasMedia "screenshot")~}} - + {{~/if~}} {{/inline}} @@ -184,16 +184,16 @@ {{! Pitch Accents }} {{#*inline "pitch-accent-item"}} - {{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}} + {{~pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}} {{/inline}} {{#*inline "pitch-accent-item-disambiguation"}} {{~#scope~}} - {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} + {{~set "exclusive" (spread exclusiveExpressions exclusiveReadings)~}} {{~#if (op ">" (property (get "exclusive") "length") 0)~}} - {{~#set "separator" ""~}}{{/set~}} + {{~set "separator" ""~}} ({{#each (get "exclusive")~}} - {{~#get "separator"}}{{/get~}}{{{.}}} + {{~get "separator"~}}{{{.}}} {{~/each}} only) {{~/if~}} {{~/scope~}} @@ -231,12 +231,12 @@ {{#*inline "clipboard-image"}} {{~#if (hasMedia "clipboardImage")~}} - + {{~/if~}} {{/inline}} {{#*inline "clipboard-text"}} - {{~#if (hasMedia "clipboardText")}}{{#getMedia "clipboardText"}}{{/getMedia}}{{/if~}} + {{~#if (hasMedia "clipboardText")}}{{getMedia "clipboardText"}}{{/if~}} {{/inline}} {{#*inline "conjugation"}} @@ -255,7 +255,7 @@
  • {{~#if (op "!==" ../definition.type "kanji")~}} {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}( - {{~#furigana expression reading~}}{{~/furigana~}} + {{~furigana expression reading~}} ) {{/if~}} {{~/if~}} {{~dictionary}}: {{frequency~}} @@ -267,10 +267,10 @@ {{#*inline "stroke-count"}} {{~#scope~}} - {{~#set "found" false}}{{/set~}} + {{~set "found" false~}} {{~#each definition.stats.misc~}} {{~#if (op "===" name "strokes")~}} - {{~#set "found" true}}{{/set~}} + {{~set "found" true~}} Stroke count: {{value}} {{~/if~}} {{~/each~}} @@ -295,14 +295,14 @@ {{#*inline "part-of-speech"}} {{~#scope~}} {{~#if (op "!==" definition.type "kanji")~}} - {{~#set "first" true}}{{/set~}} + {{~set "first" true~}} {{~#each definition.expressions~}} {{~#each wordClasses~}} {{~#unless (get (concat "used_" .))~}} {{~> part-of-speech-pretty . ~}} {{~#unless (get "first")}}, {{/unless~}} - {{~#set (concat "used_" .) true~}}{{~/set~}} - {{~#set "first" false~}}{{~/set~}} + {{~set (concat "used_" .) true~}} + {{~set "first" false~}} {{~/unless~}} {{~/each~}} {{~/each~}} @@ -316,13 +316,13 @@ {{/inline}} {{#*inline "selection-text"}} - {{~#if (hasMedia "selectionText")}}{{#getMedia "selectionText"}}{{/getMedia}}{{/if~}} + {{~#if (hasMedia "selectionText")}}{{getMedia "selectionText"}}{{/if~}} {{/inline}} {{#*inline "sentence-furigana"}} {{~#if definition.cloze~}} {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}} - {{#getMedia "textFurigana" definition.cloze.sentence escape=false}}{{/getMedia}} + {{getMedia "textFurigana" definition.cloze.sentence escape=false}} {{~else~}} {{definition.cloze.sentence}} {{~/if~}} diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 308ae4d5a7..8e8e69452d 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -2127,20 +2127,12 @@ class Backend { } async _openWelcomeGuidePageOnce() { - if (isObject(chrome.storage) && isObject(chrome.storage.session)) { - // Chrome - chrome.storage.session.get(['openedWelcomePage']).then((result) => { - if (!result.openedWelcomePage) { - this._openWelcomeGuidePage(); - chrome.storage.session.set({'openedWelcomePage': true}); - } - }); - } else { - // Firefox (storage.session is not supported yet) - // NOTE: This means that the welcome page will repeatedly open in Firefox - // until they support storage.session. - this._openWelcomeGuidePage(); - } + chrome.storage.session.get(['openedWelcomePage']).then((result) => { + if (!result.openedWelcomePage) { + this._openWelcomeGuidePage(); + chrome.storage.session.set({'openedWelcomePage': true}); + } + }); } async _openWelcomeGuidePage() { diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index 2674701f3f..1f2ffb05fe 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -470,7 +470,8 @@ class OptionsUtil { {async: false, update: this._updateVersion17.bind(this)}, {async: false, update: this._updateVersion18.bind(this)}, {async: false, update: this._updateVersion19.bind(this)}, - {async: false, update: this._updateVersion20.bind(this)} + {async: false, update: this._updateVersion20.bind(this)}, + {async: true, update: this._updateVersion21.bind(this)} ]; if (typeof targetVersion === 'number' && targetVersion < result.length) { result.splice(targetVersion); @@ -997,4 +998,36 @@ class OptionsUtil { } return options; } + + async _updateVersion21(options) { + await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v21.handlebars'); + + let customTemplates = false; + for (const {options: profileOptions} of options.profiles) { + if (profileOptions.anki.fieldTemplates !== null) { + customTemplates = true; + } + } + + if (customTemplates && isObject(chrome.storage)) { + chrome.storage.session.set({'needsCustomTemplatesWarning': true}); + await this._createTab(chrome.runtime.getURL('/welcome.html')); + chrome.storage.session.set({'openedWelcomePage': true}); + } + + return options; + } + + _createTab(url) { + return new Promise((resolve, reject) => { + chrome.tabs.create({url}, (tab) => { + const e = chrome.runtime.lastError; + if (e) { + reject(new Error(e.message)); + } else { + resolve(tab); + } + }); + }); + } } diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js index 521ce2c2de..8039dae5eb 100644 --- a/ext/js/pages/welcome-main.js +++ b/ext/js/pages/welcome-main.js @@ -58,6 +58,13 @@ async function setupGenericSettingsController(genericSettingController) { setupEnvironmentInfo(); + chrome.storage.session.get({'needsCustomTemplatesWarning': false}).then((result) => { + if (result.needsCustomTemplatesWarning) { + document.documentElement.dataset.warnCustomTemplates = 'true'; + chrome.storage.session.remove(['needsCustomTemplatesWarning']); + } + }); + const preparePromises = []; const modalController = new ModalController(); diff --git a/ext/js/templates/sandbox/anki-template-renderer.js b/ext/js/templates/sandbox/anki-template-renderer.js index 789f094246..766c7798a4 100644 --- a/ext/js/templates/sandbox/anki-template-renderer.js +++ b/ext/js/templates/sandbox/anki-template-renderer.js @@ -68,9 +68,7 @@ class AnkiTemplateRenderer { ['dumpObject', this._dumpObject.bind(this)], ['furigana', this._furigana.bind(this)], ['furiganaPlain', this._furiganaPlain.bind(this)], - ['kanjiLinks', this._kanjiLinks.bind(this)], ['multiLine', this._multiLine.bind(this)], - ['sanitizeCssClass', this._sanitizeCssClass.bind(this)], ['regexReplace', this._regexReplace.bind(this)], ['regexMatch', this._regexMatch.bind(this)], ['mergeTags', this._mergeTags.bind(this)], @@ -132,10 +130,14 @@ class AnkiTemplateRenderer { return Handlebars.Utils.escapeExpression(text); } + _safeString(text) { + return new Handlebars.SafeString(text); + } + // Template helpers - _dumpObject(context, options) { - const dump = JSON.stringify(options.fn(context), null, 4); + _dumpObject(context, object) { + const dump = JSON.stringify(object, null, 4); return this._escape(dump); } @@ -145,14 +147,16 @@ class AnkiTemplateRenderer { let result = ''; for (const {text, reading: reading2} of segs) { - if (reading2.length > 0) { - result += `${text}${reading2}`; + const safeText = this._escape(text); + const safeReading = this._escape(reading2); + if (safeReading.length > 0) { + result += `${safeText}${safeReading}`; } else { - result += text; + result += safeText; } } - return result; + return this._safeString(result); } _furiganaPlain(context, ...args) { @@ -173,29 +177,16 @@ class AnkiTemplateRenderer { } _getFuriganaExpressionAndReading(context, ...args) { - const options = args[args.length - 1]; if (args.length >= 3) { return {expression: args[0], reading: args[1]}; - } else { - const {expression, reading} = options.fn(context); + } else if (args.length === 2) { + const {expression, reading} = args[0]; return {expression, reading}; + } else { + return void 0; } } - _kanjiLinks(context, options) { - const jp = this._japaneseUtil; - let result = ''; - for (const c of options.fn(context)) { - if (jp.isCodePointKanji(c.codePointAt(0))) { - result += `${c}`; - } else { - result += c; - } - } - - return result; - } - _stringToMultiLineHtml(string) { return string.split('\n').join('
    '); } @@ -204,10 +195,6 @@ class AnkiTemplateRenderer { return this._stringToMultiLineHtml(options.fn(context)); } - _sanitizeCssClass(context, options) { - return options.fn(context).replace(/[^_a-z0-9\u00a0-\uffff]/ig, '_'); - } - _regexReplace(context, ...args) { // Usage: // {{#regexReplace regex string [flags] [content]...}}content{{/regexReplace}} @@ -219,7 +206,7 @@ class AnkiTemplateRenderer { const options = args[argCount]; let value = typeof options.fn === 'function' ? options.fn(context) : ''; if (argCount > 3) { - value = `${args.slice(3).join('')}${value}`; + value = `${args.slice(3, -1).join('')}${value}`; } if (argCount > 1) { try { @@ -243,7 +230,7 @@ class AnkiTemplateRenderer { const options = args[argCount]; let value = typeof options.fn === 'function' ? options.fn(context) : ''; if (argCount > 2) { - value = `${args.slice(2).join('')}${value}`; + value = `${args.slice(2, -1).join('')}${value}`; } if (argCount > 0) { try { @@ -490,7 +477,7 @@ class AnkiTemplateRenderer { this._normalizeHtml(container, styleApplier, datasetKeyIgnorePattern); const result = container.innerHTML; container.textContent = ''; - return result; + return this._safeString(result); } _normalizeHtml(root, styleApplier, datasetKeyIgnorePattern) { @@ -543,9 +530,8 @@ class AnkiTemplateRenderer { return instance; } - _formatGlossary(context, dictionary, options) { + _formatGlossary(context, dictionary, content, options) { const data = options.data.root; - const content = options.fn(context); if (typeof content === 'string') { return this._stringToMultiLineHtml(this._escape(content)); } if (!(typeof content === 'object' && content !== null)) { return ''; } switch (content.type) { diff --git a/ext/welcome.html b/ext/welcome.html index 14e9836737..561678666d 100644 --- a/ext/welcome.html +++ b/ext/welcome.html @@ -25,6 +25,19 @@

    Welcome to Yomitan!

    + +
    +
    +
    +

    + There are custom Anki templates in your settings. Note that some syntax has changed from previous versions of Yomitan. + Please ensure that your custom templates are using the updated syntax. +

    +
    +
    +
    + +

    Here are some basics to get started

    @@ -49,10 +62,9 @@

    Here are some basics to get started

    Yomitan requires one or more dictionaries to be installed in order to look up terms, kanji, and other information. - Several downloadable dictionaries can be found on the Yomitan homepage, - allowing you to choose the dictionaries most relevant for you. + Several downloadable dictionaries can be found on the Yomitan homepage. Dictionaries can be configured using the button below, - or later from the the Settings page. + or later from the Settings page.
    @@ -67,12 +79,17 @@

    Here are some basics to get started

    - You can also import an exported collection of dictionaries to migrate from a different device or browser from the Backup section of the Settings page. + You can also import an exported collection of dictionaries from the Backup section of the Settings page.

    - If you are migrating from Yomichan, you may be particularly interested in migrating your data from Yomichan into Yomitan. + If you are migrating from Yomichan, you may be interested in importing your data into Yomitan. Please follow instructions from Yomitan's README for that. + +

    + + If you are using or planning to use custom templates for Anki note creation, note that some syntax has changed from Yomichan and Yomibaba. + Please ensure that your custom templates are using the updated syntax.
    diff --git a/test/test-options-util.js b/test/test-options-util.js index 2be6b2f737..d94028c05d 100644 --- a/test/test-options-util.js +++ b/test/test-options-util.js @@ -622,7 +622,7 @@ function createOptionsUpdatedTestData1() { } ], profileCurrent: 0, - version: 20, + version: 21, global: { database: { prefixWildcardsSupported: false @@ -689,7 +689,8 @@ async function testFieldTemplatesUpdate(extDir) { {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: 13, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v13.handlebars')} + {version: 13, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v13.handlebars')}, + {version: 21, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v21.handlebars')} ]; const getUpdateAdditions = (startVersion, targetVersion) => { let value = ''; @@ -1214,6 +1215,370 @@ async function testFieldTemplatesUpdate(extDir) { {{! End Pitch Accents }} <<>> +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // block helper update: furigana and furiganaPlain + { + oldVersion: 20, + newVersion: 21, + old: ` +{{#*inline "furigana"}} + {{~#if merge~}} + {{~#each definition.expressions~}} + {{~#furigana}}{{{.}}}{{/furigana~}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{#furigana}}{{{definition}}}{{/furigana}} + {{~/if~}} +{{/inline}} + +{{#*inline "furigana-plain"}} + {{~#if merge~}} + {{~#each definition.expressions~}} + {{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}} + {{~/if~}} +{{/inline}} + +{{#*inline "frequencies"}} + {{~#if (op ">" definition.frequencies.length 0)~}} +
      + {{~#each definition.frequencies~}} +
    • + {{~#if (op "!==" ../definition.type "kanji")~}} + {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}( + {{~#furigana expression reading~}}{{~/furigana~}} + ) {{/if~}} + {{~/if~}} + {{~dictionary}}: {{frequency~}} +
    • + {{~/each~}} +
    + {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "furigana"}} + {{~#if merge~}} + {{~#each definition.expressions~}} + {{~furigana .~}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{furigana definition}} + {{~/if~}} +{{/inline}} + +{{#*inline "furigana-plain"}} + {{~#if merge~}} + {{~#each definition.expressions~}} + {{~furiganaPlain .~}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{furiganaPlain definition}} + {{~/if~}} +{{/inline}} + +{{#*inline "frequencies"}} + {{~#if (op ">" definition.frequencies.length 0)~}} +
      + {{~#each definition.frequencies~}} +
    • + {{~#if (op "!==" ../definition.type "kanji")~}} + {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}( + {{~furigana expression reading~}} + ) {{/if~}} + {{~/if~}} + {{~dictionary}}: {{frequency~}} +
    • + {{~/each~}} +
    + {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // block helper update: formatGlossary + { + oldVersion: 20, + newVersion: 21, + old: ` +{{#*inline "glossary-single"}} + {{~#unless brief~}} + {{~#scope~}} + {{~#set "any" false}}{{/set~}} + {{~#each definitionTags~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{name}} + {{~#set "any" true}}{{/set~}} + {{~/if~}} + {{~/each~}} + {{~#unless noDictionaryTag~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{dictionary}} + {{~#set "any" true}}{{/set~}} + {{~/if~}} + {{~/unless~}} + {{~#if (get "any")}}) {{/if~}} + {{~/scope~}} + {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} + {{~#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~}} +
      {{#each glossary}}
    • {{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}
    • {{/each}}
    + {{~/if~}} + {{~#set "previousDictionary" dictionary~}}{{~/set~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "glossary-single"}} + {{~#unless brief~}} + {{~#scope~}} + {{~set "any" false~}} + {{~#each definitionTags~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{name}} + {{~set "any" true~}} + {{~/if~}} + {{~/each~}} + {{~#unless noDictionaryTag~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{dictionary}} + {{~set "any" true~}} + {{~/if~}} + {{~/unless~}} + {{~#if (get "any")}}) {{/if~}} + {{~/scope~}} + {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} + {{~#if (op "<=" glossary.length 1)~}} + {{#each glossary}}{{formatGlossary ../dictionary .}}{{/each}} + {{~else if @root.compactGlossaries~}} + {{#each glossary}}{{formatGlossary ../dictionary .}}{{#unless @last}} | {{/unless}}{{/each}} + {{~else~}} +
      {{#each glossary}}
    • {{formatGlossary ../dictionary .}}
    • {{/each}}
    + {{~/if~}} + {{~set "previousDictionary" dictionary~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // block helper update: set and get + { + oldVersion: 20, + newVersion: 21, + old: ` +{{#*inline "pitch-accent-item-disambiguation"}} + {{~#scope~}} + {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} + {{~#if (op ">" (property (get "exclusive") "length") 0)~}} + {{~#set "separator" ""~}}{{/set~}} + ({{#each (get "exclusive")~}} + {{~#get "separator"}}{{/get~}}{{{.}}} + {{~/each}} only) + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "stroke-count"}} + {{~#scope~}} + {{~#set "found" false}}{{/set~}} + {{~#each definition.stats.misc~}} + {{~#if (op "===" name "strokes")~}} + {{~#set "found" true}}{{/set~}} + Stroke count: {{value}} + {{~/if~}} + {{~/each~}} + {{~#if (op "!" (get "found"))~}} + Stroke count: Unknown + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "part-of-speech"}} + {{~#scope~}} + {{~#if (op "!==" definition.type "kanji")~}} + {{~#set "first" true}}{{/set~}} + {{~#each definition.expressions~}} + {{~#each wordClasses~}} + {{~#unless (get (concat "used_" .))~}} + {{~> part-of-speech-pretty . ~}} + {{~#unless (get "first")}}, {{/unless~}} + {{~#set (concat "used_" .) true~}}{{~/set~}} + {{~#set "first" false~}}{{~/set~}} + {{~/unless~}} + {{~/each~}} + {{~/each~}} + {{~#if (get "first")~}}Unknown{{~/if~}} + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "pitch-accent-item-disambiguation"}} + {{~#scope~}} + {{~set "exclusive" (spread exclusiveExpressions exclusiveReadings)~}} + {{~#if (op ">" (property (get "exclusive") "length") 0)~}} + {{~set "separator" ""~}} + ({{#each (get "exclusive")~}} + {{~get "separator"~}}{{{.}}} + {{~/each}} only) + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "stroke-count"}} + {{~#scope~}} + {{~set "found" false~}} + {{~#each definition.stats.misc~}} + {{~#if (op "===" name "strokes")~}} + {{~set "found" true~}} + Stroke count: {{value}} + {{~/if~}} + {{~/each~}} + {{~#if (op "!" (get "found"))~}} + Stroke count: Unknown + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "part-of-speech"}} + {{~#scope~}} + {{~#if (op "!==" definition.type "kanji")~}} + {{~set "first" true~}} + {{~#each definition.expressions~}} + {{~#each wordClasses~}} + {{~#unless (get (concat "used_" .))~}} + {{~> part-of-speech-pretty . ~}} + {{~#unless (get "first")}}, {{/unless~}} + {{~set (concat "used_" .) true~}} + {{~set "first" false~}} + {{~/unless~}} + {{~/each~}} + {{~/each~}} + {{~#if (get "first")~}}Unknown{{~/if~}} + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // block helper update: hasMedia and getMedia + { + oldVersion: 20, + newVersion: 21, + old: ` +{{#*inline "audio"}} + {{~#if (hasMedia "audio")~}} + [sound:{{#getMedia "audio"}}{{/getMedia}}] + {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} + {{~#if (hasMedia "screenshot")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-image"}} + {{~#if (hasMedia "clipboardImage")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} + {{~#if (hasMedia "clipboardText")}}{{#getMedia "clipboardText"}}{{/getMedia}}{{/if~}} +{{/inline}} + +{{#*inline "selection-text"}} + {{~#if (hasMedia "selectionText")}}{{#getMedia "selectionText"}}{{/getMedia}}{{/if~}} +{{/inline}} + +{{#*inline "sentence-furigana"}} + {{~#if definition.cloze~}} + {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}} + {{#getMedia "textFurigana" definition.cloze.sentence escape=false}}{{/getMedia}} + {{~else~}} + {{definition.cloze.sentence}} + {{~/if~}} + {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "audio"}} + {{~#if (hasMedia "audio")~}} + [sound:{{getMedia "audio"}}] + {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} + {{~#if (hasMedia "screenshot")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-image"}} + {{~#if (hasMedia "clipboardImage")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} + {{~#if (hasMedia "clipboardText")}}{{getMedia "clipboardText"}}{{/if~}} +{{/inline}} + +{{#*inline "selection-text"}} + {{~#if (hasMedia "selectionText")}}{{getMedia "selectionText"}}{{/if~}} +{{/inline}} + +{{#*inline "sentence-furigana"}} + {{~#if definition.cloze~}} + {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}} + {{getMedia "textFurigana" definition.cloze.sentence escape=false}} + {{~else~}} + {{definition.cloze.sentence}} + {{~/if~}} + {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // block helper update: pronunciation + { + oldVersion: 20, + newVersion: 21, + old: ` +{{#*inline "pitch-accent-item"}} + {{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "pitch-accent-item"}} + {{~pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}} +{{/inline}} + {{~> (lookup . "marker") ~}}`.trimStart() } ];