aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-07-09 17:48:27 -0400
committerGitHub <noreply@github.com>2021-07-09 17:48:27 -0400
commit8c68fa4d9435b562ffe23df92a2b7b620a0ed78e (patch)
tree8c622fe11063b3f9694033f10e47b2ac05badccc /ext
parent0d167095479822adf1ed8918e3d1a349b3a53377 (diff)
Anki text furigana parsing and {sentence-furigana} marker (#1814)
* Add support for textFurigana media * Add readingMode parameter * Implement readingMode * Add {sentence-furigana} marker * Fallback to sentence if furigana isn't available * Update test data
Diffstat (limited to 'ext')
-rw-r--r--ext/data/templates/anki-field-templates-upgrade-v13.handlebars10
-rw-r--r--ext/data/templates/default-anki-field-templates.handlebars10
-rw-r--r--ext/js/data/anki-note-builder.js66
-rw-r--r--ext/js/data/options-util.js1
-rw-r--r--ext/js/display/display-anki.js14
-rw-r--r--ext/js/display/display.js2
-rw-r--r--ext/js/pages/settings/anki-controller.js2
-rw-r--r--ext/js/pages/settings/anki-templates-controller.js3
-rw-r--r--ext/js/templates/template-renderer-media-provider.js34
-rw-r--r--ext/settings.html4
10 files changed, 125 insertions, 21 deletions
diff --git a/ext/data/templates/anki-field-templates-upgrade-v13.handlebars b/ext/data/templates/anki-field-templates-upgrade-v13.handlebars
index 78820b95..25007030 100644
--- a/ext/data/templates/anki-field-templates-upgrade-v13.handlebars
+++ b/ext/data/templates/anki-field-templates-upgrade-v13.handlebars
@@ -2,6 +2,16 @@
{{~#if (hasMedia "selectionText")}}{{#getMedia "selectionText" format="text"}}{{/getMedia}}{{/if~}}
{{/inline}}
+{{#*inline "sentence-furigana"}}
+ {{~#if definition.cloze~}}
+ {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}}
+ {{#getMedia "textFurigana" definition.cloze.sentence format="html"}}{{/getMedia}}
+ {{~else~}}
+ {{definition.cloze.sentence}}
+ {{~/if~}}
+ {{~/if~}}
+{{/inline}}
+
{{<<<<<<<}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}}
{{=======}}
diff --git a/ext/data/templates/default-anki-field-templates.handlebars b/ext/data/templates/default-anki-field-templates.handlebars
index c9ee2833..cdbec87d 100644
--- a/ext/data/templates/default-anki-field-templates.handlebars
+++ b/ext/data/templates/default-anki-field-templates.handlebars
@@ -382,4 +382,14 @@
{{~#if (hasMedia "selectionText")}}{{#getMedia "selectionText" format="text"}}{{/getMedia}}{{/if~}}
{{/inline}}
+{{#*inline "sentence-furigana"}}
+ {{~#if definition.cloze~}}
+ {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}}
+ {{#getMedia "textFurigana" definition.cloze.sentence format="html"}}{{/getMedia}}
+ {{~else~}}
+ {{definition.cloze.sentence}}
+ {{~/if~}}
+ {{~/if~}}
+{{/inline}}
+
{{~> (lookup . "marker") ~}}
diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js
index 23dd648b..02aa7969 100644
--- a/ext/js/data/anki-note-builder.js
+++ b/ext/js/data/anki-note-builder.js
@@ -21,7 +21,8 @@
*/
class AnkiNoteBuilder {
- constructor() {
+ constructor({japaneseUtil}) {
+ this._japaneseUtil = japaneseUtil;
this._markerPattern = AnkiUtil.cloneFieldMarkerPattern(true);
this._templateRenderer = new TemplateRendererProxy();
this._batchedRequests = [];
@@ -284,6 +285,7 @@ class AnkiNoteBuilder {
let injectClipboardImage = false;
let injectClipboardText = false;
let injectSelectionText = false;
+ const textFuriganaDetails = [];
const dictionaryMediaDetails = [];
for (const requirement of requirements) {
const {type} = requirement;
@@ -293,6 +295,12 @@ class AnkiNoteBuilder {
case 'clipboardImage': injectClipboardImage = true; break;
case 'clipboardText': injectClipboardText = true; break;
case 'selectionText': injectSelectionText = true; break;
+ case 'textFurigana':
+ {
+ const {text, readingMode} = requirement;
+ textFuriganaDetails.push({text, readingMode});
+ }
+ break;
case 'dictionaryMedia':
{
const {dictionary, path} = requirement;
@@ -323,6 +331,14 @@ class AnkiNoteBuilder {
}
}
}
+ let textFuriganaPromise = null;
+ if (textFuriganaDetails.length > 0) {
+ const textParsingOptions = mediaOptions.textParsing;
+ if (typeof textParsingOptions === 'object' && textParsingOptions !== null) {
+ const {optionsContext, scanLength} = textParsingOptions;
+ textFuriganaPromise = this._getTextFurigana(textFuriganaDetails, optionsContext, scanLength);
+ }
+ }
// Inject media
const selectionText = injectSelectionText ? this._getSelectionText() : null;
@@ -335,6 +351,7 @@ class AnkiNoteBuilder {
dictionaryMediaDetails
);
const {audioFileName, screenshotFileName, clipboardImageFileName, clipboardText, dictionaryMedia: dictionaryMediaArray, errors} = injectedMedia;
+ const textFurigana = textFuriganaPromise !== null ? await textFuriganaPromise : [];
// Format results
const dictionaryMedia = {};
@@ -353,6 +370,7 @@ class AnkiNoteBuilder {
clipboardImage: (typeof clipboardImageFileName === 'string' ? {fileName: clipboardImageFileName} : null),
clipboardText: (typeof clipboardText === 'string' ? {text: clipboardText} : null),
selectionText: (typeof selectionText === 'string' ? {text: selectionText} : null),
+ textFurigana,
dictionaryMedia
};
return {media, errors};
@@ -361,4 +379,50 @@ class AnkiNoteBuilder {
_getSelectionText() {
return document.getSelection().toString();
}
+
+ async _getTextFurigana(entries, optionsContext, scanLength) {
+ const results = [];
+ for (const {text, readingMode} of entries) {
+ const parseResults = await yomichan.api.parseText(text, optionsContext, scanLength, true, false);
+ let data = null;
+ for (const {source, content} of parseResults) {
+ if (source !== 'scanning-parser') { continue; }
+ data = content;
+ break;
+ }
+ if (data !== null) {
+ const html = this._createFuriganaHtml(data, readingMode);
+ results.push({text, readingMode, details: {html}});
+ }
+ }
+ return results;
+ }
+
+ _createFuriganaHtml(data, readingMode) {
+ let result = '';
+ for (const term of data) {
+ result += '<span class="term">';
+ for (const {text, reading} of term) {
+ if (reading.length > 0) {
+ const reading2 = this._convertReading(reading, readingMode);
+ result += `<ruby>${text}<rt>${reading2}</rt></ruby>`;
+ } else {
+ result += text;
+ }
+ }
+ result += '</span>';
+ }
+ return result;
+ }
+
+ _convertReading(reading, readingMode) {
+ switch (readingMode) {
+ case 'hiragana':
+ return this._japaneseUtil.convertKatakanaToHiragana(reading);
+ case 'katakana':
+ return this._japaneseUtil.convertHiraganaToKatakana(reading);
+ default:
+ return reading;
+ }
+ }
}
diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js
index 3d36fc2e..36630e2f 100644
--- a/ext/js/data/options-util.js
+++ b/ext/js/data/options-util.js
@@ -855,6 +855,7 @@ class OptionsUtil {
// Handlebars templates updated to use formatGlossary.
// Handlebars templates updated to use new media format.
// Added {selection-text} field marker.
+ // Added {sentence-furigana} field marker.
await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v13.handlebars');
return options;
}
diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js
index 114a9d13..235149ad 100644
--- a/ext/js/display/display-anki.js
+++ b/ext/js/display/display-anki.js
@@ -22,11 +22,11 @@
*/
class DisplayAnki {
- constructor(display) {
+ constructor(display, japaneseUtil) {
this._display = display;
this._ankiFieldTemplates = null;
this._ankiFieldTemplatesDefault = null;
- this._ankiNoteBuilder = new AnkiNoteBuilder();
+ this._ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil});
this._ankiNoteNotification = null;
this._ankiNoteNotificationEventListeners = null;
this._ankiTagNotification = null;
@@ -44,6 +44,7 @@ class DisplayAnki {
this._duplicateScope = 'collection';
this._screenshotFormat = 'png';
this._screenshotQuality = 100;
+ this._scanLength = 10;
this._noteTags = [];
this._modeOptions = new Map();
this._dictionaryEntryTypeModeMap = new Map([
@@ -141,7 +142,8 @@ class DisplayAnki {
_onOptionsUpdated({options}) {
const {
general: {resultOutputMode, glossaryLayoutMode, compactTags},
- anki: {tags, duplicateScope, suspendNewCards, checkForDuplicates, displayTags, kanji, terms, screenshot: {format, quality}}
+ anki: {tags, duplicateScope, suspendNewCards, checkForDuplicates, displayTags, kanji, terms, screenshot: {format, quality}},
+ scanning: {length: scanLength}
} = options;
this._checkForDuplicates = checkForDuplicates;
@@ -153,6 +155,7 @@ class DisplayAnki {
this._duplicateScope = duplicateScope;
this._screenshotFormat = format;
this._screenshotQuality = quality;
+ this._scanLength = scanLength;
this._noteTags = [...tags];
this._modeOptions.clear();
this._modeOptions.set('kanji', kanji);
@@ -526,6 +529,7 @@ class DisplayAnki {
const contentOrigin = this._display.getContentOrigin();
const details = this._ankiNoteBuilder.getDictionaryEntryDetailsForNote(dictionaryEntry);
const audioDetails = (details.type === 'term' ? this._display.getAnkiNoteMediaAudioDetails(details.term, details.reading) : null);
+ const optionsContext = this._display.getOptionsContext();
const {note, errors, requirements: outputRequirements} = await this._ankiNoteBuilder.createNote({
dictionaryEntry,
@@ -547,6 +551,10 @@ class DisplayAnki {
format: this._screenshotFormat,
quality: this._screenshotQuality,
contentOrigin
+ },
+ textParsing: {
+ optionsContext,
+ scanLength: this._scanLength
}
},
requirements
diff --git a/ext/js/display/display.js b/ext/js/display/display.js
index d79cc7e2..12486a72 100644
--- a/ext/js/display/display.js
+++ b/ext/js/display/display.js
@@ -110,7 +110,7 @@ class Display extends EventDispatcher {
this._queryPostProcessor = null;
this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this);
this._elementOverflowController = new ElementOverflowController();
- this._displayAnki = new DisplayAnki(this);
+ this._displayAnki = new DisplayAnki(this, japaneseUtil);
this._hotkeyHandler.registerActions([
['close', () => { this._onHotkeyClose(); }],
diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js
index b98de1b6..c0f7f626 100644
--- a/ext/js/pages/settings/anki-controller.js
+++ b/ext/js/pages/settings/anki-controller.js
@@ -104,6 +104,7 @@ class AnkiController {
'search-query',
'selection-text',
'sentence',
+ 'sentence-furigana',
'tags',
'url'
];
@@ -123,6 +124,7 @@ class AnkiController {
'screenshot',
'search-query',
'selection-text',
+ 'sentence-furigana',
'sentence',
'stroke-count',
'tags',
diff --git a/ext/js/pages/settings/anki-templates-controller.js b/ext/js/pages/settings/anki-templates-controller.js
index aa565bad..ad2790ca 100644
--- a/ext/js/pages/settings/anki-templates-controller.js
+++ b/ext/js/pages/settings/anki-templates-controller.js
@@ -17,6 +17,7 @@
/* global
* AnkiNoteBuilder
+ * JapaneseUtil
*/
class AnkiTemplatesController {
@@ -32,7 +33,7 @@ class AnkiTemplatesController {
this._renderFieldInput = null;
this._renderResult = null;
this._fieldTemplateResetModal = null;
- this._ankiNoteBuilder = new AnkiNoteBuilder();
+ this._ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil: new JapaneseUtil(null)});
}
async prepare() {
diff --git a/ext/js/templates/template-renderer-media-provider.js b/ext/js/templates/template-renderer-media-provider.js
index 4fd40c67..604b5331 100644
--- a/ext/js/templates/template-renderer-media-provider.js
+++ b/ext/js/templates/template-renderer-media-provider.js
@@ -54,21 +54,7 @@ class TemplateRendererMediaProvider {
}
_getFormattedValue(data, format) {
- switch (format) {
- case 'fileName':
- {
- const {fileName} = data;
- if (typeof fileName === 'string') { return fileName; }
- }
- break;
- case 'text':
- {
- const {text} = data;
- if (typeof text === 'string') { return text; }
- }
- break;
- }
- return null;
+ return Object.prototype.hasOwnProperty.call(data, format) ? data[format] : null;
}
_getMediaData(media, args, namedArgs) {
@@ -79,6 +65,7 @@ class TemplateRendererMediaProvider {
case 'clipboardImage': return this._getSimpleMediaData(media, 'clipboardImage');
case 'clipboardText': return this._getSimpleMediaData(media, 'clipboardText');
case 'selectionText': return this._getSimpleMediaData(media, 'selectionText');
+ case 'textFurigana': return this._getTextFurigana(media, args[1], namedArgs);
case 'dictionaryMedia': return this._getDictionaryMedia(media, args[1], namedArgs);
default: return null;
}
@@ -114,4 +101,21 @@ class TemplateRendererMediaProvider {
});
return null;
}
+
+ _getTextFurigana(media, text, namedArgs) {
+ const {readingMode=null} = namedArgs;
+ const {textFurigana} = media;
+ if (Array.isArray(textFurigana)) {
+ for (const entry of textFurigana) {
+ if (entry.text !== text || entry.readingMode !== readingMode) { continue; }
+ return entry.details;
+ }
+ }
+ this._addRequirement({
+ type: 'textFurigana',
+ text,
+ readingMode
+ });
+ return null;
+ }
}
diff --git a/ext/settings.html b/ext/settings.html
index a8c8149b..0144ddd3 100644
--- a/ext/settings.html
+++ b/ext/settings.html
@@ -2868,6 +2868,10 @@
<td>Sentence, quote, or phrase that the term or kanji appears in from the source content.</td>
</tr>
<tr>
+ <td><code class="anki-field-marker">{sentence-furigana}</code></td>
+ <td>Sentence, quote, or phrase that the term or kanji appears in from the source content, with furigana added.</td>
+ </tr>
+ <tr>
<td><code class="anki-field-marker">{url}</code></td>
<td>Address of the web page in which the term or kanji appeared in.</td>
</tr>