From 838fd211c6737ce7e2b6802a43837cf4300b60d2 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 1 Aug 2020 16:23:33 -0400 Subject: [PATCH] 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 --- README.md | 4 + ext/bg/background.html | 1 + ...anki-field-templates-upgrade-v2.handlebars | 109 +++++++++++++++++ .../default-anki-field-templates.handlebars | 110 ++++++++++++++++++ ext/bg/js/anki-note-builder.js | 18 ++- ext/bg/js/options.js | 94 +++++++++++---- ext/bg/js/settings/anki-templates.js | 3 +- ext/bg/js/settings/anki.js | 6 + ext/bg/js/template-renderer.js | 55 ++++++--- ext/bg/settings.html | 1 + 10 files changed, 361 insertions(+), 40 deletions(-) create mode 100644 ext/bg/data/anki-field-templates-upgrade-v2.handlebars diff --git a/README.md b/README.md index 6231179c..b913fe25 100644 --- a/README.md +++ b/README.md @@ -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. 日本語にほんご). `{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. diff --git a/ext/bg/background.html b/ext/bg/background.html index a30b55a5..73dbc251 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -46,6 +46,7 @@ + diff --git a/ext/bg/data/anki-field-templates-upgrade-v2.handlebars b/ext/bg/data/anki-field-templates-upgrade-v2.handlebars new file mode 100644 index 00000000..c018094e --- /dev/null +++ b/ext/bg/data/anki-field-templates-upgrade-v2.handlebars @@ -0,0 +1,109 @@ +{{! Pitch Accents }} +{{#*inline "pitch-accent-item-downstep-notation"}} + {{~#scope~}} + + {{~#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~}} + {{{.}}} + {{~/each~}} + + {{~/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~}} + + + + + + + pitch-accent-item-graph-position index=@index position=../position~}} + {{~#set "cmd" "L"}}{{/set~}} + {{~/each~}} + "> + pitch-accent-item-graph-position index=(get "morae-count") position=position}}"> + {{#each (get "morae")}} + + {{/each}} + + + {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-item-position"~}} + [{{position}}] +{{~/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~}} + ({{#each (get "exclusive")~}} + {{~#get "separator"}}{{/get~}}{{{.}}} + {{~/each}} only) + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-list"}} + {{~#if (op ">" pitchCount 0)~}} + {{~#if (op ">" pitchCount 1)~}}
    {{~/if~}} + {{~#each pitches~}} + {{~#each pitches~}} + {{~#if (op ">" ../../pitchCount 1)~}}
  1. {{~/if~}} + {{~> pitch-accent-item-disambiguation~}} + {{~> pitch-accent-item format=../../format~}} + {{~#if (op ">" ../../pitchCount 1)~}}
  2. {{~/if~}} + {{~/each~}} + {{~/each~}} + {{~#if (op ">" pitchCount 1)~}}
{{~/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 }} diff --git a/ext/bg/data/default-anki-field-templates.handlebars b/ext/bg/data/default-anki-field-templates.handlebars index 42deae23..b348042c 100644 --- a/ext/bg/data/default-anki-field-templates.handlebars +++ b/ext/bg/data/default-anki-field-templates.handlebars @@ -166,4 +166,114 @@ {{~context.document.title~}} {{/inline}} +{{! Pitch Accents }} +{{#*inline "pitch-accent-item-downstep-notation"}} + {{~#scope~}} + + {{~#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~}} + {{{.}}} + {{~/each~}} + + {{~/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~}} + + + + + + + pitch-accent-item-graph-position index=@index position=../position~}} + {{~#set "cmd" "L"}}{{/set~}} + {{~/each~}} + "> + pitch-accent-item-graph-position index=(get "morae-count") position=position}}"> + {{#each (get "morae")}} + + {{/each}} + + + {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-item-position"~}} + [{{position}}] +{{~/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~}} + ({{#each (get "exclusive")~}} + {{~#get "separator"}}{{/get~}}{{{.}}} + {{~/each}} only) + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-list"}} + {{~#if (op ">" pitchCount 0)~}} + {{~#if (op ">" pitchCount 1)~}}
    {{~/if~}} + {{~#each pitches~}} + {{~#each pitches~}} + {{~#if (op ">" ../../pitchCount 1)~}}
  1. {{~/if~}} + {{~> pitch-accent-item-disambiguation~}} + {{~> pitch-accent-item format=../../format~}} + {{~#if (op ">" ../../pitchCount 1)~}}
  2. {{~/if~}} + {{~/each~}} + {{~/each~}} + {{~#if (op ">" pitchCount 1)~}}
{{~/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") ~}} diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 7fe8962a..2405543e 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -15,6 +15,10 @@ * along with this program. If not, see . */ +/* 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; diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index ffea96f8..0d83f428 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -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; + } } diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index 88d4fe04..4e004308 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -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); diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index 51dabba4..ac4c5455 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -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', diff --git a/ext/bg/js/template-renderer.js b/ext/bg/js/template-renderer.js index ef05cbd8..59af74c8 100644 --- a/ext/bg/js/template-renderer.js +++ b/ext/bg/js/template-renderer.js @@ -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}`); + } } diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 260c1b46..e29b1f45 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1162,6 +1162,7 @@ +