aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md23
-rw-r--r--docs/templates.md185
-rw-r--r--ext/css/settings.css7
-rw-r--r--ext/data/templates/anki-field-templates-upgrade-v21.handlebars161
-rw-r--r--ext/data/templates/default-anki-field-templates.handlebars54
-rw-r--r--ext/js/background/backend.js20
-rw-r--r--ext/js/data/options-util.js35
-rw-r--r--ext/js/pages/welcome-main.js7
-rw-r--r--ext/js/templates/sandbox/anki-template-renderer.js54
-rw-r--r--ext/welcome.html27
-rw-r--r--test/test-options-util.js369
11 files changed, 737 insertions, 205 deletions
diff --git a/README.md b/README.md
index 20306688..1f25c057 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 778b8e2d..030ca3d2 100644
--- a/docs/templates.md
+++ b/docs/templates.md
@@ -14,7 +14,7 @@ This function can be helpful for debugging values when creating templates.
<details>
<summary>Syntax:</summary>
- <code>{{#dumpObject}}<i>&lt;object&gt;</i>{{/dumpObject}}</code>
+ <code>{{dumpObject <i>object</i>}}</code>
* _`object`_ <br>
The object to convert.
@@ -23,7 +23,7 @@ This function can be helpful for debugging values when creating templates.
<summary>Example:</summary>
```handlebars
- <pre>{{#dumpObject}}{{.}}{{/dumpObject}}</pre>
+ <pre>{{dumpObject .}}</pre>
```
Output:
@@ -49,8 +49,8 @@ Converts a definition or expression/reading pair to its furigana representation.
<details>
<summary>Syntax:</summary>
- <code>{{#furigana}}<i>&lt;definition&gt;</i>{{/furigana}}</code><br>
- <code>{{#furigana <i>expression</i> <i>reading</i>}}{{/furigana}}</code><br>
+ <code>{{furigana <i>definition</i>}}</code><br>
+ <code>{{furigana <i>expression</i> <i>reading</i>}}</code><br>
* _`definition`_ <br>
The definition to convert.
@@ -63,8 +63,8 @@ Converts a definition or expression/reading pair to its furigana representation.
<summary>Example:</summary>
```handlebars
- {{#furigana}}{{.}}{{/furigana}}
- {{#furigana "読む" "よむ"}}{{/furigana}}
+ {{furigana .}}
+ {{furigana "読む" "よむ"}}
```
Output:
@@ -84,8 +84,8 @@ Converts a definition or expression/reading pair to its simplified furigana repr
<details>
<summary>Syntax:</summary>
- <code>{{#furiganaPlain}}<i>&lt;definition&gt;</i>{{/furigana}}</code>
- <code>{{#furiganaPlain <i>expression</i> <i>reading</i>}}{{/furiganaPlain}}</code><br>
+ <code>{{furiganaPlain <i>definition</i>}}</code>
+ <code>{{furiganaPlain <i>expression</i> <i>reading</i>}}</code><br>
* _`definition`_ <br>
The definition to convert.
@@ -98,8 +98,8 @@ Converts a definition or expression/reading pair to its simplified furigana repr
<summary>Example:</summary>
```handlebars
- {{~#furiganaPlain~}}{{.}}{{~/furiganaPlain~}}
- {{#furiganaPlain "読む" "よむ"}}{{/furiganaPlain}}
+ {{~furiganaPlain .~}}
+ {{furiganaPlain "読む" "よむ"}}
```
Output:
@@ -122,11 +122,11 @@ Replaces newline characters with a forced HTML line break `<br>`.
<summary>Example:</summary>
```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
<summary>Syntax:</summary>
<code>{{#regexReplace <i>regex</i> <i>replacement</i> <i>[flags]</i>}}<i>text-to-modify</i>{{/regexReplace}}</code><br>
- <code>{{#regexReplace <i>regex</i> <i>replacement</i> <i>[flags]</i> <i>[text-to-modify]...</i>}}{{/regexReplace}}</code><br>
+ <code>{{regexReplace <i>regex</i> <i>replacement</i> <i>[flags]</i> <i>[text-to-modify]...</i>}}</code><br>
* _`regex`_ <br>
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
<summary>Syntax:</summary>
<code>{{#regexMatch <i>regex</i> <i>[flags]</i>}}<i>text-to-modify</i>{{/regexMatch}}</code><br>
- <code>{{#regexMatch <i>regex</i> <i>[flags]</i> <i>[text-to-modify]...</i>}}{{/regexMatch}}</code><br>
+ <code>{{regexMatch <i>regex</i> <i>[flags]</i> <i>[text-to-modify]...</i>}}</code><br>
* _`regex`_ <br>
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
<details>
<summary>Syntax:</summary>
- <code>{{#mergeTags <i>definition</i> <i>isGroupMode</i> <i>isMergeMode</i>}}{{/mergeTags}}</code>
+ <code>{{mergeTags <i>definition</i> <i>isGroupMode</i> <i>isMergeMode</i>}}</code>
* _`definition`_ <br>
The root definition object.
@@ -225,7 +225,7 @@ Creates a set of all unique tags for the definition and returns a text represent
<summary>Example:</summary>
```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
</details>
-### `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.
<details>
@@ -279,7 +279,7 @@ This allows it to be used similar to an [`Array.concat`](https://developer.mozil
<details>
<summary>Syntax:</summary>
- <code>{{#spread <i>iterable1</i> <i>iterable2</i> <i>...</i> <i>iterableN</i>}}{{/spread}}</code>
+ <code>{{spread <i>iterable1</i> <i>iterable2</i> <i>...</i> <i>iterableN</i>}}</code>
* _`iterableN`_ <br>
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.
<details>
<summary>Syntax:</summary>
- <code>{{#op <i>operator</i> <i>operand1</i> <i>[operand2]</i> <i>[operand3]</i>}}{{/op}}</code>
+ <code>{{op <i>operator</i> <i>operand1</i> <i>[operand2]</i> <i>[operand3]</i>}}</code>
* _`operator`_ <br>
One of the unary, binary, or ternary operators.
@@ -329,9 +329,9 @@ If an unknown operator is specified, the `undefined` value is returned.
<summary>Example:</summary>
```handlebars
- {{#if (op "===" value1 value2)}}Values are equal{{/op~}}<br>
+ {{#if (op "===" value1 value2)}}Values are equal{{/if~}}<br>
{{~#op "-" value1}}{{/op~}}<br>
- {{~#op "?:" value1 "a" "b"}}{{/op}}
+ {{~op "?:" value1 "a" "b"}}
```
Output:
@@ -351,7 +351,7 @@ Gets a value from the custom state stack.
<details>
<summary>Syntax:</summary>
- <code>{{#get <i>name</i>}}{{/get}}</code>
+ <code>{{get <i>name</i>}}</code>
* _`name`_ <br>
The name of the variable to get.
@@ -360,7 +360,7 @@ Gets a value from the custom state stack.
<summary>Example:</summary>
```handlebars
- {{#get "some-text"}}{{/get}}
+ {{get "some-text"}}
```
Output:
@@ -378,7 +378,7 @@ Assigns a value to the custom state stack.
<summary>Syntax:</summary>
<code>{{#set <i>name</i>}}<i>value</i>{{/get}}</code><br>
- <code>{{#set <i>name</i> <i>value</i>}}{{/get}}</code><br>
+ <code>{{set <i>name</i> <i>value</i>}}</code><br>
* _`name`_ <br>
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.
</details>
-### `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
<summary>Example:</summary>
```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.
<details>
<summary>Syntax:</summary>
- <code>{{#property <i>object</i> <i>property1</i> <i>property2</i> <i>...</i> <i>propertyN</i>}}{{/property}}</code>
+ <code>{{property <i>object</i> <i>property1</i> <i>property2</i> <i>...</i> <i>propertyN</i>}}</code>
* _`object`_ <br>
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
<details>
<summary>Syntax:</summary>
- <code>{{#isMoraPitchHigh <i>index</i> <i>position</i>}}{{/isMoraPitchHigh}}</code>
+ <code>{{isMoraPitchHigh <i>index</i> <i>position</i>}}</code>
</details>
<details>
<summary>Example:</summary>
@@ -517,7 +517,7 @@ Returns an array of the mora for a kana string.
<details>
<summary>Syntax:</summary>
- <code>{{#getKanaMorae <i>kana-string</i>}}{{/getKanaMorae}}</code>
+ <code>{{getKanaMorae <i>kana-string</i>}}</code>
</details>
<details>
<summary>Example:</summary>
@@ -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.
<details>
<summary>Syntax:</summary>
- <code>{{#typeof <i>value</i>}}{{/typeof}}</code><br>
+ <code>{{typeof <i>value</i>}}</code><br>
<code>{{#typeof}}<i>value</i>{{/typeof}}</code><br>
* _`value`_ <br>
@@ -553,8 +553,8 @@ Returns the type of a value.
<summary>Example:</summary>
```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
<details>
<summary>Syntax:</summary>
- <code>{{#join <i>separator</i> <i>value1</i> <i>value2</i> <i>valueN</i>...}}{{/join}}</code><br>
+ <code>{{join <i>separator</i> <i>value1</i> <i>value2</i> <i>valueN</i>...}}</code><br>
* _`separator`_ <br>
The separator string to use between values.
@@ -585,8 +585,8 @@ Joins the arguments to a single string with a separator, flattening any argument
<summary>Example:</summary>
```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
<details>
<summary>Syntax:</summary>
- <code>{{#concat <i>value1</i> <i>value1</i> <i>valueN</i>...}}{{/concat}}</code><br>
+ <code>{{concat <i>value1</i> <i>value1</i> <i>valueN</i>...}}</code><br>
* _`valueN`_ <br>
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
<summary>Example:</summary>
```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
<details>
<summary>Syntax:</summary>
- <code>{{#pitchCategories @root}}{{/pitchCategories}}</code><br>
+ <code>{{pitchCategories @root}}</code><br>
* _`@root`_ <br>
The argument passed should always be the root data object.
@@ -657,7 +657,7 @@ structured-content generation.
<details>
<summary>Syntax:</summary>
- <code>{{#formatGlossary <i>dictionary</i>}}{{{definitionEntry}}}{{/pitchCategories}}</code><br>
+ <code>{{formatGlossary <i>dictionary</i> <i>definitionEntry</i>}}</code><br>
* _`dictionary`_ <br>
The dictionary that the glossary entry belongs to.
@@ -668,7 +668,7 @@ structured-content generation.
<summary>Example:</summary>
```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
<details>
<summary>Syntax:</summary>
- <code>{{#hasMedia <i>type</i> <i>args</i>...}}{{/hasMedia}}</code><br>
- <code>{{#getMedia <i>type</i> <i>args</i>... <i>[escape=true|false]</i>}}{{/getMedia}}</code><br>
+ <code>{{hasMedia <i>type</i> <i>args</i>...}}</code><br>
+ <code>{{getMedia <i>type</i> <i>args</i>... <i>[escape=true|false]</i>}}</code><br>
* _`type`_ <br>
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
<summary>Examples:</summary>
```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.
<details>
<summary>Syntax:</summary>
- <code>{{#pronunciation <i>format=string</i> <i>reading=string</i> <i>downstepPosition=integer</i> <i>[nasalPositions=array]</i> <i>[devoicePositions=array]</i>}}{{/pronunciation}}</code><br>
+ <code>{{pronunciation <i>format=string</i> <i>reading=string</i> <i>downstepPosition=integer</i> <i>[nasalPositions=array]</i> <i>[devoicePositions=array]</i>}}</code><br>
* _`format`_ <br>
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.
<summary>Example:</summary>
```handlebars
- {{~#pronunciation format='text' reading='よむ' downstepPosition=1~}}{{~/pronunciation~}}
+ {{~pronunciation format='text' reading='よむ' downstepPosition=1~}}
```
</details>
@@ -786,7 +786,7 @@ Converts katakana text to hiragana.
<details>
<summary>Syntax:</summary>
- <code>{{#hiragana <i>value</i> <i>[keepProlongedSoundMarks=true|false]</i>}}{{/hiragana}}</code><br>
+ <code>{{hiragana <i>value</i> <i>[keepProlongedSoundMarks=true|false]</i>}}</code><br>
<code>{{#hiragana <i>[keepProlongedSoundMarks=true|false]</i>}}<i>value</i>{{/hiragana}}</code><br>
* _`value`_ <br>
@@ -799,7 +799,7 @@ Converts katakana text to hiragana.
<summary>Example:</summary>
```handlebars
- {{#hiragana "よみちゃん ヨミちゃん ヨミチャン"}}{{/hiragana}}
+ {{hiragana "よみちゃん ヨミちゃん ヨミチャン"}}
{{#hiragana}}よみちゃん ヨミちゃん ヨミチャン{{/hiragana}}
{{#hiragana}}ローマ字{{/hiragana}}
{{#hiragana keepProlongedSoundMarks=true}}ローマ字{{/hiragana}}
@@ -822,7 +822,7 @@ Converts hiragana text to katakana.
<details>
<summary>Syntax:</summary>
- <code>{{#katakana <i>text</i>}}{{/katakana}}</code><br>
+ <code>{{katakana <i>text</i>}}</code><br>
<code>{{#katakana}}<i>text</i>{{/katakana}}</code><br>
* _`text`_ <br>
@@ -832,7 +832,7 @@ Converts hiragana text to katakana.
<summary>Example:</summary>
```handlebars
- {{#katakana "よみちゃん ヨミちゃん ヨミチャン"}}{{/katakana}}
+ {{katakana "よみちゃん ヨミちゃん ヨミチャン"}}
{{#katakana}}よみちゃん ヨミちゃん ヨミチャン{{/katakana}}
```
@@ -842,60 +842,3 @@ Converts hiragana text to katakana.
ヨミチャン ヨミチャン ヨミチャン
```
</details>
-
-
-## 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.
-
-<details>
- <summary>Syntax:</summary>
-
- <code>{{#kanjiLinks}}<i>text</i>{{/kanjiLinks}}</code>
-</details>
-<details>
- <summary>Example:</summary>
-
- ```handlebars
- {{#kanjiLinks}}読む{{/kanjiLinks}}
- ```
-
- Output:
- ```html
- <a href="#" class="kanji-link">読</a>む
- ```
-
- Preview:
- <pre><a href="#" class="kanji-link">読</a>む</pre>
-</details>
-
-
-### `sanitizeCssClass`
-
-Sanitizes text so it can be used as a CSS class name.
-
-<details>
- <summary>Syntax:</summary>
-
- <code>{{#sanitizeCssClass}}<i>text</i>{{/sanitizeCssClass}}</code>
-</details>
-<details>
- <summary>Example:</summary>
-
- ```handlebars
- {{#sanitizeCssClass}}some text with many types of characters !@#$%^ 読む{{/sanitizeCssClass}}
- ```
-
- Output:
- ```html
- some_text_with_many_types_of_characters________読む
- ```
-</details>
diff --git a/ext/css/settings.css b/ext/css/settings.css
index eaebc3af..a2618d88 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 00000000..33c4dc6c
--- /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 31d5d13f..d94f6d70 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}}<i>({{/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}}<i>({{/if~}}
{{dictionary}}
- {{~#set "any" true}}{{/set~}}
+ {{~set "any" true~}}
{{~/if~}}
{{~/unless~}}
{{~#if (get "any")}})</i> {{/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~}}
- <ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul>
+ <ul>{{#each glossary}}<li>{{formatGlossary ../dictionary .}}</li>{{/each}}</ul>
{{~/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~}}
- <span class="expression-{{termFrequency}}">{{~#furigana}}{{{.}}}{{/furigana~}}</span>
+ <span class="expression-{{termFrequency}}">{{~furigana .~}}</span>
{{~#unless @last}}、{{/unless~}}
{{~/each~}}
{{~else~}}
- {{#furigana}}{{{definition}}}{{/furigana}}
+ {{furigana definition}}
{{~/if~}}
{{/inline}}
{{#*inline "furigana-plain"}}
{{~#if merge~}}
{{~#each definition.expressions~}}
- <span class="expression-{{termFrequency}}">{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}</span>
+ <span class="expression-{{termFrequency}}">{{~furiganaPlain .~}}</span>
{{~#unless @last}}、{{/unless~}}
{{~/each~}}
{{~else~}}
- {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}}
+ {{furiganaPlain definition}}
{{~/if~}}
{{/inline}}
@@ -174,7 +174,7 @@
{{#*inline "screenshot"}}
{{~#if (hasMedia "screenshot")~}}
- <img src="{{#getMedia "screenshot"}}{{/getMedia}}" />
+ <img src="{{getMedia "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" ""~}}
<em>({{#each (get "exclusive")~}}
- {{~#get "separator"}}{{/get~}}{{{.}}}
+ {{~get "separator"~}}{{{.}}}
{{~/each}} only) </em>
{{~/if~}}
{{~/scope~}}
@@ -231,12 +231,12 @@
{{#*inline "clipboard-image"}}
{{~#if (hasMedia "clipboardImage")~}}
- <img src="{{#getMedia "clipboardImage"}}{{/getMedia}}" />
+ <img src="{{getMedia "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 @@
<li>
{{~#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 308ae4d5..8e8e6945 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 2674701f..1f2ffb05 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 521ce2c2..8039dae5 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 789f0942..766c7798 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 += `<ruby>${text}<rt>${reading2}</rt></ruby>`;
+ const safeText = this._escape(text);
+ const safeReading = this._escape(reading2);
+ if (safeReading.length > 0) {
+ result += `<ruby>${safeText}<rt>${safeReading}</rt></ruby>`;
} 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 += `<a href="#" class="kanji-link">${c}</a>`;
- } else {
- result += c;
- }
- }
-
- return result;
- }
-
_stringToMultiLineHtml(string) {
return string.split('\n').join('<br>');
}
@@ -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 14e98367..56167866 100644
--- a/ext/welcome.html
+++ b/ext/welcome.html
@@ -25,6 +25,19 @@
<h1>Welcome to Yomitan!</h1>
+ <!-- Notifications -->
+ <div class="settings-group settings-group-top-margin warn-custom-templates-notification">
+ <div class="settings-item">
+ <div class="settings-item-inner settings-item-inner-wrappable"><div class="settings-item-left"><div class="settings-item-label">
+ <p>
+ There are custom Anki templates in your settings. Note that <a href="https://github.com/themoeway/yomitan#custom-templates" target="_blank" rel="noopener noreferrer">some syntax has changed from previous versions of Yomitan.</a>
+ Please ensure that your custom templates are using the updated syntax.
+ </p>
+ </div></div></div>
+ </div>
+ </div>
+
+ <!-- Content -->
<h2>Here are some basics to get started</h2>
<div class="settings-group">
<div class="settings-item">
@@ -49,10 +62,9 @@
<div class="settings-item">
<div class="settings-item-inner"><div class="settings-item-left"><div class="settings-item-label">
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 <a href="https://github.com/themoeway/yomitan#dictionaries" target="_blank" rel="noopener noreferrer">Yomitan homepage</a>,
- allowing you to choose the dictionaries most relevant for you.
+ Several downloadable dictionaries can be found on the <a href="https://github.com/themoeway/yomitan#dictionaries" target="_blank" rel="noopener noreferrer">Yomitan homepage</a>.
Dictionaries can be configured using the button below,
- or later from the the <a href="/settings.html" rel="noopener">Settings</a> page.
+ or later from the <a href="/settings.html" rel="noopener">Settings</a> page.
</div></div></div>
<div class="settings-item-children settings-item-children-group">
<div class="settings-item settings-item-button" data-modal-action="show,dictionaries"><div class="settings-item-inner">
@@ -67,12 +79,17 @@
</div>
<div class="settings-item">
<div class="settings-item-inner"><div class="settings-item-left"><div class="settings-item-label">
- You can also import an exported collection of dictionaries to migrate from a different device or browser from the <a href="/settings.html#!backup">Backup section of the Settings</a> page.
+ You can also import an exported collection of dictionaries from the <a href="/settings.html#!backup">Backup section of the Settings</a> page.
<br><br>
- 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 <a href="https://github.com/themoeway/yomitan#migrating-from-yomichan" target="_blank" rel="noopener noreferrer">Yomitan's README</a> for that.
+
+ <br><br>
+
+ If you are using or planning to use custom templates for Anki note creation, note that <a href="https://github.com/themoeway/yomitan#custom-templates" target="_blank" rel="noopener noreferrer">some syntax has changed from Yomichan and Yomibaba.</a>
+ Please ensure that your custom templates are using the updated syntax.
</div></div></div>
</div>
<div class="settings-item">
diff --git a/test/test-options-util.js b/test/test-options-util.js
index 2be6b2f7..d94028c0 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 = '';
@@ -1215,6 +1216,370 @@ async function testFieldTemplatesUpdate(extDir) {
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
+ },
+ // block helper update: furigana and furiganaPlain
+ {
+ oldVersion: 20,
+ newVersion: 21,
+ old: `
+{{#*inline "furigana"}}
+ {{~#if merge~}}
+ {{~#each definition.expressions~}}
+ <span class="expression-{{termFrequency}}">{{~#furigana}}{{{.}}}{{/furigana~}}</span>
+ {{~#unless @last}}、{{/unless~}}
+ {{~/each~}}
+ {{~else~}}
+ {{#furigana}}{{{definition}}}{{/furigana}}
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "furigana-plain"}}
+ {{~#if merge~}}
+ {{~#each definition.expressions~}}
+ <span class="expression-{{termFrequency}}">{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}</span>
+ {{~#unless @last}}、{{/unless~}}
+ {{~/each~}}
+ {{~else~}}
+ {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}}
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "frequencies"}}
+ {{~#if (op ">" definition.frequencies.length 0)~}}
+ <ul style="text-align: left;">
+ {{~#each definition.frequencies~}}
+ <li>
+ {{~#if (op "!==" ../definition.type "kanji")~}}
+ {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}(
+ {{~#furigana expression reading~}}{{~/furigana~}}
+ ) {{/if~}}
+ {{~/if~}}
+ {{~dictionary}}: {{frequency~}}
+ </li>
+ {{~/each~}}
+ </ul>
+ {{~/if~}}
+{{/inline}}
+
+{{~> (lookup . "marker") ~}}`.trimStart(),
+
+ expected: `
+{{#*inline "furigana"}}
+ {{~#if merge~}}
+ {{~#each definition.expressions~}}
+ <span class="expression-{{termFrequency}}">{{~furigana .~}}</span>
+ {{~#unless @last}}、{{/unless~}}
+ {{~/each~}}
+ {{~else~}}
+ {{furigana definition}}
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "furigana-plain"}}
+ {{~#if merge~}}
+ {{~#each definition.expressions~}}
+ <span class="expression-{{termFrequency}}">{{~furiganaPlain .~}}</span>
+ {{~#unless @last}}、{{/unless~}}
+ {{~/each~}}
+ {{~else~}}
+ {{furiganaPlain definition}}
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "frequencies"}}
+ {{~#if (op ">" definition.frequencies.length 0)~}}
+ <ul style="text-align: left;">
+ {{~#each definition.frequencies~}}
+ <li>
+ {{~#if (op "!==" ../definition.type "kanji")~}}
+ {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}(
+ {{~furigana expression reading~}}
+ ) {{/if~}}
+ {{~/if~}}
+ {{~dictionary}}: {{frequency~}}
+ </li>
+ {{~/each~}}
+ </ul>
+ {{~/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}}<i>({{/if~}}
+ {{name}}
+ {{~#set "any" true}}{{/set~}}
+ {{~/if~}}
+ {{~/each~}}
+ {{~#unless noDictionaryTag~}}
+ {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}}
+ {{~#if (get "any")}}, {{else}}<i>({{/if~}}
+ {{dictionary}}
+ {{~#set "any" true}}{{/set~}}
+ {{~/if~}}
+ {{~/unless~}}
+ {{~#if (get "any")}})</i> {{/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~}}
+ <ul>{{#each glossary}}<li>{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}</li>{{/each}}</ul>
+ {{~/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}}<i>({{/if~}}
+ {{name}}
+ {{~set "any" true~}}
+ {{~/if~}}
+ {{~/each~}}
+ {{~#unless noDictionaryTag~}}
+ {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}}
+ {{~#if (get "any")}}, {{else}}<i>({{/if~}}
+ {{dictionary}}
+ {{~set "any" true~}}
+ {{~/if~}}
+ {{~/unless~}}
+ {{~#if (get "any")}})</i> {{/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~}}
+ <ul>{{#each glossary}}<li>{{formatGlossary ../dictionary .}}</li>{{/each}}</ul>
+ {{~/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~}}
+ <em>({{#each (get "exclusive")~}}
+ {{~#get "separator"}}{{/get~}}{{{.}}}
+ {{~/each}} only) </em>
+ {{~/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" ""~}}
+ <em>({{#each (get "exclusive")~}}
+ {{~get "separator"~}}{{{.}}}
+ {{~/each}} only) </em>
+ {{~/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")~}}
+ <img src="{{#getMedia "screenshot"}}{{/getMedia}}" />
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "clipboard-image"}}
+ {{~#if (hasMedia "clipboardImage")~}}
+ <img src="{{#getMedia "clipboardImage"}}{{/getMedia}}" />
+ {{~/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")~}}
+ <img src="{{getMedia "screenshot"}}" />
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "clipboard-image"}}
+ {{~#if (hasMedia "clipboardImage")~}}
+ <img src="{{getMedia "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()
}
];