Pitch accent Anki field templates (#701)

* Template helper updates

* Add pitch data to exported field formatting data

* Reuse note data

* Add no-op

* Set up pitch accent templates

* Refactor version update functions

* Implement upgrade process for new Anki templates

* Consistency

* Update README and anki.js to have matching markers
This commit is contained in:
toasted-nutbread 2020-08-01 16:23:33 -04:00 committed by GitHub
parent 1e839cd230
commit 838fd211c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 361 additions and 40 deletions

View File

@ -163,6 +163,10 @@ Flashcard fields can be configured with the following steps:
`{furigana}` | Term expressed as kanji with furigana displayed above it (e.g. <ruby>日本語<rt>にほんご</rt></ruby>).
`{furigana-plain}` | Term expressed as kanji with furigana displayed next to it in brackets (e.g. 日本語[にほんご]).
`{glossary}` | List of definitions for the term (output format depends on whether running in *grouped* mode).
`{glossary-brief}` | List of definitions for the term in a more compact format.
`{pitch-accents}` | List of pitch accent downstep notations for the term.
`{pitch-accent-graphs}` | List of pitch accent graphs for the term.
`{pitch-accent-positions}` | List of accent downstep positions for the term as a number.
`{reading}` | Kana reading for the term (empty for terms where the expression is the reading).
`{screenshot}` | Screenshot of the web page taken at the time the term was added.
`{sentence}` | Sentence, quote, or phrase that the term appears in from the source content.

View File

@ -46,6 +46,7 @@
<script src="/bg/js/translator.js"></script>
<script src="/bg/js/util.js"></script>
<script src="/mixed/js/audio-system.js"></script>
<script src="/mixed/js/dictionary-data-util.js"></script>
<script src="/mixed/js/object-property-accessor.js"></script>
<script src="/bg/js/background-main.js"></script>

View File

@ -0,0 +1,109 @@
{{! Pitch Accents }}
{{#*inline "pitch-accent-item-downstep-notation"}}
{{~#scope~}}
<span>
{{~#set "style1a"~}}display:inline-block;position:relative;{{~/set~}}
{{~#set "style1b"~}}padding-right:0.1em;margin-right:0.1em;{{~/set~}}
{{~#set "style2a"~}}display:block;user-select:none;pointer-events:none;position:absolute;top:0.1em;left:0;right:0;height:0;border-top:0.1em solid;{{~/set~}}
{{~#set "style2b"~}}right:-0.1em;height:0.4em;border-right:0.1em solid;{{~/set~}}
{{~#each (getKanaMorae reading)~}}
{{~#set "style1"}}{{#get "style1a"}}{{/get}}{{/set~}}
{{~#set "style2"}}{{/set~}}
{{~#if (isMoraPitchHigh @index ../position)}}
{{~#set "style2"}}{{#get "style2a"}}{{/get}}{{/set~}}
{{~#if (op "!" (isMoraPitchHigh (op "+" @index 1) ../position))~}}
{{~#set "style1" (op "+" (get "style1") (get "style1b"))}}{{/set~}}
{{~#set "style2" (op "+" (get "style2") (get "style2b"))}}{{/set~}}
{{~/if~}}
{{~/if~}}
<span style="{{#get "style1"}}{{/get}}">{{{.}}}<span style="{{#get "style2"}}{{/get}}"></span></span>
{{~/each~}}
</span>
{{~/scope~}}
{{/inline}}
{{#*inline "pitch-accent-item-graph-position-x"}}{{#op "+" 25 (op "*" index 50)}}{{/op}}{{/inline}}
{{#*inline "pitch-accent-item-graph-position-y"}}{{#op "+" 25 (op "?:" (isMoraPitchHigh index position) 0 50)}}{{/op}}{{/inline}}
{{#*inline "pitch-accent-item-graph-position"}}{{> pitch-accent-item-graph-position-x index=index position=position}} {{> pitch-accent-item-graph-position-y index=index position=position}}{{/inline}}
{{#*inline "pitch-accent-item-graph"}}
{{~#scope~}}
{{~#set "morae" (getKanaMorae reading)}}{{/set~}}
{{~#set "morae-count" (property (get "morae") "length")}}{{/set~}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {{#op "+" 50 (op "*" 50 (get "morae-count"))}}{{/op}} 100" style="display:inline-block;height:2em;">
<defs>
<g id="term-pitch-accent-graph-dot"><circle cx="0" cy="0" r="15" style="fill:#000;stroke:#000;stroke-width:5;" /></g>
<g id="term-pitch-accent-graph-dot-downstep"><circle cx="0" cy="0" r="15" style="fill:none;stroke:#000;stroke-width:5;" /><circle cx="0" cy="0" r="5" style="fill:none;stroke:#000;stroke-width:5;" /></g>
<g id="term-pitch-accent-graph-triangle"><path d="M0 13 L15 -13 L-15 -13 Z" style="fill:none;stroke:#000;stroke-width:5;" /></g>
</defs>
<path style="fill:none;stroke:#000;stroke-width:5;" d="
{{~#set "cmd" "M"}}{{/set~}}
{{~#each (get "morae")~}}
{{~#get "cmd"}}{{/get~}}
{{~> pitch-accent-item-graph-position index=@index position=../position~}}
{{~#set "cmd" "L"}}{{/set~}}
{{~/each~}}
"></path>
<path style="fill:none;stroke:#000;stroke-width:5;stroke-dasharray:5 5;" d="M{{> pitch-accent-item-graph-position index=(op "-" (get "morae-count") 1) position=position}} L{{> pitch-accent-item-graph-position index=(get "morae-count") position=position}}"></path>
{{#each (get "morae")}}
<use href="{{#if (op "&&" (isMoraPitchHigh @index ../position) (op "!" (isMoraPitchHigh (op "+" @index 1) ../position)))}}#term-pitch-accent-graph-dot-downstep{{else}}#term-pitch-accent-graph-dot{{/if}}" x="{{> pitch-accent-item-graph-position-x index=@index position=../position}}" y="{{> pitch-accent-item-graph-position-y index=@index position=../position}}"></use>
{{/each}}
<use href="#term-pitch-accent-graph-triangle" x="{{> pitch-accent-item-graph-position-x index=(get "morae-count") position=position}}" y="{{> pitch-accent-item-graph-position-y index=(get "morae-count") position=position}}"></use>
</svg>
{{~/scope~}}
{{/inline}}
{{#*inline "pitch-accent-item-position"~}}
<span>[{{position}}]</span>
{{~/inline}}
{{#*inline "pitch-accent-item"}}
{{~#if (op "==" format "downstep-notation")~}}
{{~> pitch-accent-item-downstep-notation~}}
{{~else if (op "==" format "graph")~}}
{{~> pitch-accent-item-graph~}}
{{~else if (op "==" format "position")~}}
{{~> pitch-accent-item-position~}}
{{~/if~}}
{{/inline}}
{{#*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 "pitch-accent-list"}}
{{~#if (op ">" pitchCount 0)~}}
{{~#if (op ">" pitchCount 1)~}}<ol>{{~/if~}}
{{~#each pitches~}}
{{~#each pitches~}}
{{~#if (op ">" ../../pitchCount 1)~}}<li>{{~/if~}}
{{~> pitch-accent-item-disambiguation~}}
{{~> pitch-accent-item format=../../format~}}
{{~#if (op ">" ../../pitchCount 1)~}}</li>{{~/if~}}
{{~/each~}}
{{~/each~}}
{{~#if (op ">" pitchCount 1)~}}</ol>{{~/if~}}
{{~else~}}
No pitch accent data
{{~/if~}}
{{/inline}}
{{#*inline "pitch-accents"}}
{{~> pitch-accent-list format='downstep-notation'~}}
{{/inline}}
{{#*inline "pitch-accent-graphs"}}
{{~> pitch-accent-list format='graph'~}}
{{/inline}}
{{#*inline "pitch-accent-positions"}}
{{~> pitch-accent-list format='position'~}}
{{/inline}}
{{! End Pitch Accents }}

View File

@ -166,4 +166,114 @@
{{~context.document.title~}}
{{/inline}}
{{! Pitch Accents }}
{{#*inline "pitch-accent-item-downstep-notation"}}
{{~#scope~}}
<span>
{{~#set "style1a"~}}display:inline-block;position:relative;{{~/set~}}
{{~#set "style1b"~}}padding-right:0.1em;margin-right:0.1em;{{~/set~}}
{{~#set "style2a"~}}display:block;user-select:none;pointer-events:none;position:absolute;top:0.1em;left:0;right:0;height:0;border-top:0.1em solid;{{~/set~}}
{{~#set "style2b"~}}right:-0.1em;height:0.4em;border-right:0.1em solid;{{~/set~}}
{{~#each (getKanaMorae reading)~}}
{{~#set "style1"}}{{#get "style1a"}}{{/get}}{{/set~}}
{{~#set "style2"}}{{/set~}}
{{~#if (isMoraPitchHigh @index ../position)}}
{{~#set "style2"}}{{#get "style2a"}}{{/get}}{{/set~}}
{{~#if (op "!" (isMoraPitchHigh (op "+" @index 1) ../position))~}}
{{~#set "style1" (op "+" (get "style1") (get "style1b"))}}{{/set~}}
{{~#set "style2" (op "+" (get "style2") (get "style2b"))}}{{/set~}}
{{~/if~}}
{{~/if~}}
<span style="{{#get "style1"}}{{/get}}">{{{.}}}<span style="{{#get "style2"}}{{/get}}"></span></span>
{{~/each~}}
</span>
{{~/scope~}}
{{/inline}}
{{#*inline "pitch-accent-item-graph-position-x"}}{{#op "+" 25 (op "*" index 50)}}{{/op}}{{/inline}}
{{#*inline "pitch-accent-item-graph-position-y"}}{{#op "+" 25 (op "?:" (isMoraPitchHigh index position) 0 50)}}{{/op}}{{/inline}}
{{#*inline "pitch-accent-item-graph-position"}}{{> pitch-accent-item-graph-position-x index=index position=position}} {{> pitch-accent-item-graph-position-y index=index position=position}}{{/inline}}
{{#*inline "pitch-accent-item-graph"}}
{{~#scope~}}
{{~#set "morae" (getKanaMorae reading)}}{{/set~}}
{{~#set "morae-count" (property (get "morae") "length")}}{{/set~}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {{#op "+" 50 (op "*" 50 (get "morae-count"))}}{{/op}} 100" style="display:inline-block;height:2em;">
<defs>
<g id="term-pitch-accent-graph-dot"><circle cx="0" cy="0" r="15" style="fill:#000;stroke:#000;stroke-width:5;" /></g>
<g id="term-pitch-accent-graph-dot-downstep"><circle cx="0" cy="0" r="15" style="fill:none;stroke:#000;stroke-width:5;" /><circle cx="0" cy="0" r="5" style="fill:none;stroke:#000;stroke-width:5;" /></g>
<g id="term-pitch-accent-graph-triangle"><path d="M0 13 L15 -13 L-15 -13 Z" style="fill:none;stroke:#000;stroke-width:5;" /></g>
</defs>
<path style="fill:none;stroke:#000;stroke-width:5;" d="
{{~#set "cmd" "M"}}{{/set~}}
{{~#each (get "morae")~}}
{{~#get "cmd"}}{{/get~}}
{{~> pitch-accent-item-graph-position index=@index position=../position~}}
{{~#set "cmd" "L"}}{{/set~}}
{{~/each~}}
"></path>
<path style="fill:none;stroke:#000;stroke-width:5;stroke-dasharray:5 5;" d="M{{> pitch-accent-item-graph-position index=(op "-" (get "morae-count") 1) position=position}} L{{> pitch-accent-item-graph-position index=(get "morae-count") position=position}}"></path>
{{#each (get "morae")}}
<use href="{{#if (op "&&" (isMoraPitchHigh @index ../position) (op "!" (isMoraPitchHigh (op "+" @index 1) ../position)))}}#term-pitch-accent-graph-dot-downstep{{else}}#term-pitch-accent-graph-dot{{/if}}" x="{{> pitch-accent-item-graph-position-x index=@index position=../position}}" y="{{> pitch-accent-item-graph-position-y index=@index position=../position}}"></use>
{{/each}}
<use href="#term-pitch-accent-graph-triangle" x="{{> pitch-accent-item-graph-position-x index=(get "morae-count") position=position}}" y="{{> pitch-accent-item-graph-position-y index=(get "morae-count") position=position}}"></use>
</svg>
{{~/scope~}}
{{/inline}}
{{#*inline "pitch-accent-item-position"~}}
<span>[{{position}}]</span>
{{~/inline}}
{{#*inline "pitch-accent-item"}}
{{~#if (op "==" format "downstep-notation")~}}
{{~> pitch-accent-item-downstep-notation~}}
{{~else if (op "==" format "graph")~}}
{{~> pitch-accent-item-graph~}}
{{~else if (op "==" format "position")~}}
{{~> pitch-accent-item-position~}}
{{~/if~}}
{{/inline}}
{{#*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 "pitch-accent-list"}}
{{~#if (op ">" pitchCount 0)~}}
{{~#if (op ">" pitchCount 1)~}}<ol>{{~/if~}}
{{~#each pitches~}}
{{~#each pitches~}}
{{~#if (op ">" ../../pitchCount 1)~}}<li>{{~/if~}}
{{~> pitch-accent-item-disambiguation~}}
{{~> pitch-accent-item format=../../format~}}
{{~#if (op ">" ../../pitchCount 1)~}}</li>{{~/if~}}
{{~/each~}}
{{~/each~}}
{{~#if (op ">" pitchCount 1)~}}</ol>{{~/if~}}
{{~else~}}
No pitch accent data
{{~/if~}}
{{/inline}}
{{#*inline "pitch-accents"}}
{{~> pitch-accent-list format='downstep-notation'~}}
{{/inline}}
{{#*inline "pitch-accent-graphs"}}
{{~> pitch-accent-list format='graph'~}}
{{/inline}}
{{#*inline "pitch-accent-positions"}}
{{~> pitch-accent-list format='position'~}}
{{/inline}}
{{! End Pitch Accents }}
{{~> (lookup . "marker") ~}}

View File

@ -15,6 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* global
* DictionaryDataUtil
*/
class AnkiNoteBuilder {
constructor({anki, audioSystem, renderTemplate}) {
this._anki = anki;
@ -39,9 +43,10 @@ class AnkiNoteBuilder {
}
};
const data = this.createNoteData(definition, mode, context, options);
const formattedFieldValuePromises = [];
for (const [, fieldValue] of modeOptionsFieldEntries) {
const formattedFieldValuePromise = this.formatField(fieldValue, definition, mode, context, options, templates, null);
const formattedFieldValuePromise = this.formatField(fieldValue, data, templates, null);
formattedFieldValuePromises.push(formattedFieldValuePromise);
}
@ -55,10 +60,14 @@ class AnkiNoteBuilder {
return note;
}
async formatField(field, definition, mode, context, options, templates, errors=null) {
const data = {
createNoteData(definition, mode, context, options) {
const pitches = DictionaryDataUtil.getPitchAccentInfos(definition);
const pitchCount = pitches.reduce((i, v) => i + v.pitches.length, 0);
return {
marker: null,
definition,
pitches,
pitchCount,
group: options.general.resultOutputMode === 'group',
merge: options.general.resultOutputMode === 'merge',
modeTermKanji: mode === 'term-kanji',
@ -67,6 +76,9 @@ class AnkiNoteBuilder {
compactGlossaries: options.general.compactGlossaries,
context
};
}
async formatField(field, data, templates, errors=null) {
const pattern = /\{([\w-]+)\}/g;
return await AnkiNoteBuilder.stringReplaceAsync(field, pattern, async (g0, marker) => {
data.marker = marker;

View File

@ -380,31 +380,83 @@ class OptionsUtil {
return [
{
async: false,
update: (options) => {
// Version 1 changes:
// Added options.global.database.prefixWildcardsSupported = false
options.global = {
database: {
prefixWildcardsSupported: false
}
};
return options;
}
update: this._updateVersion1.bind(this)
},
{
async: false,
update: (options) => {
// Version 2 changes:
// Legacy profile update process moved into this upgrade function.
for (const profile of options.profiles) {
if (!Array.isArray(profile.conditionGroups)) {
profile.conditionGroups = [];
}
profile.options = this._legacyProfileUpdateUpdateVersion(profile.options);
}
return options;
}
update: this._updateVersion2.bind(this)
},
{
async: true,
update: this._updateVersion3.bind(this)
}
];
}
static _updateVersion1(options) {
// Version 1 changes:
// Added options.global.database.prefixWildcardsSupported = false.
options.global = {
database: {
prefixWildcardsSupported: false
}
};
return options;
}
static _updateVersion2(options) {
// Version 2 changes:
// Legacy profile update process moved into this upgrade function.
for (const profile of options.profiles) {
if (!Array.isArray(profile.conditionGroups)) {
profile.conditionGroups = [];
}
profile.options = this._legacyProfileUpdateUpdateVersion(profile.options);
}
return options;
}
static async _updateVersion3(options) {
// Version 3 changes:
// Pitch accent Anki field templates added.
let addition = null;
for (const {options: profileOptions} of options.profiles) {
const fieldTemplates = profileOptions.anki.fieldTemplates;
if (fieldTemplates !== null) {
if (addition === null) {
addition = await this._updateVersion3GetAnkiFieldTemplates();
}
profileOptions.anki.fieldTemplates = this._addFieldTemplatesBeforeEnd(fieldTemplates, addition);
}
}
return options;
}
static async _updateVersion3GetAnkiFieldTemplates() {
const url = chrome.runtime.getURL('/bg/data/anki-field-templates-upgrade-v2.handlebars');
const response = await fetch(url, {
method: 'GET',
mode: 'no-cors',
cache: 'default',
credentials: 'omit',
redirect: 'follow',
referrerPolicy: 'no-referrer'
});
return await response.text();
}
static async _addFieldTemplatesBeforeEnd(fieldTemplates, addition) {
const pattern = /[ \t]*\{\{~?>\s*\(\s*lookup\s*\.\s*"marker"\s*\)\s*~?\}\}/;
const newline = '\n';
let replaced = false;
fieldTemplates = fieldTemplates.replace(pattern, (g0) => {
replaced = true;
return `${addition}${newline}${g0}`;
});
if (!replaced) {
fieldTemplates += newline;
fieldTemplates += addition;
}
return fieldTemplates;
}
}

View File

@ -144,7 +144,8 @@ class AnkiTemplatesController {
let templates = options.anki.fieldTemplates;
if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; }
const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: api.templateRender.bind(api)});
result = await ankiNoteBuilder.formatField(field, definition, mode, context, options, templates, exceptions);
const data = ankiNoteBuilder.createNoteData(definition, mode, context, options);
result = await ankiNoteBuilder.formatField(field, data, templates, exceptions);
}
} catch (e) {
exceptions.push(e);

View File

@ -54,6 +54,9 @@ class AnkiController {
'furigana-plain',
'glossary',
'glossary-brief',
'pitch-accents',
'pitch-accent-graphs',
'pitch-accent-positions',
'reading',
'screenshot',
'sentence',
@ -63,6 +66,9 @@ class AnkiController {
case 'kanji':
return [
'character',
'cloze-body',
'cloze-prefix',
'cloze-suffix',
'dictionary',
'document-title',
'glossary',

View File

@ -82,7 +82,10 @@ class TemplateRenderer {
['get', this._get.bind(this)],
['set', this._set.bind(this)],
['scope', this._scope.bind(this)],
['isMoraPitchHigh', this._isMoraPitchHigh.bind(this)]
['property', this._property.bind(this)],
['noop', this._noop.bind(this)],
['isMoraPitchHigh', this._isMoraPitchHigh.bind(this)],
['getKanaMorae', this._getKanaMorae.bind(this)]
];
for (const [name, helper] of helpers) {
@ -316,21 +319,20 @@ class TemplateRenderer {
_set(context, ...args) {
switch (args.length) {
case 2:
{
const [key, options] = args;
const value = options.fn(context);
this._stateStack[this._stateStack.length - 1].set(key, value);
return value;
}
{
const [key, options] = args;
const value = options.fn(context);
this._stateStack[this._stateStack.length - 1].set(key, value);
}
break;
case 3:
{
const [key, value] = args;
this._stateStack[this._stateStack.length - 1].set(key, value);
return value;
}
default:
return void 0;
{
const [key, value] = args;
this._stateStack[this._stateStack.length - 1].set(key, value);
}
break;
}
return '';
}
_scope(context, options) {
@ -344,7 +346,30 @@ class TemplateRenderer {
}
}
_isMoraPitchHigh(context, position, index) {
_property(context, ...args) {
const ii = args.length - 1;
if (ii <= 0) { return void 0; }
try {
let value = args[0];
for (let i = 1; i < ii; ++i) {
value = value[args[i]];
}
return value;
} catch (e) {
return void 0;
}
}
_noop(context, options) {
return options.fn(context);
}
_isMoraPitchHigh(context, index, position) {
return jp.isMoraPitchHigh(index, position);
}
_getKanaMorae(context, text) {
return jp.getKanaMorae(`${text}`);
}
}

View File

@ -1162,6 +1162,7 @@
<script src="/bg/js/settings/profiles.js"></script>
<script src="/bg/js/settings/settings-controller.js"></script>
<script src="/bg/js/settings/storage.js"></script>
<script src="/mixed/js/dictionary-data-util.js"></script>
<script src="/mixed/js/object-property-accessor.js"></script>
<script src="/mixed/js/task-accumulator.js"></script>
<script src="/mixed/js/dom-data-binder.js"></script>