diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-07-09 17:48:27 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-09 17:48:27 -0400 |
commit | 8c68fa4d9435b562ffe23df92a2b7b620a0ed78e (patch) | |
tree | 8c622fe11063b3f9694033f10e47b2ac05badccc /ext | |
parent | 0d167095479822adf1ed8918e3d1a349b3a53377 (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.handlebars | 10 | ||||
-rw-r--r-- | ext/data/templates/default-anki-field-templates.handlebars | 10 | ||||
-rw-r--r-- | ext/js/data/anki-note-builder.js | 66 | ||||
-rw-r--r-- | ext/js/data/options-util.js | 1 | ||||
-rw-r--r-- | ext/js/display/display-anki.js | 14 | ||||
-rw-r--r-- | ext/js/display/display.js | 2 | ||||
-rw-r--r-- | ext/js/pages/settings/anki-controller.js | 2 | ||||
-rw-r--r-- | ext/js/pages/settings/anki-templates-controller.js | 3 | ||||
-rw-r--r-- | ext/js/templates/template-renderer-media-provider.js | 34 | ||||
-rw-r--r-- | ext/settings.html | 4 |
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> |