From 0f15cca2dff995218a52ff7066008da4cd414e3f Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 11 Mar 2020 20:33:01 -0400 Subject: Convert Japanese utilities to a module-like style --- ext/bg/js/audio-uri-builder.js | 4 +- ext/bg/js/backend.js | 18 +- ext/bg/js/clipboard-monitor.js | 4 +- ext/bg/js/handlebars.js | 9 +- ext/bg/js/japanese.js | 770 +++++++++++++++++++++-------------------- ext/bg/js/translator.js | 24 +- 6 files changed, 421 insertions(+), 408 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/audio-uri-builder.js b/ext/bg/js/audio-uri-builder.js index 499c3441..158006bb 100644 --- a/ext/bg/js/audio-uri-builder.js +++ b/ext/bg/js/audio-uri-builder.js @@ -17,7 +17,7 @@ */ /* global - * jpIsStringEntirelyKana + * jp */ class AudioUriBuilder { @@ -66,7 +66,7 @@ class AudioUriBuilder { let kana = definition.reading; let kanji = definition.expression; - if (!kana && jpIsStringEntirelyKana(kanji)) { + if (!kana && jp.isStringEntirelyKana(kanji)) { kana = kanji; kanji = null; } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 978c5a4a..b217e64d 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -32,9 +32,7 @@ * dictEnabledSet * dictTermsSort * handlebarsRenderDynamic - * jpConvertReading - * jpDistributeFuriganaInflected - * jpKatakanaToHiragana + * jp * optionsLoad * optionsSave * profileConditionsDescriptor @@ -402,13 +400,13 @@ class Backend { dictTermsSort(definitions); const {expression, reading} = definitions[0]; const source = text.substring(0, sourceLength); - for (const {text: text2, furigana} of jpDistributeFuriganaInflected(expression, reading, source)) { - const reading2 = jpConvertReading(text2, furigana, options.parsing.readingMode); + for (const {text: text2, furigana} of jp.distributeFuriganaInflected(expression, reading, source)) { + const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode); term.push({text: text2, reading: reading2}); } text = text.substring(source.length); } else { - const reading = jpConvertReading(text[0], null, options.parsing.readingMode); + const reading = jp.convertReading(text[0], null, options.parsing.readingMode); term.push({text: text[0], reading}); text = text.substring(1); } @@ -427,16 +425,16 @@ class Backend { for (const {expression, reading, source} of parsedLine) { const term = []; if (expression !== null && reading !== null) { - for (const {text: text2, furigana} of jpDistributeFuriganaInflected( + for (const {text: text2, furigana} of jp.distributeFuriganaInflected( expression, - jpKatakanaToHiragana(reading), + jp.convertKatakanaToHiragana(reading), source )) { - const reading2 = jpConvertReading(text2, furigana, options.parsing.readingMode); + const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode); term.push({text: text2, reading: reading2}); } } else { - const reading2 = jpConvertReading(source, null, options.parsing.readingMode); + const reading2 = jp.convertReading(source, null, options.parsing.readingMode); term.push({text: source, reading: reading2}); } result.push(term); diff --git a/ext/bg/js/clipboard-monitor.js b/ext/bg/js/clipboard-monitor.js index 9a881f57..c67525fc 100644 --- a/ext/bg/js/clipboard-monitor.js +++ b/ext/bg/js/clipboard-monitor.js @@ -17,7 +17,7 @@ */ /* global - * jpIsStringPartiallyJapanese + * jp */ class ClipboardMonitor extends EventDispatcher { @@ -54,7 +54,7 @@ class ClipboardMonitor extends EventDispatcher { text !== this._previousText ) { this._previousText = text; - if (jpIsStringPartiallyJapanese(text)) { + if (jp.isStringPartiallyJapanese(text)) { this.trigger('change', {text}); } } diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js index e3ce6bd0..5fda5baa 100644 --- a/ext/bg/js/handlebars.js +++ b/ext/bg/js/handlebars.js @@ -18,8 +18,7 @@ /* global * Handlebars - * jpDistributeFurigana - * jpIsCodePointKanji + * jp */ function handlebarsEscape(text) { @@ -33,7 +32,7 @@ function handlebarsDumpObject(options) { function handlebarsFurigana(options) { const definition = options.fn(this); - const segs = jpDistributeFurigana(definition.expression, definition.reading); + const segs = jp.distributeFurigana(definition.expression, definition.reading); let result = ''; for (const seg of segs) { @@ -49,7 +48,7 @@ function handlebarsFurigana(options) { function handlebarsFuriganaPlain(options) { const definition = options.fn(this); - const segs = jpDistributeFurigana(definition.expression, definition.reading); + const segs = jp.distributeFurigana(definition.expression, definition.reading); let result = ''; for (const seg of segs) { @@ -66,7 +65,7 @@ function handlebarsFuriganaPlain(options) { function handlebarsKanjiLinks(options) { let result = ''; for (const c of options.fn(this)) { - if (jpIsCodePointKanji(c.codePointAt(0))) { + if (jp.isCodePointKanji(c.codePointAt(0))) { result += `${c}`; } else { result += c; diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js index 3b37754d..182d5b98 100644 --- a/ext/bg/js/japanese.js +++ b/ext/bg/js/japanese.js @@ -20,439 +20,461 @@ * wanakana */ -const JP_HALFWIDTH_KATAKANA_MAPPING = new Map([ - ['ヲ', 'ヲヺ-'], - ['ァ', 'ァ--'], - ['ィ', 'ィ--'], - ['ゥ', 'ゥ--'], - ['ェ', 'ェ--'], - ['ォ', 'ォ--'], - ['ャ', 'ャ--'], - ['ュ', 'ュ--'], - ['ョ', 'ョ--'], - ['ッ', 'ッ--'], - ['ー', 'ー--'], - ['ア', 'ア--'], - ['イ', 'イ--'], - ['ウ', 'ウヴ-'], - ['エ', 'エ--'], - ['オ', 'オ--'], - ['カ', 'カガ-'], - ['キ', 'キギ-'], - ['ク', 'クグ-'], - ['ケ', 'ケゲ-'], - ['コ', 'コゴ-'], - ['サ', 'サザ-'], - ['シ', 'シジ-'], - ['ス', 'スズ-'], - ['セ', 'セゼ-'], - ['ソ', 'ソゾ-'], - ['タ', 'タダ-'], - ['チ', 'チヂ-'], - ['ツ', 'ツヅ-'], - ['テ', 'テデ-'], - ['ト', 'トド-'], - ['ナ', 'ナ--'], - ['ニ', 'ニ--'], - ['ヌ', 'ヌ--'], - ['ネ', 'ネ--'], - ['ノ', 'ノ--'], - ['ハ', 'ハバパ'], - ['ヒ', 'ヒビピ'], - ['フ', 'フブプ'], - ['ヘ', 'ヘベペ'], - ['ホ', 'ホボポ'], - ['マ', 'マ--'], - ['ミ', 'ミ--'], - ['ム', 'ム--'], - ['メ', 'メ--'], - ['モ', 'モ--'], - ['ヤ', 'ヤ--'], - ['ユ', 'ユ--'], - ['ヨ', 'ヨ--'], - ['ラ', 'ラ--'], - ['リ', 'リ--'], - ['ル', 'ル--'], - ['レ', 'レ--'], - ['ロ', 'ロ--'], - ['ワ', 'ワ--'], - ['ン', 'ン--'] -]); - -const JP_HIRAGANA_RANGE = [0x3040, 0x309f]; -const JP_KATAKANA_RANGE = [0x30a0, 0x30ff]; -const JP_KANA_RANGES = [JP_HIRAGANA_RANGE, JP_KATAKANA_RANGE]; - -const JP_CJK_COMMON_RANGE = [0x4e00, 0x9fff]; -const JP_CJK_RARE_RANGE = [0x3400, 0x4dbf]; -const JP_CJK_RANGES = [JP_CJK_COMMON_RANGE, JP_CJK_RARE_RANGE]; - -const JP_ITERATION_MARK_CHAR_CODE = 0x3005; - -// Japanese character ranges, roughly ordered in order of expected frequency -const JP_JAPANESE_RANGES = [ - JP_HIRAGANA_RANGE, - JP_KATAKANA_RANGE, - - JP_CJK_COMMON_RANGE, - JP_CJK_RARE_RANGE, - - [0xff66, 0xff9f], // Halfwidth katakana - - [0x30fb, 0x30fc], // Katakana punctuation - [0xff61, 0xff65], // Kana punctuation - [0x3000, 0x303f], // CJK punctuation - - [0xff10, 0xff19], // Fullwidth numbers - [0xff21, 0xff3a], // Fullwidth upper case Latin letters - [0xff41, 0xff5a], // Fullwidth lower case Latin letters - - [0xff01, 0xff0f], // Fullwidth punctuation 1 - [0xff1a, 0xff1f], // Fullwidth punctuation 2 - [0xff3b, 0xff3f], // Fullwidth punctuation 3 - [0xff5b, 0xff60], // Fullwidth punctuation 4 - [0xffe0, 0xffee] // Currency markers -]; - - -// Helper functions - -function _jpIsCodePointInRanges(codePoint, ranges) { - for (const [min, max] of ranges) { - if (codePoint >= min && codePoint <= max) { - return true; - } +const jp = (() => { + const HALFWIDTH_KATAKANA_MAPPING = new Map([ + ['ヲ', 'ヲヺ-'], + ['ァ', 'ァ--'], + ['ィ', 'ィ--'], + ['ゥ', 'ゥ--'], + ['ェ', 'ェ--'], + ['ォ', 'ォ--'], + ['ャ', 'ャ--'], + ['ュ', 'ュ--'], + ['ョ', 'ョ--'], + ['ッ', 'ッ--'], + ['ー', 'ー--'], + ['ア', 'ア--'], + ['イ', 'イ--'], + ['ウ', 'ウヴ-'], + ['エ', 'エ--'], + ['オ', 'オ--'], + ['カ', 'カガ-'], + ['キ', 'キギ-'], + ['ク', 'クグ-'], + ['ケ', 'ケゲ-'], + ['コ', 'コゴ-'], + ['サ', 'サザ-'], + ['シ', 'シジ-'], + ['ス', 'スズ-'], + ['セ', 'セゼ-'], + ['ソ', 'ソゾ-'], + ['タ', 'タダ-'], + ['チ', 'チヂ-'], + ['ツ', 'ツヅ-'], + ['テ', 'テデ-'], + ['ト', 'トド-'], + ['ナ', 'ナ--'], + ['ニ', 'ニ--'], + ['ヌ', 'ヌ--'], + ['ネ', 'ネ--'], + ['ノ', 'ノ--'], + ['ハ', 'ハバパ'], + ['ヒ', 'ヒビピ'], + ['フ', 'フブプ'], + ['ヘ', 'ヘベペ'], + ['ホ', 'ホボポ'], + ['マ', 'マ--'], + ['ミ', 'ミ--'], + ['ム', 'ム--'], + ['メ', 'メ--'], + ['モ', 'モ--'], + ['ヤ', 'ヤ--'], + ['ユ', 'ユ--'], + ['ヨ', 'ヨ--'], + ['ラ', 'ラ--'], + ['リ', 'リ--'], + ['ル', 'ル--'], + ['レ', 'レ--'], + ['ロ', 'ロ--'], + ['ワ', 'ワ--'], + ['ン', 'ン--'] + ]); + + const HIRAGANA_RANGE = [0x3040, 0x309f]; + const KATAKANA_RANGE = [0x30a0, 0x30ff]; + const KANA_RANGES = [HIRAGANA_RANGE, KATAKANA_RANGE]; + + const CJK_COMMON_RANGE = [0x4e00, 0x9fff]; + const CJK_RARE_RANGE = [0x3400, 0x4dbf]; + const CJK_RANGES = [CJK_COMMON_RANGE, CJK_RARE_RANGE]; + + const ITERATION_MARK_CODE_POINT = 0x3005; + + // Japanese character ranges, roughly ordered in order of expected frequency + const JAPANESE_RANGES = [ + HIRAGANA_RANGE, + KATAKANA_RANGE, + + CJK_COMMON_RANGE, + CJK_RARE_RANGE, + + [0xff66, 0xff9f], // Halfwidth katakana + + [0x30fb, 0x30fc], // Katakana punctuation + [0xff61, 0xff65], // Kana punctuation + [0x3000, 0x303f], // CJK punctuation + + [0xff10, 0xff19], // Fullwidth numbers + [0xff21, 0xff3a], // Fullwidth upper case Latin letters + [0xff41, 0xff5a], // Fullwidth lower case Latin letters + + [0xff01, 0xff0f], // Fullwidth punctuation 1 + [0xff1a, 0xff1f], // Fullwidth punctuation 2 + [0xff3b, 0xff3f], // Fullwidth punctuation 3 + [0xff5b, 0xff60], // Fullwidth punctuation 4 + [0xffe0, 0xffee] // Currency markers + ]; + + + // Character code testing functions + + function isCodePointKanji(codePoint) { + return isCodePointInRanges(codePoint, CJK_RANGES); } - return false; -} - - -// Character code testing functions -function jpIsCodePointKanji(codePoint) { - return _jpIsCodePointInRanges(codePoint, JP_CJK_RANGES); -} + function isCodePointKana(codePoint) { + return isCodePointInRanges(codePoint, KANA_RANGES); + } -function jpIsCodePointKana(codePoint) { - return _jpIsCodePointInRanges(codePoint, JP_KANA_RANGES); -} + function isCodePointJapanese(codePoint) { + return isCodePointInRanges(codePoint, JAPANESE_RANGES); + } -function jpIsCodePointJapanese(codePoint) { - return _jpIsCodePointInRanges(codePoint, JP_JAPANESE_RANGES); -} + function isCodePointInRanges(codePoint, ranges) { + for (const [min, max] of ranges) { + if (codePoint >= min && codePoint <= max) { + return true; + } + } + return false; + } -// String testing functions + // String testing functions -function jpIsStringEntirelyKana(str) { - if (str.length === 0) { return false; } - for (const c of str) { - if (!jpIsCodePointKana(c.codePointAt(0))) { - return false; + function isStringEntirelyKana(str) { + if (str.length === 0) { return false; } + for (const c of str) { + if (!isCodePointKana(c.codePointAt(0))) { + return false; + } } + return true; } - return true; -} - -function jpIsStringPartiallyJapanese(str) { - if (str.length === 0) { return false; } - for (const c of str) { - if (jpIsCodePointJapanese(c.codePointAt(0))) { - return true; + + function isStringPartiallyJapanese(str) { + if (str.length === 0) { return false; } + for (const c of str) { + if (isCodePointJapanese(c.codePointAt(0))) { + return true; + } } + return false; } - return false; -} -// Conversion functions + // Conversion functions -function jpKatakanaToHiragana(text) { - let result = ''; - for (const c of text) { - if (wanakana.isKatakana(c)) { - result += wanakana.toHiragana(c); - } else { - result += c; + function convertKatakanaToHiragana(text) { + let result = ''; + for (const c of text) { + if (wanakana.isKatakana(c)) { + result += wanakana.toHiragana(c); + } else { + result += c; + } } - } - return result; -} - -function jpHiraganaToKatakana(text) { - let result = ''; - for (const c of text) { - if (wanakana.isHiragana(c)) { - result += wanakana.toKatakana(c); - } else { - result += c; - } + return result; } - return result; -} - -function jpToRomaji(text) { - return wanakana.toRomaji(text); -} - -function jpConvertReading(expressionFragment, readingFragment, readingMode) { - switch (readingMode) { - case 'hiragana': - return jpKatakanaToHiragana(readingFragment || ''); - case 'katakana': - return jpHiraganaToKatakana(readingFragment || ''); - case 'romaji': - if (readingFragment) { - return jpToRomaji(readingFragment); + function convertHiraganaToKatakana(text) { + let result = ''; + for (const c of text) { + if (wanakana.isHiragana(c)) { + result += wanakana.toKatakana(c); } else { - if (jpIsStringEntirelyKana(expressionFragment)) { - return jpToRomaji(expressionFragment); - } + result += c; } - return readingFragment; - case 'none': - return null; - default: - return readingFragment; + } + + return result; } -} -function jpDistributeFurigana(expression, reading) { - const fallback = [{furigana: reading, text: expression}]; - if (!reading) { - return fallback; + function convertToRomaji(text) { + return wanakana.toRomaji(text); } - let isAmbiguous = false; - const segmentize = (reading2, groups) => { - if (groups.length === 0 || isAmbiguous) { - return []; + function convertReading(expressionFragment, readingFragment, readingMode) { + switch (readingMode) { + case 'hiragana': + return convertKatakanaToHiragana(readingFragment || ''); + case 'katakana': + return convertHiraganaToKatakana(readingFragment || ''); + case 'romaji': + if (readingFragment) { + return convertToRomaji(readingFragment); + } else { + if (isStringEntirelyKana(expressionFragment)) { + return convertToRomaji(expressionFragment); + } + } + return readingFragment; + case 'none': + return null; + default: + return readingFragment; } + } - const group = groups[0]; - if (group.mode === 'kana') { - if (jpKatakanaToHiragana(reading2).startsWith(jpKatakanaToHiragana(group.text))) { - const readingLeft = reading2.substring(group.text.length); - const segs = segmentize(readingLeft, groups.splice(1)); - if (segs) { - return [{text: group.text}].concat(segs); - } + function convertNumericTofullWidth(text) { + let result = ''; + for (const char of text) { + let c = char.codePointAt(0); + if (c >= 0x30 && c <= 0x39) { // ['0', '9'] + c += 0xff10 - 0x30; // 0xff10 = '0' full width + result += String.fromCodePoint(c); + } else { + result += char; } - } else { - let foundSegments = null; - for (let i = reading2.length; i >= group.text.length; --i) { - const readingUsed = reading2.substring(0, i); - const readingLeft = reading2.substring(i); - const segs = segmentize(readingLeft, groups.slice(1)); - if (segs) { - if (foundSegments !== null) { - // more than one way to segmentize the tail, mark as ambiguous - isAmbiguous = true; - return null; - } - foundSegments = [{text: group.text, furigana: readingUsed}].concat(segs); - } - // there is only one way to segmentize the last non-kana group - if (groups.length === 1) { + } + return result; + } + + function convertHalfWidthKanaToFullWidth(text, sourceMapping) { + let result = ''; + const hasSourceMapping = Array.isArray(sourceMapping); + + // This function is safe to use charCodeAt instead of codePointAt, since all + // the relevant characters are represented with a single UTF-16 character code. + for (let i = 0, ii = text.length; i < ii; ++i) { + const c = text[i]; + const mapping = HALFWIDTH_KATAKANA_MAPPING.get(c); + if (typeof mapping !== 'string') { + result += c; + continue; + } + + let index = 0; + switch (text.charCodeAt(i + 1)) { + case 0xff9e: // dakuten + index = 1; + break; + case 0xff9f: // handakuten + index = 2; break; + } + + let c2 = mapping[index]; + if (index > 0) { + if (c2 === '-') { // invalid + index = 0; + c2 = mapping[0]; + } else { + ++i; } } - return foundSegments; - } - }; - const groups = []; - let modePrev = null; - for (const c of expression) { - const codePoint = c.codePointAt(0); - const modeCurr = jpIsCodePointKanji(codePoint) || codePoint === JP_ITERATION_MARK_CHAR_CODE ? 'kanji' : 'kana'; - if (modeCurr === modePrev) { - groups[groups.length - 1].text += c; - } else { - groups.push({mode: modeCurr, text: c}); - modePrev = modeCurr; + if (hasSourceMapping && index > 0) { + index = result.length; + const v = sourceMapping.splice(index + 1, 1)[0]; + sourceMapping[index] += v; + } + result += c2; } - } - - const segments = segmentize(reading, groups); - if (segments && !isAmbiguous) { - return segments; - } - return fallback; -} - -function jpDistributeFuriganaInflected(expression, reading, source) { - const output = []; - - let stemLength = 0; - const shortest = Math.min(source.length, expression.length); - const sourceHiragana = jpKatakanaToHiragana(source); - const expressionHiragana = jpKatakanaToHiragana(expression); - while (stemLength < shortest && sourceHiragana[stemLength] === expressionHiragana[stemLength]) { - ++stemLength; - } - const offset = source.length - stemLength; - - const stemExpression = source.substring(0, source.length - offset); - const stemReading = reading.substring( - 0, - offset === 0 ? reading.length : reading.length - expression.length + stemLength - ); - for (const segment of jpDistributeFurigana(stemExpression, stemReading)) { - output.push(segment); - } - if (stemLength !== source.length) { - output.push({text: source.substring(stemLength)}); + return result; } - return output; -} - -function jpConvertHalfWidthKanaToFullWidth(text, sourceMapping) { - let result = ''; - const hasSourceMapping = Array.isArray(sourceMapping); - - // This function is safe to use charCodeAt instead of codePointAt, since all - // the relevant characters are represented with a single UTF-16 character code. - for (let i = 0, ii = text.length; i < ii; ++i) { - const c = text[i]; - const mapping = JP_HALFWIDTH_KATAKANA_MAPPING.get(c); - if (typeof mapping !== 'string') { - result += c; - continue; - } + function convertAlphabeticToKana(text, sourceMapping) { + let part = ''; + let result = ''; + const ii = text.length; - let index = 0; - switch (text.charCodeAt(i + 1)) { - case 0xff9e: // dakuten - index = 1; - break; - case 0xff9f: // handakuten - index = 2; - break; + if (sourceMapping.length === ii) { + sourceMapping.length = ii; + sourceMapping.fill(1); } - let c2 = mapping[index]; - if (index > 0) { - if (c2 === '-') { // invalid - index = 0; - c2 = mapping[0]; + for (const char of text) { + // Note: 0x61 is the character code for 'a' + let c = char.codePointAt(0); + if (c >= 0x41 && c <= 0x5a) { // ['A', 'Z'] + c += (0x61 - 0x41); + } else if (c >= 0x61 && c <= 0x7a) { // ['a', 'z'] + // NOP; c += (0x61 - 0x61); + } else if (c >= 0xff21 && c <= 0xff3a) { // ['A', 'Z'] fullwidth + c += (0x61 - 0xff21); + } else if (c >= 0xff41 && c <= 0xff5a) { // ['a', 'z'] fullwidth + c += (0x61 - 0xff41); + } else if (c === 0x2d || c === 0xff0d) { // '-' or fullwidth dash + c = 0x2d; // '-' } else { - ++i; + if (part.length > 0) { + result += convertAlphabeticPartToKana(part, sourceMapping, result.length); + part = ''; + } + result += char; + continue; } + part += String.fromCodePoint(c); } - if (hasSourceMapping && index > 0) { - index = result.length; - const v = sourceMapping.splice(index + 1, 1)[0]; - sourceMapping[index] += v; + if (part.length > 0) { + result += convertAlphabeticPartToKana(part, sourceMapping, result.length); } - result += c2; + return result; } - return result; -} - -function jpConvertNumericTofullWidth(text) { - let result = ''; - for (const char of text) { - let c = char.codePointAt(0); - if (c >= 0x30 && c <= 0x39) { // ['0', '9'] - c += 0xff10 - 0x30; // 0xff10 = '0' full width - result += String.fromCodePoint(c); - } else { - result += char; - } - } - return result; -} + function convertAlphabeticPartToKana(text, sourceMapping, sourceMappingStart) { + const result = wanakana.toHiragana(text); + + // Generate source mapping + if (Array.isArray(sourceMapping)) { + if (typeof sourceMappingStart !== 'number') { sourceMappingStart = 0; } + let i = 0; + let resultPos = 0; + const ii = text.length; + while (i < ii) { + // Find smallest matching substring + let iNext = i + 1; + let resultPosNext = result.length; + while (iNext < ii) { + const t = wanakana.toHiragana(text.substring(0, iNext)); + if (t === result.substring(0, t.length)) { + resultPosNext = t.length; + break; + } + ++iNext; + } -function jpConvertAlphabeticToKana(text, sourceMapping) { - let part = ''; - let result = ''; - const ii = text.length; + // Merge characters + const removals = iNext - i - 1; + if (removals > 0) { + let sum = 0; + const vs = sourceMapping.splice(sourceMappingStart + 1, removals); + for (const v of vs) { sum += v; } + sourceMapping[sourceMappingStart] += sum; + } + ++sourceMappingStart; - if (sourceMapping.length === ii) { - sourceMapping.length = ii; - sourceMapping.fill(1); - } + // Empty elements + const additions = resultPosNext - resultPos - 1; + for (let j = 0; j < additions; ++j) { + sourceMapping.splice(sourceMappingStart, 0, 0); + ++sourceMappingStart; + } - for (const char of text) { - // Note: 0x61 is the character code for 'a' - let c = char.codePointAt(0); - if (c >= 0x41 && c <= 0x5a) { // ['A', 'Z'] - c += (0x61 - 0x41); - } else if (c >= 0x61 && c <= 0x7a) { // ['a', 'z'] - // NOP; c += (0x61 - 0x61); - } else if (c >= 0xff21 && c <= 0xff3a) { // ['A', 'Z'] fullwidth - c += (0x61 - 0xff21); - } else if (c >= 0xff41 && c <= 0xff5a) { // ['a', 'z'] fullwidth - c += (0x61 - 0xff41); - } else if (c === 0x2d || c === 0xff0d) { // '-' or fullwidth dash - c = 0x2d; // '-' - } else { - if (part.length > 0) { - result += jpToHiragana(part, sourceMapping, result.length); - part = ''; + i = iNext; + resultPos = resultPosNext; } - result += char; - continue; } - part += String.fromCodePoint(c); - } - if (part.length > 0) { - result += jpToHiragana(part, sourceMapping, result.length); + return result; } - return result; -} -function jpToHiragana(text, sourceMapping, sourceMappingStart) { - const result = wanakana.toHiragana(text); - // Generate source mapping - if (Array.isArray(sourceMapping)) { - if (typeof sourceMappingStart !== 'number') { sourceMappingStart = 0; } - let i = 0; - let resultPos = 0; - const ii = text.length; - while (i < ii) { - // Find smallest matching substring - let iNext = i + 1; - let resultPosNext = result.length; - while (iNext < ii) { - const t = wanakana.toHiragana(text.substring(0, iNext)); - if (t === result.substring(0, t.length)) { - resultPosNext = t.length; - break; - } - ++iNext; - } + // Furigana distribution - // Merge characters - const removals = iNext - i - 1; - if (removals > 0) { - let sum = 0; - const vs = sourceMapping.splice(sourceMappingStart + 1, removals); - for (const v of vs) { sum += v; } - sourceMapping[sourceMappingStart] += sum; + function distributeFurigana(expression, reading) { + const fallback = [{furigana: reading, text: expression}]; + if (!reading) { + return fallback; + } + + let isAmbiguous = false; + const segmentize = (reading2, groups) => { + if (groups.length === 0 || isAmbiguous) { + return []; } - ++sourceMappingStart; - // Empty elements - const additions = resultPosNext - resultPos - 1; - for (let j = 0; j < additions; ++j) { - sourceMapping.splice(sourceMappingStart, 0, 0); - ++sourceMappingStart; + const group = groups[0]; + if (group.mode === 'kana') { + if (convertKatakanaToHiragana(reading2).startsWith(convertKatakanaToHiragana(group.text))) { + const readingLeft = reading2.substring(group.text.length); + const segs = segmentize(readingLeft, groups.splice(1)); + if (segs) { + return [{text: group.text}].concat(segs); + } + } + } else { + let foundSegments = null; + for (let i = reading2.length; i >= group.text.length; --i) { + const readingUsed = reading2.substring(0, i); + const readingLeft = reading2.substring(i); + const segs = segmentize(readingLeft, groups.slice(1)); + if (segs) { + if (foundSegments !== null) { + // more than one way to segmentize the tail, mark as ambiguous + isAmbiguous = true; + return null; + } + foundSegments = [{text: group.text, furigana: readingUsed}].concat(segs); + } + // there is only one way to segmentize the last non-kana group + if (groups.length === 1) { + break; + } + } + return foundSegments; + } + }; + + const groups = []; + let modePrev = null; + for (const c of expression) { + const codePoint = c.codePointAt(0); + const modeCurr = isCodePointKanji(codePoint) || codePoint === ITERATION_MARK_CODE_POINT ? 'kanji' : 'kana'; + if (modeCurr === modePrev) { + groups[groups.length - 1].text += c; + } else { + groups.push({mode: modeCurr, text: c}); + modePrev = modeCurr; } + } + + const segments = segmentize(reading, groups); + if (segments && !isAmbiguous) { + return segments; + } + return fallback; + } + + function distributeFuriganaInflected(expression, reading, source) { + const output = []; + + let stemLength = 0; + const shortest = Math.min(source.length, expression.length); + const sourceHiragana = convertKatakanaToHiragana(source); + const expressionHiragana = convertKatakanaToHiragana(expression); + while (stemLength < shortest && sourceHiragana[stemLength] === expressionHiragana[stemLength]) { + ++stemLength; + } + const offset = source.length - stemLength; + + const stemExpression = source.substring(0, source.length - offset); + const stemReading = reading.substring( + 0, + offset === 0 ? reading.length : reading.length - expression.length + stemLength + ); + for (const segment of distributeFurigana(stemExpression, stemReading)) { + output.push(segment); + } - i = iNext; - resultPos = resultPosNext; + if (stemLength !== source.length) { + output.push({text: source.substring(stemLength)}); } + + return output; } - return result; -} + + // Exports + + return { + isCodePointKanji, + isCodePointKana, + isCodePointJapanese, + isStringEntirelyKana, + isStringPartiallyJapanese, + convertKatakanaToHiragana, + convertHiraganaToKatakana, + convertToRomaji, + convertReading, + convertNumericTofullWidth, + convertHalfWidthKanaToFullWidth, + convertAlphabeticToKana, + distributeFurigana, + distributeFuriganaInflected + }; +})(); diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 25da9bf0..54d046cf 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -29,13 +29,7 @@ * dictTermsMergeBySequence * dictTermsSort * dictTermsUndupe - * jpConvertAlphabeticToKana - * jpConvertHalfWidthKanaToFullWidth - * jpConvertNumericTofullWidth - * jpDistributeFurigana - * jpHiraganaToKatakana - * jpIsCodePointJapanese - * jpKatakanaToHiragana + * jp * requestJson */ @@ -275,7 +269,7 @@ class Translator { const termTags = await this.expandTags(definition.termTags, definition.dictionary); const {expression, reading} = definition; - const furiganaSegments = jpDistributeFurigana(expression, reading); + const furiganaSegments = jp.distributeFurigana(expression, reading); definitions.push({ source: deinflection.source, @@ -376,20 +370,20 @@ class Translator { let sourceMapping = null; if (halfWidth) { if (sourceMapping === null) { sourceMapping = Translator.createTextSourceMapping(text2); } - text2 = jpConvertHalfWidthKanaToFullWidth(text2, sourceMapping); + text2 = jp.convertHalfWidthKanaToFullWidth(text2, sourceMapping); } if (numeric) { - text2 = jpConvertNumericTofullWidth(text2); + text2 = jp.convertNumericTofullWidth(text2); } if (alphabetic) { if (sourceMapping === null) { sourceMapping = Translator.createTextSourceMapping(text2); } - text2 = jpConvertAlphabeticToKana(text2, sourceMapping); + text2 = jp.convertAlphabeticToKana(text2, sourceMapping); } if (katakana) { - text2 = jpHiraganaToKatakana(text2); + text2 = jp.convertHiraganaToKatakana(text2); } if (hiragana) { - text2 = jpKatakanaToHiragana(text2); + text2 = jp.convertKatakanaToHiragana(text2); } for (let i = text2.length; i > 0; --i) { @@ -590,7 +584,7 @@ class Translator { } static createExpression(expression, reading, termTags=null, termFrequency=null) { - const furiganaSegments = jpDistributeFurigana(expression, reading); + const furiganaSegments = jp.distributeFurigana(expression, reading); return { expression, reading, @@ -639,7 +633,7 @@ class Translator { if (!options.scanning.alphanumeric) { let newText = ''; for (const c of text) { - if (!jpIsCodePointJapanese(c.codePointAt(0))) { + if (!jp.isCodePointJapanese(c.codePointAt(0))) { break; } newText += c; -- cgit v1.2.3 From 264820f2087e7dee13e358ba703d3dd863ed7faa Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 14 Mar 2020 16:11:07 -0400 Subject: Add more unicode code point ranges --- ext/bg/js/japanese.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js index 182d5b98..4c2df674 100644 --- a/ext/bg/js/japanese.js +++ b/ext/bg/js/japanese.js @@ -84,9 +84,24 @@ const jp = (() => { const KATAKANA_RANGE = [0x30a0, 0x30ff]; const KANA_RANGES = [HIRAGANA_RANGE, KATAKANA_RANGE]; - const CJK_COMMON_RANGE = [0x4e00, 0x9fff]; - const CJK_RARE_RANGE = [0x3400, 0x4dbf]; - const CJK_RANGES = [CJK_COMMON_RANGE, CJK_RARE_RANGE]; + const CJK_UNIFIED_IDEOGRAPHS_RANGE = [0x4e00, 0x9fff]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A_RANGE = [0x3400, 0x4dbf]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B_RANGE = [0x20000, 0x2a6df]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C_RANGE = [0x2a700, 0x2b73f]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D_RANGE = [0x2b740, 0x2b81f]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E_RANGE = [0x2b820, 0x2ceaf]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F_RANGE = [0x2ceb0, 0x2ebef]; + const CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT_RANGE = [0x2f800, 0x2fa1f]; + const CJK_UNIFIED_IDEOGRAPHS_RANGES = [ + CJK_UNIFIED_IDEOGRAPHS_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F_RANGE, + CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT_RANGE + ]; const ITERATION_MARK_CODE_POINT = 0x3005; @@ -95,8 +110,7 @@ const jp = (() => { HIRAGANA_RANGE, KATAKANA_RANGE, - CJK_COMMON_RANGE, - CJK_RARE_RANGE, + ...CJK_UNIFIED_IDEOGRAPHS_RANGES, [0xff66, 0xff9f], // Halfwidth katakana @@ -119,7 +133,7 @@ const jp = (() => { // Character code testing functions function isCodePointKanji(codePoint) { - return isCodePointInRanges(codePoint, CJK_RANGES); + return isCodePointInRanges(codePoint, CJK_UNIFIED_IDEOGRAPHS_RANGES); } function isCodePointKana(codePoint) { -- cgit v1.2.3 From 248a18dd72c687a470246c26d5c74e440058bf55 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 14 Mar 2020 16:38:12 -0400 Subject: Fix case issue --- ext/bg/js/japanese.js | 4 ++-- ext/bg/js/translator.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js index 4c2df674..fa40fc98 100644 --- a/ext/bg/js/japanese.js +++ b/ext/bg/js/japanese.js @@ -231,7 +231,7 @@ const jp = (() => { } } - function convertNumericTofullWidth(text) { + function convertNumericToFullWidth(text) { let result = ''; for (const char of text) { let c = char.codePointAt(0); @@ -485,7 +485,7 @@ const jp = (() => { convertHiraganaToKatakana, convertToRomaji, convertReading, - convertNumericTofullWidth, + convertNumericToFullWidth, convertHalfWidthKanaToFullWidth, convertAlphabeticToKana, distributeFurigana, diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 54d046cf..6f43f7b0 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -373,7 +373,7 @@ class Translator { text2 = jp.convertHalfWidthKanaToFullWidth(text2, sourceMapping); } if (numeric) { - text2 = jp.convertNumericTofullWidth(text2); + text2 = jp.convertNumericToFullWidth(text2); } if (alphabetic) { if (sourceMapping === null) { sourceMapping = Translator.createTextSourceMapping(text2); } -- cgit v1.2.3 From a50b76fd219b873df7bb7e3b6a1b03850c59f239 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 14 Mar 2020 17:10:53 -0400 Subject: Remove unnecessary sourceMapping population in convertAlphabeticToKana --- ext/bg/js/japanese.js | 6 ------ 1 file changed, 6 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js index fa40fc98..d2a577e6 100644 --- a/ext/bg/js/japanese.js +++ b/ext/bg/js/japanese.js @@ -293,12 +293,6 @@ const jp = (() => { function convertAlphabeticToKana(text, sourceMapping) { let part = ''; let result = ''; - const ii = text.length; - - if (sourceMapping.length === ii) { - sourceMapping.length = ii; - sourceMapping.fill(1); - } for (const char of text) { // Note: 0x61 is the character code for 'a' -- cgit v1.2.3 From 77a2cc60e9a4a89da354cadb1bf060204ee3b951 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 21 Mar 2020 13:18:34 -0400 Subject: Move basic string/character testing functions into a mixed/js/japanese.js --- ext/bg/background.html | 1 + ext/bg/js/japanese.js | 106 +++------------------------------------- ext/bg/search.html | 1 + ext/bg/settings.html | 1 + ext/mixed/js/japanese.js | 124 +++++++++++++++++++++++++++++++++++++++++++++++ test/test-japanese.js | 1 + 6 files changed, 135 insertions(+), 99 deletions(-) create mode 100644 ext/mixed/js/japanese.js (limited to 'ext/bg') diff --git a/ext/bg/background.html b/ext/bg/background.html index 44abe8fd..f7cf6e55 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -20,6 +20,7 @@ + diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js index d2a577e6..c5873cf1 100644 --- a/ext/bg/js/japanese.js +++ b/ext/bg/js/japanese.js @@ -17,10 +17,11 @@ */ /* global + * jp * wanakana */ -const jp = (() => { +(() => { const HALFWIDTH_KATAKANA_MAPPING = new Map([ ['ヲ', 'ヲヺ-'], ['ァ', 'ァ--'], @@ -80,101 +81,13 @@ const jp = (() => { ['ン', 'ン--'] ]); - const HIRAGANA_RANGE = [0x3040, 0x309f]; - const KATAKANA_RANGE = [0x30a0, 0x30ff]; - const KANA_RANGES = [HIRAGANA_RANGE, KATAKANA_RANGE]; - - const CJK_UNIFIED_IDEOGRAPHS_RANGE = [0x4e00, 0x9fff]; - const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A_RANGE = [0x3400, 0x4dbf]; - const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B_RANGE = [0x20000, 0x2a6df]; - const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C_RANGE = [0x2a700, 0x2b73f]; - const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D_RANGE = [0x2b740, 0x2b81f]; - const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E_RANGE = [0x2b820, 0x2ceaf]; - const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F_RANGE = [0x2ceb0, 0x2ebef]; - const CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT_RANGE = [0x2f800, 0x2fa1f]; - const CJK_UNIFIED_IDEOGRAPHS_RANGES = [ - CJK_UNIFIED_IDEOGRAPHS_RANGE, - CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A_RANGE, - CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B_RANGE, - CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C_RANGE, - CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D_RANGE, - CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E_RANGE, - CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F_RANGE, - CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT_RANGE - ]; - const ITERATION_MARK_CODE_POINT = 0x3005; - // Japanese character ranges, roughly ordered in order of expected frequency - const JAPANESE_RANGES = [ - HIRAGANA_RANGE, - KATAKANA_RANGE, - - ...CJK_UNIFIED_IDEOGRAPHS_RANGES, - - [0xff66, 0xff9f], // Halfwidth katakana - - [0x30fb, 0x30fc], // Katakana punctuation - [0xff61, 0xff65], // Kana punctuation - [0x3000, 0x303f], // CJK punctuation - - [0xff10, 0xff19], // Fullwidth numbers - [0xff21, 0xff3a], // Fullwidth upper case Latin letters - [0xff41, 0xff5a], // Fullwidth lower case Latin letters - - [0xff01, 0xff0f], // Fullwidth punctuation 1 - [0xff1a, 0xff1f], // Fullwidth punctuation 2 - [0xff3b, 0xff3f], // Fullwidth punctuation 3 - [0xff5b, 0xff60], // Fullwidth punctuation 4 - [0xffe0, 0xffee] // Currency markers - ]; - - - // Character code testing functions - - function isCodePointKanji(codePoint) { - return isCodePointInRanges(codePoint, CJK_UNIFIED_IDEOGRAPHS_RANGES); - } - - function isCodePointKana(codePoint) { - return isCodePointInRanges(codePoint, KANA_RANGES); - } - - function isCodePointJapanese(codePoint) { - return isCodePointInRanges(codePoint, JAPANESE_RANGES); - } - function isCodePointInRanges(codePoint, ranges) { - for (const [min, max] of ranges) { - if (codePoint >= min && codePoint <= max) { - return true; - } - } - return false; - } + // Existing functions - - // String testing functions - - function isStringEntirelyKana(str) { - if (str.length === 0) { return false; } - for (const c of str) { - if (!isCodePointKana(c.codePointAt(0))) { - return false; - } - } - return true; - } - - function isStringPartiallyJapanese(str) { - if (str.length === 0) { return false; } - for (const c of str) { - if (isCodePointJapanese(c.codePointAt(0))) { - return true; - } - } - return false; - } + const isCodePointKanji = jp.isCodePointKanji; + const isStringEntirelyKana = jp.isStringEntirelyKana; // Conversion functions @@ -469,12 +382,7 @@ const jp = (() => { // Exports - return { - isCodePointKanji, - isCodePointKana, - isCodePointJapanese, - isStringEntirelyKana, - isStringPartiallyJapanese, + Object.assign(jp, { convertKatakanaToHiragana, convertHiraganaToKatakana, convertToRomaji, @@ -484,5 +392,5 @@ const jp = (() => { convertAlphabeticToKana, distributeFurigana, distributeFuriganaInflected - }; + }); })(); diff --git a/ext/bg/search.html b/ext/bg/search.html index f4c1a737..eacc1893 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -74,6 +74,7 @@ + diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 0db76d71..cfe20be4 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1088,6 +1088,7 @@ + diff --git a/ext/mixed/js/japanese.js b/ext/mixed/js/japanese.js new file mode 100644 index 00000000..61a247b2 --- /dev/null +++ b/ext/mixed/js/japanese.js @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2020 Alex Yatskov + * Author: Alex Yatskov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const jp = (() => { + const HIRAGANA_RANGE = [0x3040, 0x309f]; + const KATAKANA_RANGE = [0x30a0, 0x30ff]; + const KANA_RANGES = [HIRAGANA_RANGE, KATAKANA_RANGE]; + + const CJK_UNIFIED_IDEOGRAPHS_RANGE = [0x4e00, 0x9fff]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A_RANGE = [0x3400, 0x4dbf]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B_RANGE = [0x20000, 0x2a6df]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C_RANGE = [0x2a700, 0x2b73f]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D_RANGE = [0x2b740, 0x2b81f]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E_RANGE = [0x2b820, 0x2ceaf]; + const CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F_RANGE = [0x2ceb0, 0x2ebef]; + const CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT_RANGE = [0x2f800, 0x2fa1f]; + const CJK_UNIFIED_IDEOGRAPHS_RANGES = [ + CJK_UNIFIED_IDEOGRAPHS_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E_RANGE, + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F_RANGE, + CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT_RANGE + ]; + + // Japanese character ranges, roughly ordered in order of expected frequency + const JAPANESE_RANGES = [ + HIRAGANA_RANGE, + KATAKANA_RANGE, + + ...CJK_UNIFIED_IDEOGRAPHS_RANGES, + + [0xff66, 0xff9f], // Halfwidth katakana + + [0x30fb, 0x30fc], // Katakana punctuation + [0xff61, 0xff65], // Kana punctuation + [0x3000, 0x303f], // CJK punctuation + + [0xff10, 0xff19], // Fullwidth numbers + [0xff21, 0xff3a], // Fullwidth upper case Latin letters + [0xff41, 0xff5a], // Fullwidth lower case Latin letters + + [0xff01, 0xff0f], // Fullwidth punctuation 1 + [0xff1a, 0xff1f], // Fullwidth punctuation 2 + [0xff3b, 0xff3f], // Fullwidth punctuation 3 + [0xff5b, 0xff60], // Fullwidth punctuation 4 + [0xffe0, 0xffee] // Currency markers + ]; + + + // Character code testing functions + + function isCodePointKanji(codePoint) { + return isCodePointInRanges(codePoint, CJK_UNIFIED_IDEOGRAPHS_RANGES); + } + + function isCodePointKana(codePoint) { + return isCodePointInRanges(codePoint, KANA_RANGES); + } + + function isCodePointJapanese(codePoint) { + return isCodePointInRanges(codePoint, JAPANESE_RANGES); + } + + function isCodePointInRanges(codePoint, ranges) { + for (const [min, max] of ranges) { + if (codePoint >= min && codePoint <= max) { + return true; + } + } + return false; + } + + + // String testing functions + + function isStringEntirelyKana(str) { + if (str.length === 0) { return false; } + for (const c of str) { + if (!isCodePointKana(c.codePointAt(0))) { + return false; + } + } + return true; + } + + function isStringPartiallyJapanese(str) { + if (str.length === 0) { return false; } + for (const c of str) { + if (isCodePointJapanese(c.codePointAt(0))) { + return true; + } + } + return false; + } + + + // Exports + + return { + isCodePointKanji, + isCodePointKana, + isCodePointJapanese, + isStringEntirelyKana, + isStringPartiallyJapanese + }; +})(); diff --git a/test/test-japanese.js b/test/test-japanese.js index 78f63c0b..32e4d176 100644 --- a/test/test-japanese.js +++ b/test/test-japanese.js @@ -22,6 +22,7 @@ const {VM} = require('./yomichan-vm'); const vm = new VM(); vm.execute([ 'mixed/lib/wanakana.min.js', + 'mixed/js/japanese.js', 'bg/js/japanese.js' ]); const jp = vm.get('jp'); -- cgit v1.2.3 From 962c2a381f3dace4d97fd0625504ec841e378354 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Fri, 13 Mar 2020 23:23:08 +0200 Subject: apply all options on profile change --- ext/bg/js/search-frontend.js | 7 +------ ext/bg/js/search-query-parser.js | 17 ++++++----------- ext/bg/js/search.js | 16 +++++++--------- ext/fg/js/float.js | 17 ++++++++--------- ext/fg/js/frontend-initialize.js | 4 ++-- ext/fg/js/frontend.js | 18 ++++++++++++++++-- ext/fg/js/popup-nested.js | 7 +------ ext/fg/js/popup-proxy.js | 4 ++++ ext/fg/js/popup.js | 34 +++++++++++++++------------------- ext/mixed/js/display.js | 18 +++++++++--------- ext/mixed/js/text-scanner.js | 7 +++++-- 11 files changed, 74 insertions(+), 75 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js index a470e873..2d2aa8d4 100644 --- a/ext/bg/js/search-frontend.js +++ b/ext/bg/js/search-frontend.js @@ -30,12 +30,7 @@ async function searchFrontendSetup() { const options = await apiOptionsGet(optionsContext); if (!options.scanning.enableOnSearchPage) { return; } - const ignoreNodes = ['.scan-disable', '.scan-disable *']; - if (!options.scanning.enableOnPopupExpressions) { - ignoreNodes.push('.source-text', '.source-text *'); - } - - window.frontendInitializationData = {depth: 1, ignoreNodes, proxy: false}; + window.frontendInitializationData = {depth: 1, proxy: false}; const scriptSrcs = [ '/mixed/js/text-scanner.js', diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 06316ce2..6e18073b 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -28,11 +28,10 @@ class QueryParser extends TextScanner { constructor(search) { - super(document.querySelector('#query-parser-content'), [], [], []); + super(document.querySelector('#query-parser-content'), [], []); this.search = search; this.parseResults = []; - this.selectedParser = null; this.queryParser = document.querySelector('#query-parser-content'); this.queryParserSelect = document.querySelector('#query-parser-select-container'); @@ -79,9 +78,7 @@ class QueryParser extends TextScanner { onParserChange(e) { const selectedParser = e.target.value; - this.selectedParser = selectedParser; apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext()); - this.renderParseResult(); } getMouseEventListeners() { @@ -112,19 +109,16 @@ class QueryParser extends TextScanner { refreshSelectedParser() { if (this.parseResults.length > 0) { - if (this.selectedParser === null) { - this.selectedParser = this.search.options.parsing.selectedParser; - } - if (this.selectedParser === null || !this.getParseResult()) { + if (!this.getParseResult()) { const selectedParser = this.parseResults[0].id; - this.selectedParser = selectedParser; apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext()); } } } getParseResult() { - return this.parseResults.find((r) => r.id === this.selectedParser); + const {selectedParser} = this.options.parsing; + return this.parseResults.find((r) => r.id === selectedParser); } async setText(text) { @@ -176,7 +170,8 @@ class QueryParser extends TextScanner { renderParserSelect() { this.queryParserSelect.textContent = ''; if (this.parseResults.length > 1) { - const select = this.queryParserGenerator.createParserSelect(this.parseResults, this.selectedParser); + const {selectedParser} = this.options.parsing; + const select = this.queryParserGenerator.createParserSelect(this.parseResults, selectedParser); select.addEventListener('change', this.onParserChange.bind(this)); this.queryParserSelect.appendChild(select); } diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index e2bdff73..8b8ee55e 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -247,15 +247,12 @@ class DisplaySearch extends Display { } onWanakanaEnableChange(e) { - const {queryParams: {query=''}} = parseUrl(window.location.href); const enableWanakana = e.target.checked; if (enableWanakana) { window.wanakana.bind(this.query); } else { window.wanakana.unbind(this.query); } - this.setQuery(query); - this.onSearchQueryUpdated(this.query.value, false); apiOptionsSet({general: {enableWanakana}}, this.getOptionsContext()); } @@ -278,19 +275,20 @@ class DisplaySearch extends Display { } } - async updateOptions(options) { - await super.updateOptions(options); + async updateOptions() { + await super.updateOptions(); this.queryParser.setOptions(this.options); + const query = this.query.value; + if (query) { + this.setQuery(query); + this.onSearchQueryUpdated(query, false); + } } isWanakanaEnabled() { return this.wanakanaEnable !== null && this.wanakanaEnable.checked; } - getOptionsContext() { - return this.optionsContext; - } - setQuery(query) { const interpretedQuery = this.isWanakanaEnabled() ? window.wanakana.toKana(query) : query; this.query.value = interpretedQuery; diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 393c2719..9b720ebe 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -28,6 +28,8 @@ class DisplayFloat extends Display { super(document.querySelector('#spinner'), document.querySelector('#definitions')); this.autoPlayAudioTimer = null; + this._popupId = null; + this.optionsContext = { depth: 0, url: window.location.href @@ -53,7 +55,7 @@ class DisplayFloat extends Display { ['setContent', ({type, details}) => this.setContent(type, details)], ['clearAutoPlayTimer', () => this.clearAutoPlayTimer()], ['setCustomCss', ({css}) => this.setCustomCss(css)], - ['prepare', ({options, popupInfo, url, childrenSupported, scale, uniqueId}) => this.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)], + ['prepare', ({popupInfo, url, childrenSupported, scale}) => this.prepare(popupInfo, url, childrenSupported, scale)], ['setContentScale', ({scale}) => this.setContentScale(scale)] ]); @@ -61,23 +63,24 @@ class DisplayFloat extends Display { window.addEventListener('message', this.onMessage.bind(this), false); } - async prepare(options, popupInfo, url, childrenSupported, scale, uniqueId) { + async prepare(popupInfo, url, childrenSupported, scale) { if (this._prepareInvoked) { return; } this._prepareInvoked = true; - await super.prepare(options); - const {id, depth, parentFrameId} = popupInfo; + this._popupId = id; this.optionsContext.depth = depth; this.optionsContext.url = url; + await super.prepare(); + if (childrenSupported) { popupNestedInitialize(id, depth, parentFrameId, url); } this.setContentScale(scale); - apiForward('popupPrepareCompleted', {uniqueId}); + apiForward('popupPrepareCompleted', {targetPopupId: this._popupId}); } onError(error) { @@ -144,10 +147,6 @@ class DisplayFloat extends Display { handler(params); } - getOptionsContext() { - return this.optionsContext; - } - autoPlayAudio() { this.clearAutoPlayTimer(); this.autoPlayAudioTimer = window.setTimeout(() => super.autoPlayAudio(), 400); diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 8424b21d..3a191247 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -26,7 +26,7 @@ async function main() { await yomichan.prepare(); const data = window.frontendInitializationData || {}; - const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data; + const {id, depth=0, parentFrameId, url, proxy=false} = data; let popup; if (proxy) { @@ -38,7 +38,7 @@ async function main() { popup = popupHost.getOrCreatePopup(null, null, depth); } - const frontend = new Frontend(popup, ignoreNodes); + const frontend = new Frontend(popup); await frontend.prepare(); } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 768b9326..d7bc02cc 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -26,10 +26,9 @@ */ class Frontend extends TextScanner { - constructor(popup, ignoreNodes) { + constructor(popup) { super( window, - ignoreNodes, popup.isProxy() ? [] : [popup.getContainer()], [(x, y) => this.popup.containsPoint(x, y)] ); @@ -95,6 +94,9 @@ class Frontend extends TextScanner { } onRuntimeMessage({action, params}, sender, callback) { + const {targetPopupId} = params || {}; + if (targetPopupId !== 'all' && targetPopupId !== this.popup.id) { return; } + const handler = this._runtimeMessageHandlers.get(action); if (typeof handler !== 'function') { return false; } @@ -129,8 +131,20 @@ class Frontend extends TextScanner { async updateOptions() { this.setOptions(await apiOptionsGet(this.getOptionsContext())); + + const ignoreNodes = ['.scan-disable', '.scan-disable *']; + if (!this.options.scanning.enableOnPopupExpressions) { + ignoreNodes.push('.source-text', '.source-text *'); + } + this.ignoreNodes = ignoreNodes.join(','); + await this.popup.setOptions(this.options); + this._updateContentScale(); + + if (this.textSourceCurrent !== null && this.causeCurrent !== null) { + await this.onSearchSource(this.textSourceCurrent, this.causeCurrent); + } } async onSearchSource(textSource, cause) { diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index 06f8fc4b..39d91fd8 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -36,12 +36,7 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) { return; } - const ignoreNodes = ['.scan-disable', '.scan-disable *']; - if (!options.scanning.enableOnPopupExpressions) { - ignoreNodes.push('.source-text', '.source-text *'); - } - - window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url, proxy: true}; + window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true}; const scriptSrcs = [ '/mixed/js/text-scanner.js', diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index f7cef214..997b1317 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -33,6 +33,10 @@ class PopupProxy { // Public properties + get id() { + return this._id; + } + get parent() { return null; } diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index d752812e..e6e93a76 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -210,11 +210,9 @@ class Popup { const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null); this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); this._container.addEventListener('load', () => { - const uniqueId = yomichan.generateId(32); - Popup._listenForDisplayPrepareCompleted(uniqueId, resolve); + this._listenForDisplayPrepareCompleted(resolve); this._invokeApi('prepare', { - options: this._options, popupInfo: { id: this._id, depth: this._depth, @@ -222,8 +220,7 @@ class Popup { }, url: this.url, childrenSupported: this._childrenSupported, - scale: this._contentScale, - uniqueId + scale: this._contentScale }); }); this._observeFullscreen(true); @@ -364,23 +361,12 @@ class Popup { contentWindow.postMessage({action, params, token}, this._targetOrigin); } - static _getFullscreenElement() { - return ( - document.fullscreenElement || - document.msFullscreenElement || - document.mozFullScreenElement || - document.webkitFullscreenElement || - null - ); - } - - static _listenForDisplayPrepareCompleted(uniqueId, resolve) { + _listenForDisplayPrepareCompleted(resolve) { const runtimeMessageCallback = ({action, params}, sender, callback) => { if ( action === 'popupPrepareCompleted' && - typeof params === 'object' && - params !== null && - params.uniqueId === uniqueId + isObject(params) && + params.targetPopupId === this._id ) { chrome.runtime.onMessage.removeListener(runtimeMessageCallback); callback(); @@ -391,6 +377,16 @@ class Popup { chrome.runtime.onMessage.addListener(runtimeMessageCallback); } + static _getFullscreenElement() { + return ( + document.fullscreenElement || + document.msFullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + null + ); + } + static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 515e28a7..9a7a91f3 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -40,6 +40,7 @@ class Display { this.spinner = spinner; this.container = container; this.definitions = []; + this.optionsContext = null; this.options = null; this.context = null; this.index = 0; @@ -165,12 +166,11 @@ class Display { this.setInteractive(true); } - async prepare(options=null) { + async prepare() { await yomichan.prepare(); - const displayGeneratorPromise = this.displayGenerator.prepare(); - const updateOptionsPromise = this.updateOptions(options); - await Promise.all([displayGeneratorPromise, updateOptionsPromise]); - yomichan.on('optionsUpdated', () => this.updateOptions(null)); + await this.displayGenerator.prepare(); + await this.updateOptions(); + yomichan.on('optionsUpdated', () => this.updateOptions()); } onError(_error) { @@ -369,11 +369,11 @@ class Display { } getOptionsContext() { - throw new Error('Override me'); + return this.optionsContext; } - async updateOptions(options) { - this.options = options ? options : await apiOptionsGet(this.getOptionsContext()); + async updateOptions() { + this.options = await apiOptionsGet(this.getOptionsContext()); this.updateDocumentOptions(this.options); this.updateTheme(this.options.general.popupTheme); this.setCustomCss(this.options.general.customPopupCss); @@ -851,7 +851,7 @@ class Display { } setPopupVisibleOverride(visible) { - return apiForward('popupSetVisibleOverride', {visible}); + return apiForward('popupSetVisibleOverride', {visible, targetPopupId: 'all'}); } setSpinnerVisible(visible) { diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index a08e09fb..b8156c01 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -23,13 +23,15 @@ */ class TextScanner { - constructor(node, ignoreNodes, ignoreElements, ignorePoints) { + constructor(node, ignoreElements, ignorePoints) { this.node = node; - this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null); this.ignoreElements = ignoreElements; this.ignorePoints = ignorePoints; + this.ignoreNodes = null; + this.scanTimerPromise = null; + this.causeCurrent = null; this.textSourceCurrent = null; this.pendingLookup = false; this.options = null; @@ -298,6 +300,7 @@ class TextScanner { this.pendingLookup = true; const result = await this.onSearchSource(textSource, cause); if (result !== null) { + this.causeCurrent = cause; this.textSourceCurrent = textSource; if (this.options.scanning.selectText) { textSource.select(); -- cgit v1.2.3 From 46c6ad98f33ea1536452beb7e41f78f9a1895997 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 14 Mar 2020 02:51:39 +0200 Subject: use dependency injection in QueryParser Also fix an issue with settings update triggering a lookup on unprepared QueryParser. --- ext/bg/js/search-query-parser.js | 43 +++++++++++++++++++++++++++------------- ext/bg/js/search.js | 11 +++++++++- 2 files changed, 39 insertions(+), 15 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 6e18073b..4a4fcdde 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -27,9 +27,12 @@ */ class QueryParser extends TextScanner { - constructor(search) { + constructor({getOptionsContext, setContent, setSpinnerVisible}) { super(document.querySelector('#query-parser-content'), [], []); - this.search = search; + + this.getOptionsContext = getOptionsContext; + this.setContent = setContent; + this.setSpinnerVisible = setSpinnerVisible; this.parseResults = []; @@ -55,18 +58,18 @@ class QueryParser extends TextScanner { async onSearchSource(textSource, cause) { if (textSource === null) { return null; } - this.setTextSourceScanLength(textSource, this.search.options.scanning.length); + this.setTextSourceScanLength(textSource, this.options.scanning.length); const searchText = textSource.text(); if (searchText.length === 0) { return; } - const {definitions, length} = await apiTermsFind(searchText, {}, this.search.getOptionsContext()); + const {definitions, length} = await apiTermsFind(searchText, {}, this.getOptionsContext()); if (definitions.length === 0) { return null; } - const sentence = docSentenceExtract(textSource, this.search.options.anki.sentenceExt); + const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); textSource.setEndOffset(length); - this.search.setContent('terms', {definitions, context: { + this.setContent('terms', {definitions, context: { focus: false, disableHistory: cause === 'mouse', sentence, @@ -78,7 +81,7 @@ class QueryParser extends TextScanner { onParserChange(e) { const selectedParser = e.target.value; - apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext()); + apiOptionsSet({parsing: {selectedParser}}, this.getOptionsContext()); } getMouseEventListeners() { @@ -107,11 +110,23 @@ class QueryParser extends TextScanner { this.queryParser.dataset.termSpacing = `${options.parsing.termSpacing}`; } + getOptionsContext() { + throw new Error('Override me'); + } + + setContent(_type, _details) { + throw new Error('Override me'); + } + + setSpinnerVisible(_visible) { + throw new Error('Override me'); + } + refreshSelectedParser() { if (this.parseResults.length > 0) { if (!this.getParseResult()) { const selectedParser = this.parseResults[0].id; - apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext()); + apiOptionsSet({parsing: {selectedParser}}, this.getOptionsContext()); } } } @@ -122,7 +137,7 @@ class QueryParser extends TextScanner { } async setText(text) { - this.search.setSpinnerVisible(true); + this.setSpinnerVisible(true); this.setPreview(text); @@ -132,20 +147,20 @@ class QueryParser extends TextScanner { this.renderParserSelect(); this.renderParseResult(); - this.search.setSpinnerVisible(false); + this.setSpinnerVisible(false); } async parseText(text) { const results = []; - if (this.search.options.parsing.enableScanningParser) { + if (this.options.parsing.enableScanningParser) { results.push({ name: 'Scanning parser', id: 'scan', - parsedText: await apiTextParse(text, this.search.getOptionsContext()) + parsedText: await apiTextParse(text, this.getOptionsContext()) }); } - if (this.search.options.parsing.enableMecabParser) { - const mecabResults = await apiTextParseMecab(text, this.search.getOptionsContext()); + if (this.options.parsing.enableMecabParser) { + const mecabResults = await apiTextParseMecab(text, this.getOptionsContext()); for (const [mecabDictName, mecabDictResults] of mecabResults) { results.push({ name: `MeCab: ${mecabDictName}`, diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 8b8ee55e..9250fdde 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -29,12 +29,18 @@ class DisplaySearch extends Display { constructor() { super(document.querySelector('#spinner'), document.querySelector('#content')); + this._isPrepared = false; + this.optionsContext = { depth: 0, url: window.location.href }; - this.queryParser = new QueryParser(this); + this.queryParser = new QueryParser({ + getOptionsContext: this.getOptionsContext.bind(this), + setContent: this.setContent.bind(this), + setSpinnerVisible: this.setSpinnerVisible.bind(this) + }); this.search = document.querySelector('#search'); this.query = document.querySelector('#query'); @@ -112,6 +118,8 @@ class DisplaySearch extends Display { this.clipboardMonitor.on('change', this.onExternalSearchUpdate.bind(this)); this.updateSearchButton(); + + this._isPrepared = true; } catch (e) { this.onError(e); } @@ -278,6 +286,7 @@ class DisplaySearch extends Display { async updateOptions() { await super.updateOptions(); this.queryParser.setOptions(this.options); + if (!this._isPrepared) { return; } const query = this.query.value; if (query) { this.setQuery(query); -- cgit v1.2.3 From 2c4fd648dbc37d3d5e10acfe2db054d7cc876a63 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 14 Mar 2020 13:21:05 +0200 Subject: remove stubs --- ext/bg/js/search-query-parser.js | 12 ------------ 1 file changed, 12 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 4a4fcdde..9f59f2e5 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -110,18 +110,6 @@ class QueryParser extends TextScanner { this.queryParser.dataset.termSpacing = `${options.parsing.termSpacing}`; } - getOptionsContext() { - throw new Error('Override me'); - } - - setContent(_type, _details) { - throw new Error('Override me'); - } - - setSpinnerVisible(_visible) { - throw new Error('Override me'); - } - refreshSelectedParser() { if (this.parseResults.length > 0) { if (!this.getParseResult()) { -- cgit v1.2.3 From 93f7278586f7b943ae49c00cd14559a2f4b99561 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 23 Feb 2020 14:03:37 -0500 Subject: Update dictionary schema to support pitch accent data --- .../data/dictionary-term-meta-bank-v3-schema.json | 64 +++++++++++++++++++++- .../dictionaries/valid-dictionary1/tag_bank_3.json | 4 ++ .../valid-dictionary1/term_meta_bank_1.json | 36 +++++++++++- test/test-database.js | 9 +-- 4 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 test/data/dictionaries/valid-dictionary1/tag_bank_3.json (limited to 'ext/bg') diff --git a/ext/bg/data/dictionary-term-meta-bank-v3-schema.json b/ext/bg/data/dictionary-term-meta-bank-v3-schema.json index 1cc0557f..8475db81 100644 --- a/ext/bg/data/dictionary-term-meta-bank-v3-schema.json +++ b/ext/bg/data/dictionary-term-meta-bank-v3-schema.json @@ -13,13 +13,71 @@ }, { "type": "string", - "enum": ["freq"], - "description": "Type of data. \"freq\" corresponds to frequency information." + "enum": ["freq", "pitch"], + "description": "Type of data. \"freq\" corresponds to frequency information; \"pitch\" corresponds to pitch information." }, { - "type": ["string", "number"], "description": "Data for the term/expression." } + ], + "oneOf": [ + { + "items": [ + {}, + {"enum": ["freq"]}, + { + "type": ["string", "number"], + "description": "Frequency information for the term or expression." + } + ] + }, + { + "items": [ + {}, + {"enum": ["pitch"]}, + { + "type": ["object"], + "description": "Pitch accent information for the term or expression.", + "required": [ + "reading", + "pitches" + ], + "additionalProperties": false, + "properties": { + "reading": { + "type": "string", + "description": "Reading for the term or expression." + }, + "pitches": { + "type": "array", + "description": "List of different pitch accent information for the term and reading combination.", + "additionalItems": { + "type": "object", + "required": [ + "position" + ], + "additionalProperties": false, + "properties": { + "position": { + "type": "integer", + "description": "Mora position of the pitch accent downstep. A value of 0 indicates that the word does not have a downstep (heiban).", + "minimum": 0 + }, + "tags": { + "type": "array", + "description": "List of tags for this pitch accent.", + "items": { + "type": "string", + "description": "Tag for this pitch accent. This typically corresponds to a certain type of part of speech." + } + } + } + } + } + } + } + ] + } ] } } \ No newline at end of file diff --git a/test/data/dictionaries/valid-dictionary1/tag_bank_3.json b/test/data/dictionaries/valid-dictionary1/tag_bank_3.json new file mode 100644 index 00000000..572221fe --- /dev/null +++ b/test/data/dictionaries/valid-dictionary1/tag_bank_3.json @@ -0,0 +1,4 @@ +[ + ["ptag1", "pcategory1", 0, "ptag1 notes", 0], + ["ptag2", "pcategory2", 0, "ptag2 notes", 0] +] \ No newline at end of file diff --git a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json index 78096502..26922394 100644 --- a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json +++ b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json @@ -1,5 +1,39 @@ [ ["打", "freq", 1], ["打つ", "freq", 2], - ["打ち込む", "freq", 3] + ["打ち込む", "freq", 3], + [ + "打ち込む", + "pitch", + { + "reading": "うちこむ", + "pitches": [ + {"position": 0}, + {"position": 3} + ] + } + ], + [ + "打ち込む", + "pitch", + { + "reading": "ぶちこむ", + "pitches": [ + {"position": 0}, + {"position": 3} + ] + } + ], + [ + "お手前", + "pitch", + { + "reading": "おてまえ", + "pitches": [ + {"position": 2, "tags": ["ptag1"]}, + {"position": 2, "tags": ["ptag2"]}, + {"position": 0, "tags": ["ptag2"]} + ] + } + ] ] \ No newline at end of file diff --git a/test/test-database.js b/test/test-database.js index 833aa75d..dbd67257 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -231,8 +231,8 @@ async function testDatabase1() { true ); vm.assert.deepStrictEqual(counts, { - counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}], - total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12} + counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 6, tagMeta: 14}], + total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 6, tagMeta: 14} }); // Test find* functions @@ -648,9 +648,10 @@ async function testFindTermMetaBulk1(database, titles) { } ], expectedResults: { - total: 1, + total: 3, modes: [ - ['freq', 1] + ['freq', 1], + ['pitch', 2] ] } }, -- cgit v1.2.3 From 047efaa3dbe48cde7ea3b96ff6ef0ac07df0ce42 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 1 Mar 2020 14:06:52 -0500 Subject: Add support for returning pitch data from the database --- ext/bg/js/translator.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'ext/bg') diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 6f43f7b0..f16889ce 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -490,6 +490,7 @@ class Translator { // New data term.frequencies = []; + term.pitches = []; } const metas = await this.database.findTermMetaBulk(expressionsUnique, dictionaries); @@ -500,6 +501,13 @@ class Translator { term.frequencies.push({expression, frequency: data, dictionary}); } break; + case 'pitch': + for (const term of termsUnique[index]) { + const pitchData = await this.getPitchData(expression, data, dictionary, term); + if (pitchData === null) { continue; } + term.pitches.push(pitchData); + } + break; } } } @@ -583,6 +591,20 @@ class Translator { return tagMetaList; } + async getPitchData(expression, data, dictionary, term) { + const reading = data.reading; + const termReading = term.reading || expression; + if (reading !== termReading) { return null; } + + const pitches = []; + for (let {position, tags} of data.pitches) { + tags = Array.isArray(tags) ? await this.getTagMetaList(tags, dictionary) : []; + pitches.push({position, tags}); + } + + return {reading, pitches, dictionary}; + } + static createExpression(expression, reading, termTags=null, termFrequency=null) { const furiganaSegments = jp.distributeFurigana(expression, reading); return { -- cgit v1.2.3 From cbc7e2646d2ce34f1aff7ca2b737fdb2db690c40 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 1 Mar 2020 14:38:16 -0500 Subject: Add options --- ext/bg/data/options-schema.json | 17 ++++++++++++++++- ext/bg/js/options.js | 5 ++++- ext/bg/js/settings/main.js | 6 ++++++ ext/bg/settings.html | 12 ++++++++++++ ext/mixed/css/display.css | 13 +++++++++++++ ext/mixed/js/display.js | 3 +++ 6 files changed, 54 insertions(+), 2 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index d6207952..cb759b72 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -105,7 +105,10 @@ "customPopupCss", "customPopupOuterCss", "enableWanakana", - "enableClipboardMonitor" + "enableClipboardMonitor", + "showPitchAccentDownstepNotation", + "showPitchAccentPositionNotation", + "showPitchAccentGraph" ], "properties": { "enable": { @@ -227,6 +230,18 @@ "enableClipboardMonitor": { "type": "boolean", "default": false + }, + "showPitchAccentDownstepNotation": { + "type": "boolean", + "default": true + }, + "showPitchAccentPositionNotation": { + "type": "boolean", + "default": true + }, + "showPitchAccentGraph": { + "type": "boolean", + "default": false } } }, diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index bd0bbe0e..b36fe812 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -124,7 +124,10 @@ function profileOptionsCreateDefaults() { customPopupCss: '', customPopupOuterCss: '', enableWanakana: true, - enableClipboardMonitor: false + enableClipboardMonitor: false, + showPitchAccentDownstepNotation: true, + showPitchAccentPositionNotation: true, + showPitchAccentGraph: false }, audio: { diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index ebc443df..7caeaea0 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -84,6 +84,9 @@ async function formRead(options) { options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val()); options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked'); options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').prop('checked'); + options.general.showPitchAccentDownstepNotation = $('#show-pitch-accent-downstep-notation').prop('checked'); + options.general.showPitchAccentPositionNotation = $('#show-pitch-accent-position-notation').prop('checked'); + options.general.showPitchAccentGraph = $('#show-pitch-accent-graph').prop('checked'); options.general.popupTheme = $('#popup-theme').val(); options.general.popupOuterTheme = $('#popup-outer-theme').val(); options.general.customPopupCss = $('#custom-popup-css').val(); @@ -161,6 +164,9 @@ async function formWrite(options) { $('#popup-scaling-factor').val(options.general.popupScalingFactor); $('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom); $('#popup-scale-relative-to-visual-viewport').prop('checked', options.general.popupScaleRelativeToVisualViewport); + $('#show-pitch-accent-downstep-notation').prop('checked', options.general.showPitchAccentDownstepNotation); + $('#show-pitch-accent-position-notation').prop('checked', options.general.showPitchAccentPositionNotation); + $('#show-pitch-accent-graph').prop('checked', options.general.showPitchAccentGraph); $('#popup-theme').val(options.general.popupTheme); $('#popup-outer-theme').val(options.general.popupOuterTheme); $('#custom-popup-css').val(options.general.customPopupCss); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index cfe20be4..0b2e4f9c 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -162,6 +162,18 @@ +
+ +
+ +
+ +
+ +
+ +
+
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index 7bd82785..cb2f045f 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -537,6 +537,19 @@ button.action-button { } +:root[data-show-pitch-accent-downstep-notation=false] .term-pitch-accent-characters { + display: none; +} + +:root[data-show-pitch-accent-position-notation=false] .term-pitch-accent-position { + display: none; +} + +:root[data-show-pitch-accent-graph=false] .term-pitch-accent-details { + display: none; +} + + /* * Pitch accent graph styles */ diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 6898a6eb..4a71efe0 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -385,6 +385,9 @@ class Display { data.audioEnabled = `${options.audio.enabled}`; data.compactGlossaries = `${options.general.compactGlossaries}`; data.enableSearchTags = `${options.scanning.enableSearchTags}`; + data.showPitchAccentDownstepNotation = `${options.general.showPitchAccentDownstepNotation}`; + data.showPitchAccentPositionNotation = `${options.general.showPitchAccentPositionNotation}`; + data.showPitchAccentGraph = `${options.general.showPitchAccentGraph}`; data.debug = `${options.general.debugInfo}`; } -- cgit v1.2.3 From a339bf69d3841fa97ce9f6673472bd451b813937 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 28 Mar 2020 13:20:42 -0400 Subject: Move set functions into core.js --- .eslintrc.json | 2 ++ ext/bg/js/dictionary.js | 32 ++++---------------------------- ext/mixed/js/core.js | 24 ++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 28 deletions(-) (limited to 'ext/bg') diff --git a/.eslintrc.json b/.eslintrc.json index db8ff1fa..045fd6e3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -87,6 +87,8 @@ "stringReverse": "readonly", "promiseTimeout": "readonly", "parseUrl": "readonly", + "areSetsEqual": "readonly", + "getSetIntersection": "readonly", "EventDispatcher": "readonly", "EventListenerCollection": "readonly", "EXTENSION_IS_BROWSER_EDGE": "readonly" diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index 3dd1d0c1..74bd5a64 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -137,30 +137,6 @@ function dictTermsGroup(definitions, dictionaries) { return dictTermsSort(results); } -function dictAreSetsEqual(set1, set2) { - if (set1.size !== set2.size) { - return false; - } - - for (const value of set1) { - if (!set2.has(value)) { - return false; - } - } - - return true; -} - -function dictGetSetIntersection(set1, set2) { - const result = []; - for (const value of set1) { - if (set2.has(value)) { - result.push(value); - } - } - return result; -} - function dictTermsMergeBySequence(definitions, mainDictionary) { const sequencedDefinitions = new Map(); const nonSequencedDefinitions = []; @@ -281,11 +257,11 @@ function dictTermsMergeByGloss(result, definitions, appendTo=null, mergedIndices const only = []; const expressionSet = definition.expression; const readingSet = definition.reading; - if (!dictAreSetsEqual(expressionSet, resultExpressionSet)) { - only.push(...dictGetSetIntersection(expressionSet, resultExpressionSet)); + if (!areSetsEqual(expressionSet, resultExpressionSet)) { + only.push(...getSetIntersection(expressionSet, resultExpressionSet)); } - if (!dictAreSetsEqual(readingSet, resultReadingSet)) { - only.push(...dictGetSetIntersection(readingSet, resultReadingSet)); + if (!areSetsEqual(readingSet, resultReadingSet)) { + only.push(...getSetIntersection(readingSet, resultReadingSet)); } definition.only = only; } diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 0d50e915..fd762e97 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -132,6 +132,30 @@ function parseUrl(url) { return {baseUrl, queryParams}; } +function areSetsEqual(set1, set2) { + if (set1.size !== set2.size) { + return false; + } + + for (const value of set1) { + if (!set2.has(value)) { + return false; + } + } + + return true; +} + +function getSetIntersection(set1, set2) { + const result = []; + for (const value of set1) { + if (set2.has(value)) { + result.push(value); + } + } + return result; +} + /* * Async utilities -- cgit v1.2.3 From ae84d13757a98e640c8d62f8d856cecbd84dd66f Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 28 Mar 2020 17:51:58 -0400 Subject: Create simplified source map class --- ext/bg/background.html | 1 + ext/bg/js/japanese.js | 31 +++++------- ext/bg/js/text-source-map.js | 115 +++++++++++++++++++++++++++++++++++++++++++ ext/bg/js/translator.js | 30 ++--------- test/test-japanese.js | 18 ++++--- 5 files changed, 143 insertions(+), 52 deletions(-) create mode 100644 ext/bg/js/text-source-map.js (limited to 'ext/bg') diff --git a/ext/bg/background.html b/ext/bg/background.html index f7cf6e55..e456717e 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -38,6 +38,7 @@ + diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js index c5873cf1..2a2b39fd 100644 --- a/ext/bg/js/japanese.js +++ b/ext/bg/js/japanese.js @@ -158,9 +158,8 @@ return result; } - function convertHalfWidthKanaToFullWidth(text, sourceMapping) { + function convertHalfWidthKanaToFullWidth(text, sourceMap=null) { let result = ''; - const hasSourceMapping = Array.isArray(sourceMapping); // This function is safe to use charCodeAt instead of codePointAt, since all // the relevant characters are represented with a single UTF-16 character code. @@ -192,10 +191,8 @@ } } - if (hasSourceMapping && index > 0) { - index = result.length; - const v = sourceMapping.splice(index + 1, 1)[0]; - sourceMapping[index] += v; + if (sourceMap !== null && index > 0) { + sourceMap.combine(result.length, 1); } result += c2; } @@ -203,7 +200,7 @@ return result; } - function convertAlphabeticToKana(text, sourceMapping) { + function convertAlphabeticToKana(text, sourceMap=null) { let part = ''; let result = ''; @@ -222,7 +219,7 @@ c = 0x2d; // '-' } else { if (part.length > 0) { - result += convertAlphabeticPartToKana(part, sourceMapping, result.length); + result += convertAlphabeticPartToKana(part, sourceMap, result.length); part = ''; } result += char; @@ -232,17 +229,16 @@ } if (part.length > 0) { - result += convertAlphabeticPartToKana(part, sourceMapping, result.length); + result += convertAlphabeticPartToKana(part, sourceMap, result.length); } return result; } - function convertAlphabeticPartToKana(text, sourceMapping, sourceMappingStart) { + function convertAlphabeticPartToKana(text, sourceMap, sourceMapStart) { const result = wanakana.toHiragana(text); // Generate source mapping - if (Array.isArray(sourceMapping)) { - if (typeof sourceMappingStart !== 'number') { sourceMappingStart = 0; } + if (sourceMap !== null) { let i = 0; let resultPos = 0; const ii = text.length; @@ -262,18 +258,15 @@ // Merge characters const removals = iNext - i - 1; if (removals > 0) { - let sum = 0; - const vs = sourceMapping.splice(sourceMappingStart + 1, removals); - for (const v of vs) { sum += v; } - sourceMapping[sourceMappingStart] += sum; + sourceMap.combine(sourceMapStart, removals); } - ++sourceMappingStart; + ++sourceMapStart; // Empty elements const additions = resultPosNext - resultPos - 1; for (let j = 0; j < additions; ++j) { - sourceMapping.splice(sourceMappingStart, 0, 0); - ++sourceMappingStart; + sourceMap.insert(sourceMapStart, 0); + ++sourceMapStart; } i = iNext; diff --git a/ext/bg/js/text-source-map.js b/ext/bg/js/text-source-map.js new file mode 100644 index 00000000..24970978 --- /dev/null +++ b/ext/bg/js/text-source-map.js @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2020 Alex Yatskov + * Author: Alex Yatskov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +class TextSourceMap { + constructor(source, mapping=null) { + this._source = source; + this._mapping = (Array.isArray(mapping) ? TextSourceMap._normalizeMapping(mapping) : null); + } + + get source() { + return this._source; + } + + equals(other) { + if (this === other) { + return true; + } + + const source = this._source; + if (!(other instanceof TextSourceMap && source === other._source)) { + return false; + } + + let mapping = this._mapping; + let otherMapping = other._mapping; + if (mapping === null) { + if (otherMapping === null) { + return true; + } + mapping = TextSourceMap._createMapping(source); + } else if (otherMapping === null) { + otherMapping = TextSourceMap._createMapping(source); + } + + const mappingLength = mapping.length; + if (mappingLength !== otherMapping.length) { + return false; + } + + for (let i = 0; i < mappingLength; ++i) { + if (mapping[i] !== otherMapping[i]) { + return false; + } + } + + return true; + } + + getSourceLength(finalLength) { + const mapping = this._mapping; + if (mapping === null) { + return finalLength; + } + + let sourceLength = 0; + for (let i = 0; i < finalLength; ++i) { + sourceLength += mapping[i]; + } + return sourceLength; + } + + combine(index, count) { + if (count <= 0) { return; } + + if (this._mapping === null) { + this._mapping = TextSourceMap._createMapping(this._source); + } + + let sum = this._mapping[index]; + const parts = this._mapping.splice(index + 1, count); + for (const part of parts) { + sum += part; + } + this._mapping[index] = sum; + } + + insert(index, ...items) { + if (this._mapping === null) { + this._mapping = TextSourceMap._createMapping(this._source); + } + + this._mapping.splice(index, 0, ...items); + } + + static _createMapping(text) { + return new Array(text.length).fill(1); + } + + static _normalizeMapping(mapping) { + const result = []; + for (const value of mapping) { + result.push( + (typeof value === 'number' && Number.isFinite(value)) ? + Math.floor(value) : + 0 + ); + } + return result; + } +} diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 6f43f7b0..584da02c 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -19,6 +19,7 @@ /* global * Database * Deinflector + * TextSourceMap * dictEnabledSet * dictTagBuildSource * dictTagSanitize @@ -367,17 +368,15 @@ class Translator { const used = new Set(); for (const [halfWidth, numeric, alphabetic, katakana, hiragana] of Translator.getArrayVariants(textOptionVariantArray)) { let text2 = text; - let sourceMapping = null; + const sourceMap = new TextSourceMap(text2); if (halfWidth) { - if (sourceMapping === null) { sourceMapping = Translator.createTextSourceMapping(text2); } - text2 = jp.convertHalfWidthKanaToFullWidth(text2, sourceMapping); + text2 = jp.convertHalfWidthKanaToFullWidth(text2, sourceMap); } if (numeric) { text2 = jp.convertNumericToFullWidth(text2); } if (alphabetic) { - if (sourceMapping === null) { sourceMapping = Translator.createTextSourceMapping(text2); } - text2 = jp.convertAlphabeticToKana(text2, sourceMapping); + text2 = jp.convertAlphabeticToKana(text2, sourceMap); } if (katakana) { text2 = jp.convertHiraganaToKatakana(text2); @@ -391,7 +390,7 @@ class Translator { if (used.has(text2Substring)) { break; } used.add(text2Substring); for (const deinflection of this.deinflector.deinflect(text2Substring)) { - deinflection.rawSource = Translator.getDeinflectionRawSource(text, i, sourceMapping); + deinflection.rawSource = sourceMap.source.substring(0, sourceMap.getSourceLength(i)); deinflections.push(deinflection); } } @@ -407,25 +406,6 @@ class Translator { } } - static getDeinflectionRawSource(source, length, sourceMapping) { - if (sourceMapping === null) { - return source.substring(0, length); - } - - let result = ''; - let index = 0; - for (let i = 0; i < length; ++i) { - const c = sourceMapping[i]; - result += source.substring(index, index + c); - index += c; - } - return result; - } - - static createTextSourceMapping(text) { - return new Array(text.length).fill(1); - } - async findKanji(text, options) { const dictionaries = dictEnabledSet(options); const kanjiUnique = new Set(); diff --git a/test/test-japanese.js b/test/test-japanese.js index c5d220e7..a16a73b7 100644 --- a/test/test-japanese.js +++ b/test/test-japanese.js @@ -23,9 +23,11 @@ const vm = new VM(); vm.execute([ 'mixed/lib/wanakana.min.js', 'mixed/js/japanese.js', + 'bg/js/text-source-map.js', 'bg/js/japanese.js' ]); const jp = vm.get('jp'); +const TextSourceMap = vm.get('TextSourceMap'); function testIsCodePointKanji() { @@ -262,13 +264,13 @@ function testConvertHalfWidthKanaToFullWidth() { ]; for (const [string, expected, expectedSourceMapping] of data) { - const sourceMapping = new Array(string.length).fill(1); + const sourceMap = new TextSourceMap(string); const actual1 = jp.convertHalfWidthKanaToFullWidth(string, null); - const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMapping); + const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMap); assert.strictEqual(actual1, expected); assert.strictEqual(actual2, expected); - if (Array.isArray(expectedSourceMapping)) { - vm.assert.deepStrictEqual(sourceMapping, expectedSourceMapping); + if (typeof expectedSourceMapping !== 'undefined') { + assert.ok(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))); } } } @@ -285,13 +287,13 @@ function testConvertAlphabeticToKana() { ]; for (const [string, expected, expectedSourceMapping] of data) { - const sourceMapping = new Array(string.length).fill(1); + const sourceMap = new TextSourceMap(string); const actual1 = jp.convertAlphabeticToKana(string, null); - const actual2 = jp.convertAlphabeticToKana(string, sourceMapping); + const actual2 = jp.convertAlphabeticToKana(string, sourceMap); assert.strictEqual(actual1, expected); assert.strictEqual(actual2, expected); - if (Array.isArray(expectedSourceMapping)) { - vm.assert.deepStrictEqual(sourceMapping, expectedSourceMapping); + if (typeof expectedSourceMapping !== 'undefined') { + assert.ok(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))); } } } -- cgit v1.2.3 From a6fedae9c7fd5c3d9dfe5d20a8b10ad79af7693f Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 30 Mar 2020 20:19:39 -0400 Subject: Update bulkAdd implementation --- ext/bg/js/database.js | 70 ++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 37 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 08a2a39f..5109e9e8 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -322,9 +322,39 @@ class Database { return result; } + bulkAdd(objectStoreName, items, start, count) { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([objectStoreName], 'readwrite'); + const objectStore = transaction.objectStore(objectStoreName); + + if (start + count > items.length) { + count = items.length - start; + } + + if (count <= 0) { + resolve(); + return; + } + + const end = start + count; + let completedCount = 0; + const onError = (e) => reject(e); + const onSuccess = () => { + if (++completedCount >= count) { + resolve(); + } + }; + + for (let i = start; i < end; ++i) { + const request = objectStore.add(items[i]); + request.onerror = onError; + request.onsuccess = onSuccess; + } + }); + } + async importDictionary(archiveSource, onProgress, details) { this._validate(); - const db = this.db; const hasOnProgress = (typeof onProgress === 'function'); // Read archive @@ -448,11 +478,7 @@ class Database { prefixWildcardsSupported }; - { - const transaction = db.transaction(['dictionaries'], 'readwrite'); - const objectStore = transaction.objectStore('dictionaries'); - await Database._bulkAdd(objectStore, [summary], 0, 1); - } + await this.bulkAdd('dictionaries', [summary], 0, 1); // Add data const errors = []; @@ -472,9 +498,7 @@ class Database { const count = Math.min(maxTransactionLength, ii - i); try { - const transaction = db.transaction([objectStoreName], 'readwrite'); - const objectStore = transaction.objectStore(objectStoreName); - await Database._bulkAdd(objectStore, entries, i, count); + await this.bulkAdd(objectStoreName, entries, i, count); } catch (e) { errors.push(e); } @@ -760,34 +784,6 @@ class Database { }); } - static _bulkAdd(objectStore, items, start, count) { - return new Promise((resolve, reject) => { - if (start + count > items.length) { - count = items.length - start; - } - - if (count <= 0) { - resolve(); - return; - } - - const end = start + count; - let completedCount = 0; - const onError = (e) => reject(e); - const onSuccess = () => { - if (++completedCount >= count) { - resolve(); - } - }; - - for (let i = start; i < end; ++i) { - const request = objectStore.add(items[i]); - request.onerror = onError; - request.onsuccess = onSuccess; - } - }); - } - static _open(name, version, onUpgradeNeeded) { return new Promise((resolve, reject) => { const request = window.indexedDB.open(name, version * 10); -- cgit v1.2.3 From 1a8bbf32d580173f5997754f6b36015ae212c9f9 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 30 Mar 2020 20:27:37 -0400 Subject: Make dictionaryExists public --- ext/bg/js/database.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 5109e9e8..effa50a6 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -322,6 +322,15 @@ class Database { return result; } + async dictionaryExists(title) { + this._validate(); + const transaction = this.db.transaction(['dictionaries'], 'readonly'); + const index = transaction.objectStore('dictionaries').index('title'); + const query = IDBKeyRange.only(title); + const count = await Database._getCount(index, query); + return count > 0; + } + bulkAdd(objectStoreName, items, start, count) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([objectStoreName], 'readwrite'); @@ -380,7 +389,7 @@ class Database { } // Verify database is not already imported - if (await this._dictionaryExists(dictionaryTitle)) { + if (await this.dictionaryExists(dictionaryTitle)) { throw new Error('Dictionary is already imported'); } @@ -592,15 +601,6 @@ class Database { return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank]; } - async _dictionaryExists(title) { - const db = this.db; - const dbCountTransaction = db.transaction(['dictionaries'], 'readonly'); - const dbIndex = dbCountTransaction.objectStore('dictionaries').index('title'); - const only = IDBKeyRange.only(title); - const count = await Database._getCount(dbIndex, only); - return count > 0; - } - async _findGenericBulk(tableName, indexName, indexValueList, dictionaries, createResult) { this._validate(); -- cgit v1.2.3 From 8095d9138c413f27b26c9ccba2ca2ae9762707f3 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 30 Mar 2020 20:27:44 -0400 Subject: Add isPrepared --- ext/bg/js/database.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ext/bg') diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index effa50a6..269ad57e 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -110,6 +110,10 @@ class Database { this.db = null; } + isPrepared() { + return this.db !== null; + } + async purge() { this._validate(); -- cgit v1.2.3 From c193a703cc1444fbc75e62954ca3a10210ad95d0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 30 Mar 2020 20:39:04 -0400 Subject: Move database creation into Backend --- ext/bg/js/backend.js | 5 ++++- ext/bg/js/translator.js | 18 +++++------------- 2 files changed, 9 insertions(+), 14 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index b217e64d..dd666b0d 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -24,6 +24,7 @@ * AudioUriBuilder * BackendApiForwarder * ClipboardMonitor + * Database * JsonSchema * Mecab * Translator @@ -43,7 +44,8 @@ class Backend { constructor() { - this.translator = new Translator(); + this.database = new Database(); + this.translator = new Translator(this.database); this.anki = new AnkiNull(); this.mecab = new Mecab(); this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); @@ -107,6 +109,7 @@ class Backend { } async prepare() { + await this.database.prepare(); await this.translator.prepare(); this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET'); diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 6f43f7b0..df19eee1 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -17,7 +17,6 @@ */ /* global - * Database * Deinflector * dictEnabledSet * dictTagBuildSource @@ -34,23 +33,16 @@ */ class Translator { - constructor() { - this.database = null; + constructor(database) { + this.database = database; this.deinflector = null; this.tagCache = new Map(); } async prepare() { - if (!this.database) { - this.database = new Database(); - await this.database.prepare(); - } - - if (!this.deinflector) { - const url = chrome.runtime.getURL('/bg/lang/deinflect.json'); - const reasons = await requestJson(url, 'GET'); - this.deinflector = new Deinflector(reasons); - } + const url = chrome.runtime.getURL('/bg/lang/deinflect.json'); + const reasons = await requestJson(url, 'GET'); + this.deinflector = new Deinflector(reasons); } async purgeDatabase() { -- cgit v1.2.3 From 02f7763f004790a432302637586a876291e32f61 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 30 Mar 2020 20:45:36 -0400 Subject: Add importDictionary function to Backend --- ext/bg/js/backend.js | 4 ++++ ext/bg/js/util.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'ext/bg') diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index dd666b0d..3ef7c62c 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -299,6 +299,10 @@ class Backend { return true; } + async importDictionary(archiveSource, onProgress, details) { + return await this.translator.database.importDictionary(archiveSource, onProgress, details); + } + // Message handlers _onApiYomichanCoreReady(_params, sender) { diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 79c6af06..a7ed4a34 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -118,7 +118,7 @@ async function utilDatabaseDeleteDictionary(dictionaryName, onProgress) { async function utilDatabaseImport(data, onProgress, details) { data = await utilReadFile(data); - return utilIsolate(await utilBackend().translator.database.importDictionary( + return utilIsolate(await utilBackend().importDictionary( utilBackgroundIsolate(data), utilBackgroundFunctionIsolate(onProgress), utilBackgroundIsolate(details) -- cgit v1.2.3 From 9052ab8ebd5af505f1992bfc001b226202e2f393 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 30 Mar 2020 20:51:20 -0400 Subject: Move dictionary import functionality into a new class --- ext/bg/background.html | 1 + ext/bg/js/backend.js | 4 +- ext/bg/js/database.js | 231 ---------------------------------- ext/bg/js/dictionary-importer.js | 266 +++++++++++++++++++++++++++++++++++++++ test/test-database.js | 16 ++- 5 files changed, 281 insertions(+), 237 deletions(-) create mode 100644 ext/bg/js/dictionary-importer.js (limited to 'ext/bg') diff --git a/ext/bg/background.html b/ext/bg/background.html index f7cf6e55..62802341 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -30,6 +30,7 @@ + diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 3ef7c62c..1e8c979f 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -25,6 +25,7 @@ * BackendApiForwarder * ClipboardMonitor * Database + * DictionaryImporter * JsonSchema * Mecab * Translator @@ -45,6 +46,7 @@ class Backend { constructor() { this.database = new Database(); + this.dictionaryImporter = new DictionaryImporter(); this.translator = new Translator(this.database); this.anki = new AnkiNull(); this.mecab = new Mecab(); @@ -300,7 +302,7 @@ class Backend { } async importDictionary(archiveSource, onProgress, details) { - return await this.translator.database.importDictionary(archiveSource, onProgress, details); + return await this.dictionaryImporter.import(this.database, archiveSource, onProgress, details); } // Message handlers diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 269ad57e..7a4d094b 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -366,172 +366,6 @@ class Database { }); } - async importDictionary(archiveSource, onProgress, details) { - this._validate(); - const hasOnProgress = (typeof onProgress === 'function'); - - // Read archive - const archive = await JSZip.loadAsync(archiveSource); - - // Read and validate index - const indexFileName = 'index.json'; - const indexFile = archive.files[indexFileName]; - if (!indexFile) { - throw new Error('No dictionary index found in archive'); - } - - const index = JSON.parse(await indexFile.async('string')); - - const indexSchema = await this._getSchema('/bg/data/dictionary-index-schema.json'); - Database._validateJsonSchema(index, indexSchema, indexFileName); - - const dictionaryTitle = index.title; - const version = index.format || index.version; - - if (!dictionaryTitle || !index.revision) { - throw new Error('Unrecognized dictionary format'); - } - - // Verify database is not already imported - if (await this.dictionaryExists(dictionaryTitle)) { - throw new Error('Dictionary is already imported'); - } - - // Data format converters - const convertTermBankEntry = (entry) => { - if (version === 1) { - const [expression, reading, definitionTags, rules, score, ...glossary] = entry; - return {expression, reading, definitionTags, rules, score, glossary}; - } else { - const [expression, reading, definitionTags, rules, score, glossary, sequence, termTags] = entry; - return {expression, reading, definitionTags, rules, score, glossary, sequence, termTags}; - } - }; - - const convertTermMetaBankEntry = (entry) => { - const [expression, mode, data] = entry; - return {expression, mode, data}; - }; - - const convertKanjiBankEntry = (entry) => { - if (version === 1) { - const [character, onyomi, kunyomi, tags, ...meanings] = entry; - return {character, onyomi, kunyomi, tags, meanings}; - } else { - const [character, onyomi, kunyomi, tags, meanings, stats] = entry; - return {character, onyomi, kunyomi, tags, meanings, stats}; - } - }; - - const convertKanjiMetaBankEntry = (entry) => { - const [character, mode, data] = entry; - return {character, mode, data}; - }; - - const convertTagBankEntry = (entry) => { - const [name, category, order, notes, score] = entry; - return {name, category, order, notes, score}; - }; - - // Archive file reading - const readFileSequence = async (fileNameFormat, convertEntry, schema) => { - const results = []; - for (let i = 1; true; ++i) { - const fileName = fileNameFormat.replace(/\?/, `${i}`); - const file = archive.files[fileName]; - if (!file) { break; } - - const entries = JSON.parse(await file.async('string')); - Database._validateJsonSchema(entries, schema, fileName); - - for (let entry of entries) { - entry = convertEntry(entry); - entry.dictionary = dictionaryTitle; - results.push(entry); - } - } - return results; - }; - - // Load schemas - const dataBankSchemaPaths = this.constructor._getDataBankSchemaPaths(version); - const dataBankSchemas = await Promise.all(dataBankSchemaPaths.map((path) => this._getSchema(path))); - - // Load data - const termList = await readFileSequence('term_bank_?.json', convertTermBankEntry, dataBankSchemas[0]); - const termMetaList = await readFileSequence('term_meta_bank_?.json', convertTermMetaBankEntry, dataBankSchemas[1]); - const kanjiList = await readFileSequence('kanji_bank_?.json', convertKanjiBankEntry, dataBankSchemas[2]); - const kanjiMetaList = await readFileSequence('kanji_meta_bank_?.json', convertKanjiMetaBankEntry, dataBankSchemas[3]); - const tagList = await readFileSequence('tag_bank_?.json', convertTagBankEntry, dataBankSchemas[4]); - - // Old tags - const indexTagMeta = index.tagMeta; - if (typeof indexTagMeta === 'object' && indexTagMeta !== null) { - for (const name of Object.keys(indexTagMeta)) { - const {category, order, notes, score} = indexTagMeta[name]; - tagList.push({name, category, order, notes, score}); - } - } - - // Prefix wildcard support - const prefixWildcardsSupported = !!details.prefixWildcardsSupported; - if (prefixWildcardsSupported) { - for (const entry of termList) { - entry.expressionReverse = stringReverse(entry.expression); - entry.readingReverse = stringReverse(entry.reading); - } - } - - // Add dictionary - const summary = { - title: dictionaryTitle, - revision: index.revision, - sequenced: index.sequenced, - version, - prefixWildcardsSupported - }; - - await this.bulkAdd('dictionaries', [summary], 0, 1); - - // Add data - const errors = []; - const total = ( - termList.length + - termMetaList.length + - kanjiList.length + - kanjiMetaList.length + - tagList.length - ); - let loadedCount = 0; - const maxTransactionLength = 1000; - - const bulkAdd = async (objectStoreName, entries) => { - const ii = entries.length; - for (let i = 0; i < ii; i += maxTransactionLength) { - const count = Math.min(maxTransactionLength, ii - i); - - try { - await this.bulkAdd(objectStoreName, entries, i, count); - } catch (e) { - errors.push(e); - } - - loadedCount += count; - if (hasOnProgress) { - onProgress(total, loadedCount); - } - } - }; - - await bulkAdd('terms', termList); - await bulkAdd('termMeta', termMetaList); - await bulkAdd('kanji', kanjiList); - await bulkAdd('kanjiMeta', kanjiMetaList); - await bulkAdd('tagMeta', tagList); - - return {result: summary, errors}; - } - // Private _validate() { @@ -540,71 +374,6 @@ class Database { } } - async _getSchema(fileName) { - let schemaPromise = this._schemas.get(fileName); - if (typeof schemaPromise !== 'undefined') { - return schemaPromise; - } - - schemaPromise = requestJson(chrome.runtime.getURL(fileName), 'GET'); - this._schemas.set(fileName, schemaPromise); - return schemaPromise; - } - - static _validateJsonSchema(value, schema, fileName) { - try { - JsonSchema.validate(value, schema); - } catch (e) { - throw Database._formatSchemaError(e, fileName); - } - } - - static _formatSchemaError(e, fileName) { - const valuePathString = Database._getSchemaErrorPathString(e.info.valuePath, 'dictionary'); - const schemaPathString = Database._getSchemaErrorPathString(e.info.schemaPath, 'schema'); - - const e2 = new Error(`Dictionary has invalid data in '${fileName}' for value '${valuePathString}', validated against '${schemaPathString}': ${e.message}`); - e2.data = e; - - return e2; - } - - static _getSchemaErrorPathString(infoList, base='') { - let result = base; - for (const [part] of infoList) { - switch (typeof part) { - case 'string': - if (result.length > 0) { - result += '.'; - } - result += part; - break; - case 'number': - result += `[${part}]`; - break; - } - } - return result; - } - - static _getDataBankSchemaPaths(version) { - const termBank = ( - version === 1 ? - '/bg/data/dictionary-term-bank-v1-schema.json' : - '/bg/data/dictionary-term-bank-v3-schema.json' - ); - const termMetaBank = '/bg/data/dictionary-term-meta-bank-v3-schema.json'; - const kanjiBank = ( - version === 1 ? - '/bg/data/dictionary-kanji-bank-v1-schema.json' : - '/bg/data/dictionary-kanji-bank-v3-schema.json' - ); - const kanjiMetaBank = '/bg/data/dictionary-kanji-meta-bank-v3-schema.json'; - const tagBank = '/bg/data/dictionary-tag-bank-v3-schema.json'; - - return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank]; - } - async _findGenericBulk(tableName, indexName, indexValueList, dictionaries, createResult) { this._validate(); diff --git a/ext/bg/js/dictionary-importer.js b/ext/bg/js/dictionary-importer.js new file mode 100644 index 00000000..589e7656 --- /dev/null +++ b/ext/bg/js/dictionary-importer.js @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2020 Alex Yatskov + * Author: Alex Yatskov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global + * JSZip + * JsonSchema + * requestJson + */ + +class DictionaryImporter { + constructor() { + this._schemas = new Map(); + } + + async import(database, archiveSource, onProgress, details) { + if (!database) { + throw new Error('Invalid database'); + } + if (!database.isPrepared()) { + throw new Error('Database is not ready'); + } + + const hasOnProgress = (typeof onProgress === 'function'); + + // Read archive + const archive = await JSZip.loadAsync(archiveSource); + + // Read and validate index + const indexFileName = 'index.json'; + const indexFile = archive.files[indexFileName]; + if (!indexFile) { + throw new Error('No dictionary index found in archive'); + } + + const index = JSON.parse(await indexFile.async('string')); + + const indexSchema = await this._getSchema('/bg/data/dictionary-index-schema.json'); + this._validateJsonSchema(index, indexSchema, indexFileName); + + const dictionaryTitle = index.title; + const version = index.format || index.version; + + if (!dictionaryTitle || !index.revision) { + throw new Error('Unrecognized dictionary format'); + } + + // Verify database is not already imported + if (await database.dictionaryExists(dictionaryTitle)) { + throw new Error('Dictionary is already imported'); + } + + // Data format converters + const convertTermBankEntry = (entry) => { + if (version === 1) { + const [expression, reading, definitionTags, rules, score, ...glossary] = entry; + return {expression, reading, definitionTags, rules, score, glossary}; + } else { + const [expression, reading, definitionTags, rules, score, glossary, sequence, termTags] = entry; + return {expression, reading, definitionTags, rules, score, glossary, sequence, termTags}; + } + }; + + const convertTermMetaBankEntry = (entry) => { + const [expression, mode, data] = entry; + return {expression, mode, data}; + }; + + const convertKanjiBankEntry = (entry) => { + if (version === 1) { + const [character, onyomi, kunyomi, tags, ...meanings] = entry; + return {character, onyomi, kunyomi, tags, meanings}; + } else { + const [character, onyomi, kunyomi, tags, meanings, stats] = entry; + return {character, onyomi, kunyomi, tags, meanings, stats}; + } + }; + + const convertKanjiMetaBankEntry = (entry) => { + const [character, mode, data] = entry; + return {character, mode, data}; + }; + + const convertTagBankEntry = (entry) => { + const [name, category, order, notes, score] = entry; + return {name, category, order, notes, score}; + }; + + // Archive file reading + const readFileSequence = async (fileNameFormat, convertEntry, schema) => { + const results = []; + for (let i = 1; true; ++i) { + const fileName = fileNameFormat.replace(/\?/, `${i}`); + const file = archive.files[fileName]; + if (!file) { break; } + + const entries = JSON.parse(await file.async('string')); + this._validateJsonSchema(entries, schema, fileName); + + for (let entry of entries) { + entry = convertEntry(entry); + entry.dictionary = dictionaryTitle; + results.push(entry); + } + } + return results; + }; + + // Load schemas + const dataBankSchemaPaths = this._getDataBankSchemaPaths(version); + const dataBankSchemas = await Promise.all(dataBankSchemaPaths.map((path) => this._getSchema(path))); + + // Load data + const termList = await readFileSequence('term_bank_?.json', convertTermBankEntry, dataBankSchemas[0]); + const termMetaList = await readFileSequence('term_meta_bank_?.json', convertTermMetaBankEntry, dataBankSchemas[1]); + const kanjiList = await readFileSequence('kanji_bank_?.json', convertKanjiBankEntry, dataBankSchemas[2]); + const kanjiMetaList = await readFileSequence('kanji_meta_bank_?.json', convertKanjiMetaBankEntry, dataBankSchemas[3]); + const tagList = await readFileSequence('tag_bank_?.json', convertTagBankEntry, dataBankSchemas[4]); + + // Old tags + const indexTagMeta = index.tagMeta; + if (typeof indexTagMeta === 'object' && indexTagMeta !== null) { + for (const name of Object.keys(indexTagMeta)) { + const {category, order, notes, score} = indexTagMeta[name]; + tagList.push({name, category, order, notes, score}); + } + } + + // Prefix wildcard support + const prefixWildcardsSupported = !!details.prefixWildcardsSupported; + if (prefixWildcardsSupported) { + for (const entry of termList) { + entry.expressionReverse = stringReverse(entry.expression); + entry.readingReverse = stringReverse(entry.reading); + } + } + + // Add dictionary + const summary = { + title: dictionaryTitle, + revision: index.revision, + sequenced: index.sequenced, + version, + prefixWildcardsSupported + }; + + database.bulkAdd('dictionaries', [summary], 0, 1); + + // Add data + const errors = []; + const total = ( + termList.length + + termMetaList.length + + kanjiList.length + + kanjiMetaList.length + + tagList.length + ); + let loadedCount = 0; + const maxTransactionLength = 1000; + + const bulkAdd = async (objectStoreName, entries) => { + const ii = entries.length; + for (let i = 0; i < ii; i += maxTransactionLength) { + const count = Math.min(maxTransactionLength, ii - i); + + try { + await database.bulkAdd(objectStoreName, entries, i, count); + } catch (e) { + errors.push(e); + } + + loadedCount += count; + if (hasOnProgress) { + onProgress(total, loadedCount); + } + } + }; + + await bulkAdd('terms', termList); + await bulkAdd('termMeta', termMetaList); + await bulkAdd('kanji', kanjiList); + await bulkAdd('kanjiMeta', kanjiMetaList); + await bulkAdd('tagMeta', tagList); + + return {result: summary, errors}; + } + + async _getSchema(fileName) { + let schemaPromise = this._schemas.get(fileName); + if (typeof schemaPromise !== 'undefined') { + return schemaPromise; + } + + schemaPromise = requestJson(chrome.runtime.getURL(fileName), 'GET'); + this._schemas.set(fileName, schemaPromise); + return schemaPromise; + } + + _validateJsonSchema(value, schema, fileName) { + try { + JsonSchema.validate(value, schema); + } catch (e) { + throw this._formatSchemaError(e, fileName); + } + } + + _formatSchemaError(e, fileName) { + const valuePathString = this._getSchemaErrorPathString(e.info.valuePath, 'dictionary'); + const schemaPathString = this._getSchemaErrorPathString(e.info.schemaPath, 'schema'); + + const e2 = new Error(`Dictionary has invalid data in '${fileName}' for value '${valuePathString}', validated against '${schemaPathString}': ${e.message}`); + e2.data = e; + + return e2; + } + + _getSchemaErrorPathString(infoList, base='') { + let result = base; + for (const [part] of infoList) { + switch (typeof part) { + case 'string': + if (result.length > 0) { + result += '.'; + } + result += part; + break; + case 'number': + result += `[${part}]`; + break; + } + } + return result; + } + + _getDataBankSchemaPaths(version) { + const termBank = ( + version === 1 ? + '/bg/data/dictionary-term-bank-v1-schema.json' : + '/bg/data/dictionary-term-bank-v3-schema.json' + ); + const termMetaBank = '/bg/data/dictionary-term-meta-bank-v3-schema.json'; + const kanjiBank = ( + version === 1 ? + '/bg/data/dictionary-kanji-bank-v1-schema.json' : + '/bg/data/dictionary-kanji-bank-v3-schema.json' + ); + const kanjiMetaBank = '/bg/data/dictionary-kanji-meta-bank-v3-schema.json'; + const tagBank = '/bg/data/dictionary-tag-bank-v3-schema.json'; + + return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank]; + } +} diff --git a/test/test-database.js b/test/test-database.js index 833aa75d..c3402b73 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -107,8 +107,10 @@ vm.execute([ 'bg/js/dictionary.js', 'mixed/js/core.js', 'bg/js/request.js', + 'bg/js/dictionary-importer.js', 'bg/js/database.js' ]); +const DictionaryImporter = vm.get('DictionaryImporter'); const Database = vm.get('Database'); @@ -196,6 +198,7 @@ async function testDatabase1() { ]; // Setup database + const dictionaryImporter = new DictionaryImporter(); const database = new Database(); await database.prepare(); @@ -210,7 +213,8 @@ async function testDatabase1() { // Import data let progressEvent = false; - const {result, errors} = await database.importDictionary( + const {result, errors} = await dictionaryImporter.import( + database, testDictionarySource, () => { progressEvent = true; @@ -847,6 +851,7 @@ async function testDatabase2() { ]); // Setup database + const dictionaryImporter = new DictionaryImporter(); const database = new Database(); // Error: not prepared @@ -862,17 +867,17 @@ async function testDatabase2() { await assert.rejects(async () => await database.findTagForTitle('tag', title)); await assert.rejects(async () => await database.getDictionaryInfo()); await assert.rejects(async () => await database.getDictionaryCounts(titles, true)); - await assert.rejects(async () => await database.importDictionary(testDictionarySource, () => {}, {})); + await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, () => {}, {})); await database.prepare(); // Error: already prepared await assert.rejects(async () => await database.prepare()); - await database.importDictionary(testDictionarySource, () => {}, {}); + await dictionaryImporter.import(database, testDictionarySource, () => {}, {}); // Error: dictionary already imported - await assert.rejects(async () => await database.importDictionary(testDictionarySource, () => {}, {})); + await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, () => {}, {})); await database.close(); } @@ -889,6 +894,7 @@ async function testDatabase3() { ]; // Setup database + const dictionaryImporter = new DictionaryImporter(); const database = new Database(); await database.prepare(); @@ -898,7 +904,7 @@ async function testDatabase3() { let error = null; try { - await database.importDictionary(testDictionarySource, () => {}, {}); + await dictionaryImporter.import(database, testDictionarySource, () => {}, {}); } catch (e) { error = e; } -- cgit v1.2.3 From d20ece9f074bb9d241a902f29344e5906e3c8210 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Thu, 19 Mar 2020 17:46:05 +0200 Subject: move frame offset forwarding code to a class --- ext/bg/js/search-frontend.js | 1 + ext/fg/js/frame-offset-forwarder.js | 94 +++++++++++++++++++++++++++++++++++++ ext/fg/js/frontend-initialize.js | 6 ++- ext/fg/js/popup-proxy-host.js | 31 ------------ ext/fg/js/popup-proxy.js | 67 ++------------------------ ext/manifest.json | 1 + 6 files changed, 105 insertions(+), 95 deletions(-) create mode 100644 ext/fg/js/frame-offset-forwarder.js (limited to 'ext/bg') diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js index 2d2aa8d4..f130a6fa 100644 --- a/ext/bg/js/search-frontend.js +++ b/ext/bg/js/search-frontend.js @@ -35,6 +35,7 @@ async function searchFrontendSetup() { const scriptSrcs = [ '/mixed/js/text-scanner.js', '/fg/js/frontend-api-receiver.js', + '/fg/js/frame-offset-forwarder.js', '/fg/js/popup.js', '/fg/js/popup-proxy-host.js', '/fg/js/frontend.js', diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js new file mode 100644 index 00000000..b3715c2a --- /dev/null +++ b/ext/fg/js/frame-offset-forwarder.js @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 Alex Yatskov + * Author: Alex Yatskov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global + * apiForward + */ + +class FrameOffsetForwarder { + constructor() { + this._forwardFrameOffset = window !== window.parent ? + this._forwardFrameOffsetParent.bind(this) : + this._forwardFrameOffsetOrigin.bind(this); + + this._windowMessageHandlers = new Map([ + ['getFrameOffset', ({offset, uniqueId}, e) => { return this._onGetFrameOffset(offset, uniqueId, e); }] + ]); + + window.addEventListener('message', this.onMessage.bind(this), false); + } + + async applyOffset(x, y) { + const uniqueId = yomichan.generateId(16); + + let frameOffsetResolve = null; + const frameOffsetPromise = new Promise((resolve) => (frameOffsetResolve = resolve)); + + const runtimeMessageCallback = ({action, params}, sender, callback) => { + if (action === 'frameOffset' && isObject(params) && params.uniqueId === uniqueId) { + chrome.runtime.onMessage.removeListener(runtimeMessageCallback); + callback(); + frameOffsetResolve(params); + return false; + } + }; + chrome.runtime.onMessage.addListener(runtimeMessageCallback); + + window.parent.postMessage({ + action: 'getFrameOffset', + params: { + uniqueId, + offset: [x, y] + } + }, '*'); + + const {offset} = await frameOffsetPromise; + return offset; + } + + onMessage(e) { + const {action, params} = e.data; + const handler = this._windowMessageHandlers.get(action); + if (typeof handler !== 'function') { return; } + handler(params, e); + } + + _onGetFrameOffset(offset, uniqueId, e) { + let sourceFrame = null; + for (const frame of document.querySelectorAll('frame, iframe:not(.yomichan-float)')) { + if (frame.contentWindow !== e.source) { continue; } + sourceFrame = frame; + break; + } + if (sourceFrame === null) { return; } + + const [forwardedX, forwardedY] = offset; + const {x, y} = sourceFrame.getBoundingClientRect(); + offset = [forwardedX + x, forwardedY + y]; + + this._forwardFrameOffset(offset, uniqueId); + } + + _forwardFrameOffsetParent(offset, uniqueId) { + window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*'); + } + + _forwardFrameOffsetOrigin(offset, uniqueId) { + apiForward('frameOffset', {offset, uniqueId}); + } +} diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 352c6bd9..777291fe 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -17,6 +17,7 @@ */ /* global + * FrameOffsetForwarder * Frontend * PopupProxy * PopupProxyHost @@ -47,12 +48,15 @@ async function main() { const {popupId, frameId} = await rootPopupInformationPromise; - popup = new PopupProxy(popupId, 0, null, frameId, url); + window._frameOffsetForwarder = new FrameOffsetForwarder(); + const applyFrameOffset = window._frameOffsetForwarder.applyOffset.bind(window._frameOffsetForwarder); + popup = new PopupProxy(popupId, 0, null, frameId, url, applyFrameOffset); await popup.prepare(); } else if (proxy) { popup = new PopupProxy(null, depth + 1, id, parentFrameId, url); await popup.prepare(); } else { + window._frameOffsetForwarder = new FrameOffsetForwarder(); const popupHost = new PopupProxyHost(); await popupHost.prepare(); diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index 487dda90..4b136e41 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -19,7 +19,6 @@ /* global * FrontendApiReceiver * Popup - * apiForward * apiFrameInformationGet */ @@ -49,12 +48,6 @@ class PopupProxyHost { ['clearAutoPlayTimer', this._onApiClearAutoPlayTimer.bind(this)], ['setContentScale', this._onApiSetContentScale.bind(this)] ])); - - this._windowMessageHandlers = new Map([ - ['getIframeOffset', ({offset, uniqueId}, e) => { return this._onGetIframeOffset(offset, uniqueId, e); }] - ]); - - window.addEventListener('message', this.onMessage.bind(this), false); } getOrCreatePopup(id=null, parentId=null, depth=null) { @@ -159,30 +152,6 @@ class PopupProxyHost { return popup.setContentScale(scale); } - // Window message handlers - - onMessage(e) { - const {action, params} = e.data; - const handler = this._windowMessageHandlers.get(action); - if (typeof handler !== 'function') { return; } - handler(params, e); - } - - _onGetIframeOffset(offset, uniqueId, e) { - let sourceIframe = null; - for (const iframe of document.querySelectorAll('iframe:not(.yomichan-float)')) { - if (iframe.contentWindow !== e.source) { continue; } - sourceIframe = iframe; - break; - } - if (sourceIframe === null) { return; } - - const [forwardedX, forwardedY] = offset; - const {x, y} = sourceIframe.getBoundingClientRect(); - offset = [forwardedX + x, forwardedY + y]; - apiForward('iframeOffset', {offset, uniqueId}); - } - // Private functions _getPopup(id) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 8693ef17..73148eee 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -21,19 +21,14 @@ */ class PopupProxy { - constructor(id, depth, parentId, parentFrameId, url) { + constructor(id, depth, parentId, parentFrameId, url, applyFrameOffset=async (x, y) => [x, y]) { this._parentId = parentId; this._parentFrameId = parentFrameId; this._id = id; this._depth = depth; this._url = url; this._apiSender = new FrontendApiSender(); - - this._windowMessageHandlers = new Map([ - ['getIframeOffset', ({offset, uniqueId}, e) => { return this._onGetIframeOffset(offset, uniqueId, e); }] - ]); - - window.addEventListener('message', this.onMessage.bind(this), false); + this._applyFrameOffset = applyFrameOffset; } // Public properties @@ -87,7 +82,7 @@ class PopupProxy { async containsPoint(x, y) { if (this._depth === 0) { - [x, y] = await PopupProxy._convertIframePointToRootPagePoint(x, y); + [x, y] = await this._applyFrameOffset(x, y); } return await this._invokeHostApi('containsPoint', {id: this._id, x, y}); } @@ -95,7 +90,7 @@ class PopupProxy { async showContent(elementRect, writingMode, type=null, details=null) { let {x, y, width, height} = elementRect; if (this._depth === 0) { - [x, y] = await PopupProxy._convertIframePointToRootPagePoint(x, y); + [x, y] = await this._applyFrameOffset(x, y); } elementRect = {x, y, width, height}; return await this._invokeHostApi('showContent', {id: this._id, elementRect, writingMode, type, details}); @@ -113,31 +108,6 @@ class PopupProxy { this._invokeHostApi('setContentScale', {id: this._id, scale}); } - // Window message handlers - - onMessage(e) { - const {action, params} = e.data; - const handler = this._windowMessageHandlers.get(action); - if (typeof handler !== 'function') { return; } - handler(params, e); - } - - _onGetIframeOffset(offset, uniqueId, e) { - let sourceIframe = null; - for (const iframe of document.querySelectorAll('iframe:not(.yomichan-float)')) { - if (iframe.contentWindow !== e.source) { continue; } - sourceIframe = iframe; - break; - } - if (sourceIframe === null) { return; } - - const [forwardedX, forwardedY] = offset; - const {x, y} = sourceIframe.getBoundingClientRect(); - offset = [forwardedX + x, forwardedY + y]; - window.parent.postMessage({action: 'getIframeOffset', params: {offset, uniqueId}}, '*'); - } - - // Private _invokeHostApi(action, params={}) { @@ -146,33 +116,4 @@ class PopupProxy { } return this._apiSender.invoke(action, params, `popup-proxy-host#${this._parentFrameId}`); } - - static async _convertIframePointToRootPagePoint(x, y) { - const uniqueId = yomichan.generateId(16); - - let frameOffsetResolve = null; - const frameOffsetPromise = new Promise((resolve) => (frameOffsetResolve = resolve)); - - const runtimeMessageCallback = ({action, params}, sender, callback) => { - if (action === 'iframeOffset' && isObject(params) && params.uniqueId === uniqueId) { - chrome.runtime.onMessage.removeListener(runtimeMessageCallback); - callback(); - frameOffsetResolve(params); - return false; - } - }; - chrome.runtime.onMessage.addListener(runtimeMessageCallback); - - window.parent.postMessage({ - action: 'getIframeOffset', - params: { - uniqueId, - offset: [x, y] - } - }, '*'); - - const {offset} = await frameOffsetPromise; - - return offset; - } } diff --git a/ext/manifest.json b/ext/manifest.json index 97d59e49..98965389 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -27,6 +27,7 @@ "fg/js/frontend-api-receiver.js", "fg/js/popup.js", "fg/js/source.js", + "fg/js/frame-offset-forwarder.js", "fg/js/popup-proxy.js", "fg/js/popup-proxy-host.js", "fg/js/frontend.js", -- cgit v1.2.3 From 31a326fe636683e71fa61f11ed25b4f2adaead44 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 5 Apr 2020 01:43:12 +0300 Subject: add option for iframe popups --- ext/bg/data/options-schema.json | 7 ++++++- ext/bg/js/options.js | 3 ++- ext/bg/js/settings/main.js | 2 ++ ext/bg/settings.html | 4 ++++ ext/fg/js/frontend-initialize.js | 6 +++++- 5 files changed, 19 insertions(+), 3 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index cb759b72..da1f1ce0 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -108,7 +108,8 @@ "enableClipboardMonitor", "showPitchAccentDownstepNotation", "showPitchAccentPositionNotation", - "showPitchAccentGraph" + "showPitchAccentGraph", + "showIframePopupsInRootFrame" ], "properties": { "enable": { @@ -242,6 +243,10 @@ "showPitchAccentGraph": { "type": "boolean", "default": false + }, + "showIframePopupsInRootFrame": { + "type": "boolean", + "default": false } } }, diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index b36fe812..5c68c403 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -127,7 +127,8 @@ function profileOptionsCreateDefaults() { enableClipboardMonitor: false, showPitchAccentDownstepNotation: true, showPitchAccentPositionNotation: true, - showPitchAccentGraph: false + showPitchAccentGraph: false, + showIframePopupsInRootFrame: false }, audio: { diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 7caeaea0..1653ee35 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -87,6 +87,7 @@ async function formRead(options) { options.general.showPitchAccentDownstepNotation = $('#show-pitch-accent-downstep-notation').prop('checked'); options.general.showPitchAccentPositionNotation = $('#show-pitch-accent-position-notation').prop('checked'); options.general.showPitchAccentGraph = $('#show-pitch-accent-graph').prop('checked'); + options.general.showIframePopupsInRootFrame = $('#show-iframe-popups-in-root-frame').prop('checked'); options.general.popupTheme = $('#popup-theme').val(); options.general.popupOuterTheme = $('#popup-outer-theme').val(); options.general.customPopupCss = $('#custom-popup-css').val(); @@ -167,6 +168,7 @@ async function formWrite(options) { $('#show-pitch-accent-downstep-notation').prop('checked', options.general.showPitchAccentDownstepNotation); $('#show-pitch-accent-position-notation').prop('checked', options.general.showPitchAccentPositionNotation); $('#show-pitch-accent-graph').prop('checked', options.general.showPitchAccentGraph); + $('#show-iframe-popups-in-root-frame').prop('checked', options.general.showIframePopupsInRootFrame); $('#popup-theme').val(options.general.popupTheme); $('#popup-outer-theme').val(options.general.popupOuterTheme); $('#custom-popup-css').val(options.general.customPopupCss); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 0b2e4f9c..237162c7 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -174,6 +174,10 @@ +
+ +
+
diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 2f86f5c8..4a1409db 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -22,6 +22,7 @@ * PopupProxy * PopupProxyHost * apiForward + * apiOptionsGet */ async function main() { @@ -30,8 +31,11 @@ async function main() { const data = window.frontendInitializationData || {}; const {id, depth=0, parentFrameId, url, proxy=false} = data; + const optionsContext = {depth, url}; + const options = await apiOptionsGet(optionsContext); + let popup; - if (!proxy && (window !== window.parent)) { + if (!proxy && (window !== window.parent) && options.general.showIframePopupsInRootFrame) { const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( chrome.runtime.onMessage, ({action, params}, {resolve}) => { -- cgit v1.2.3 From cd831d88cc822e4292bf6d9d97f46f38f52455fa Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 5 Apr 2020 14:45:37 -0400 Subject: Update schema to include additional fields --- ext/bg/data/dictionary-index-schema.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'ext/bg') diff --git a/ext/bg/data/dictionary-index-schema.json b/ext/bg/data/dictionary-index-schema.json index 9311f14c..09cff711 100644 --- a/ext/bg/data/dictionary-index-schema.json +++ b/ext/bg/data/dictionary-index-schema.json @@ -30,6 +30,22 @@ "description": "Alias for format.", "enum": [1, 2, 3] }, + "author": { + "type": "string", + "description": "Creator of the dictionary." + }, + "url": { + "type": "string", + "description": "URL for the source of the dictionary." + }, + "description": { + "type": "string", + "description": "Description of the dictionary data." + }, + "attribution": { + "type": "string", + "description": "Attribution information for the dictionary data." + }, "tagMeta": { "type": "object", "description": "Tag information for terms and kanji. This object is obsolete and individual tag files should be used instead.", -- cgit v1.2.3 From 1b97629cd8014d7523a12ae857305e65a6d06672 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 5 Apr 2020 14:45:54 -0400 Subject: Store new dictionary data --- ext/bg/js/dictionary-importer.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/dictionary-importer.js b/ext/bg/js/dictionary-importer.js index 589e7656..f9e173ea 100644 --- a/ext/bg/js/dictionary-importer.js +++ b/ext/bg/js/dictionary-importer.js @@ -150,13 +150,7 @@ class DictionaryImporter { } // Add dictionary - const summary = { - title: dictionaryTitle, - revision: index.revision, - sequenced: index.sequenced, - version, - prefixWildcardsSupported - }; + const summary = this._createSummary(dictionaryTitle, version, index, {prefixWildcardsSupported}); database.bulkAdd('dictionaries', [summary], 0, 1); @@ -199,6 +193,25 @@ class DictionaryImporter { return {result: summary, errors}; } + _createSummary(dictionaryTitle, version, index, details) { + const summary = { + title: dictionaryTitle, + revision: index.revision, + sequenced: index.sequenced, + version + }; + + const {author, url, description, attribution} = index; + if (typeof author === 'string') { summary.author = author; } + if (typeof url === 'string') { summary.url = url; } + if (typeof description === 'string') { summary.description = description; } + if (typeof attribution === 'string') { summary.attribution = attribution; } + + Object.assign(summary, details); + + return summary; + } + async _getSchema(fileName) { let schemaPromise = this._schemas.get(fileName); if (typeof schemaPromise !== 'undefined') { -- cgit v1.2.3 From dd9d50bfc194aa4c3d8e99188cfac0214476f868 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 5 Apr 2020 14:46:21 -0400 Subject: Update settings page to display additional information --- ext/bg/css/settings.css | 37 ++++++++++++++++++++++++++++ ext/bg/js/settings/dictionaries.js | 50 ++++++++++++++++++++++++++++++++++++++ ext/bg/settings.html | 4 +++ 3 files changed, 91 insertions(+) (limited to 'ext/bg') diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index d686e8f8..6344bd38 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -235,6 +235,43 @@ html:root[data-operating-system=openbsd] [data-hide-for-operating-system~=openbs display: none; } +.dict-details-container { + margin: 0.5em 0; +} + +.dict-details-toggle-link { + cursor: pointer; +} + +.dict-details { + margin-left: 1em; +} + +.dict-details-table { + display: table; + width: 100% +} + +.dict-details-entry { + display: table-row; +} + +.dict-details-entry+.dict-details-entry>* { + padding-top: 0.25em; +} + +.dict-details-entry-label { + display: table-cell; + font-weight: bold; + white-space: nowrap; + padding-right: 0.5em; +} + +.dict-details-entry-info { + display: table-cell; + white-space: pre-line; +} + @media screen and (max-width: 740px) { .col-xs-6 { diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index 5e59cc3d..b9e4fe82 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -199,11 +199,16 @@ class SettingsDictionaryEntryUI { this.allowSecondarySearchesCheckbox = this.content.querySelector('.dict-allow-secondary-searches'); this.priorityInput = this.content.querySelector('.dict-priority'); this.deleteButton = this.content.querySelector('.dict-delete-button'); + this.detailsToggleLink = this.content.querySelector('.dict-details-toggle-link'); + this.detailsContainer = this.content.querySelector('.dict-details'); + this.detailsTable = this.content.querySelector('.dict-details-table'); if (this.dictionaryInfo.version < 3) { this.content.querySelector('.dict-outdated').hidden = false; } + this.setupDetails(dictionaryInfo); + this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title; this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`; this.content.querySelector('.dict-prefix-wildcard-searches-supported').checked = !!this.dictionaryInfo.prefixWildcardsSupported; @@ -214,6 +219,45 @@ class SettingsDictionaryEntryUI { this.eventListeners.addEventListener(this.allowSecondarySearchesCheckbox, 'change', this.onAllowSecondarySearchesChanged.bind(this), false); this.eventListeners.addEventListener(this.priorityInput, 'change', this.onPriorityChanged.bind(this), false); this.eventListeners.addEventListener(this.deleteButton, 'click', this.onDeleteButtonClicked.bind(this), false); + this.eventListeners.addEventListener(this.detailsToggleLink, 'click', this.onDetailsToggleLinkClicked.bind(this), false); + } + + setupDetails(dictionaryInfo) { + const targets = [ + ['Author', 'author'], + ['URL', 'url'], + ['Description', 'description'], + ['Attribution', 'attribution'] + ]; + + let count = 0; + for (const [label, key] of targets) { + const info = dictionaryInfo[key]; + if (typeof info !== 'string') { continue; } + + const n1 = document.createElement('div'); + n1.className = 'dict-details-entry'; + n1.dataset.type = key; + + const n2 = document.createElement('span'); + n2.className = 'dict-details-entry-label'; + n2.textContent = `${label}:`; + n1.appendChild(n2); + + const n3 = document.createElement('span'); + n3.className = 'dict-details-entry-info'; + n3.textContent = info; + n1.appendChild(n3); + + this.detailsTable.appendChild(n1); + + ++count; + } + + if (count === 0) { + this.detailsContainer.hidden = true; + this.detailsToggleLink.hidden = true; + } } cleanup() { @@ -318,6 +362,12 @@ class SettingsDictionaryEntryUI { document.querySelector('#dict-remove-modal-dict-name').textContent = title; $(n).modal('show'); } + + onDetailsToggleLinkClicked(e) { + e.preventDefault(); + + this.detailsContainer.hidden = !this.detailsContainer.hidden; + } } class SettingsDictionaryExtraUI { diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 237162c7..1297a9cc 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -674,6 +674,10 @@ +
+ Details... + +
-- cgit v1.2.3 From 7449ffd4dc74ea79c1e7337a6402b1c697c0a875 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 5 Apr 2020 18:26:38 -0400 Subject: Fix error reporting during dictionary import --- ext/bg/js/dictionary-importer.js | 2 +- ext/bg/js/settings/dictionaries.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/dictionary-importer.js b/ext/bg/js/dictionary-importer.js index 589e7656..607a8b5e 100644 --- a/ext/bg/js/dictionary-importer.js +++ b/ext/bg/js/dictionary-importer.js @@ -180,7 +180,7 @@ class DictionaryImporter { try { await database.bulkAdd(objectStoreName, entries, i, count); } catch (e) { - errors.push(e); + errors.push(errorToJson(e)); } loadedCount += count; diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index 5e59cc3d..ed883869 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -643,9 +643,9 @@ async function onDictionaryImport(e) { await settingsSaveOptions(); if (errors.length > 0) { - errors.push(...errors); - errors.push(`Dictionary may not have been imported properly: ${errors.length} error${errors.length === 1 ? '' : 's'} reported.`); - dictionaryErrorsShow(errors); + const errors2 = errors.map((error) => jsonToError(error)); + errors2.push(`Dictionary may not have been imported properly: ${errors2.length} error${errors2.length === 1 ? '' : 's'} reported.`); + dictionaryErrorsShow(errors2); } onDatabaseUpdated(); -- cgit v1.2.3 From 9dfe531dfd75af0eec26d532a40095fe8a982df1 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 5 Apr 2020 18:27:53 -0400 Subject: Use logError instead of console.log --- ext/bg/js/database.js | 2 +- ext/bg/js/settings/dictionaries.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 7a4d094b..4a677fea 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -99,7 +99,7 @@ class Database { }); return true; } catch (e) { - console.error(e); + logError(e); return false; } } diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index ed883869..00056e2e 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -505,7 +505,7 @@ function dictionaryErrorsShow(errors) { if (errors !== null && errors.length > 0) { const uniqueErrors = new Map(); for (let e of errors) { - console.error(e); + logError(e); e = dictionaryErrorToString(e); let count = uniqueErrors.get(e); if (typeof count === 'undefined') { -- cgit v1.2.3 From 8b07a23de95ded3e6af93c78ab4f7f70cc449ea0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 15 Mar 2020 17:02:34 -0400 Subject: Rename context to details --- ext/bg/js/backend.js | 6 +++--- ext/mixed/js/api.js | 4 ++-- ext/mixed/js/display.js | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 1e8c979f..e7ae7026 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -455,7 +455,7 @@ class Backend { return results; } - async _onApiDefinitionAdd({definition, mode, context, optionsContext}) { + async _onApiDefinitionAdd({definition, mode, details, optionsContext}) { const options = this.getOptions(optionsContext); const templates = this.defaultAnkiFieldTemplates; @@ -468,11 +468,11 @@ class Backend { ); } - if (context && context.screenshot) { + if (details && details.screenshot) { await this._injectScreenshot( definition, options.anki.terms.fields, - context.screenshot + details.screenshot ); } diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 0ab07039..df6a93f5 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -53,8 +53,8 @@ function apiKanjiFind(text, optionsContext) { return _apiInvoke('kanjiFind', {text, optionsContext}); } -function apiDefinitionAdd(definition, mode, context, optionsContext) { - return _apiInvoke('definitionAdd', {definition, mode, context, optionsContext}); +function apiDefinitionAdd(definition, mode, details, optionsContext) { + return _apiInvoke('definitionAdd', {definition, mode, details, optionsContext}); } function apiDefinitionsAddable(definitions, modes, optionsContext) { diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 4a71efe0..8587657f 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -752,15 +752,15 @@ class Display { try { this.setSpinnerVisible(true); - const context = {}; + const details = {}; if (this.noteUsesScreenshot(mode)) { const screenshot = await this.getScreenshot(); if (screenshot) { - context.screenshot = screenshot; + details.screenshot = screenshot; } } - const noteId = await apiDefinitionAdd(definition, mode, context, this.getOptionsContext()); + const noteId = await apiDefinitionAdd(definition, mode, details, this.getOptionsContext()); if (noteId) { const index = this.definitions.indexOf(definition); const adderButton = this.adderButtonFind(index, mode); -- cgit v1.2.3 From 059db280bba858a3cab3a542aef13f19737aaf6e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 15 Mar 2020 17:13:00 -0400 Subject: Add custom context object for note creation --- ext/bg/js/anki-note-builder.js | 9 +++++---- ext/bg/js/backend.js | 8 ++++---- ext/bg/js/settings/anki-templates.js | 3 ++- ext/mixed/js/api.js | 8 ++++---- ext/mixed/js/display.js | 8 ++++++-- 5 files changed, 21 insertions(+), 15 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index d0ff8205..51022da3 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -21,7 +21,7 @@ class AnkiNoteBuilder { this._renderTemplate = renderTemplate; } - async createNote(definition, mode, options, templates) { + async createNote(definition, mode, context, options, templates) { const isKanji = (mode === 'kanji'); const tags = options.anki.tags; const modeOptions = isKanji ? options.anki.kanji : options.anki.terms; @@ -35,7 +35,7 @@ class AnkiNoteBuilder { }; for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { - note.fields[fieldName] = await this.formatField(fieldValue, definition, mode, options, templates, null); + note.fields[fieldName] = await this.formatField(fieldValue, definition, mode, context, options, templates, null); } if (!isKanji && definition.audio) { @@ -60,7 +60,7 @@ class AnkiNoteBuilder { return note; } - async formatField(field, definition, mode, options, templates, errors=null) { + async formatField(field, definition, mode, context, options, templates, errors=null) { const data = { marker: null, definition, @@ -69,7 +69,8 @@ class AnkiNoteBuilder { modeTermKanji: mode === 'term-kanji', modeTermKana: mode === 'term-kana', modeKanji: mode === 'kanji', - compactGlossaries: options.general.compactGlossaries + compactGlossaries: options.general.compactGlossaries, + context }; const pattern = /\{([\w-]+)\}/g; return await AnkiNoteBuilder.stringReplaceAsync(field, pattern, async (g0, marker) => { diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index e7ae7026..d4c822ca 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -455,7 +455,7 @@ class Backend { return results; } - async _onApiDefinitionAdd({definition, mode, details, optionsContext}) { + async _onApiDefinitionAdd({definition, mode, context, details, optionsContext}) { const options = this.getOptions(optionsContext); const templates = this.defaultAnkiFieldTemplates; @@ -476,11 +476,11 @@ class Backend { ); } - const note = await this.ankiNoteBuilder.createNote(definition, mode, options, templates); + const note = await this.ankiNoteBuilder.createNote(definition, mode, context, options, templates); return this.anki.addNote(note); } - async _onApiDefinitionsAddable({definitions, modes, optionsContext}) { + async _onApiDefinitionsAddable({definitions, modes, context, optionsContext}) { const options = this.getOptions(optionsContext); const templates = this.defaultAnkiFieldTemplates; const states = []; @@ -489,7 +489,7 @@ class Backend { const notes = []; for (const definition of definitions) { for (const mode of modes) { - const note = await this.ankiNoteBuilder.createNote(definition, mode, options, templates); + const note = await this.ankiNoteBuilder.createNote(definition, mode, context, options, templates); notes.push(note); } } diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index c5222d30..88bca024 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -99,10 +99,11 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext); if (definition !== null) { const options = await apiOptionsGet(optionsContext); + const context = {}; let templates = options.anki.fieldTemplates; if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); } const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: apiTemplateRender}); - result = await ankiNoteBuilder.formatField(field, definition, mode, options, templates, exceptions); + result = await ankiNoteBuilder.formatField(field, definition, mode, context, options, templates, exceptions); } } catch (e) { exceptions.push(e); diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index df6a93f5..feec94df 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -53,12 +53,12 @@ function apiKanjiFind(text, optionsContext) { return _apiInvoke('kanjiFind', {text, optionsContext}); } -function apiDefinitionAdd(definition, mode, details, optionsContext) { - return _apiInvoke('definitionAdd', {definition, mode, details, optionsContext}); +function apiDefinitionAdd(definition, mode, context, details, optionsContext) { + return _apiInvoke('definitionAdd', {definition, mode, context, details, optionsContext}); } -function apiDefinitionsAddable(definitions, modes, optionsContext) { - return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext}); +function apiDefinitionsAddable(definitions, modes, context, optionsContext) { + return _apiInvoke('definitionsAddable', {definitions, modes, context, optionsContext}); } function apiNoteView(noteId) { diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 8587657f..ecf92013 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -760,7 +760,7 @@ class Display { } } - const noteId = await apiDefinitionAdd(definition, mode, details, this.getOptionsContext()); + const noteId = await apiDefinitionAdd(definition, mode, this._getNoteContext(), details, this.getOptionsContext()); if (noteId) { const index = this.definitions.indexOf(definition); const adderButton = this.adderButtonFind(index, mode); @@ -908,7 +908,7 @@ class Display { async getDefinitionsAddable(definitions, modes) { try { - return await apiDefinitionsAddable(definitions, modes, this.getOptionsContext()); + return await apiDefinitionsAddable(definitions, modes, this._getNoteContext(), this.getOptionsContext()); } catch (e) { return []; } @@ -934,6 +934,10 @@ class Display { return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : ''); } + _getNoteContext() { + return {}; + } + async _getAudioUri(definition, source) { const optionsContext = this.getOptionsContext(); return await apiAudioGetUri(definition, source, optionsContext); -- cgit v1.2.3 From 4011a091b69475e7096d80103b4dad9aa1b8d80b Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 15 Mar 2020 17:32:31 -0400 Subject: Add support for {document-title} --- README.md | 2 ++ ext/bg/data/default-anki-field-templates.handlebars | 4 ++++ ext/bg/js/options.js | 9 +++++++++ ext/bg/js/settings/anki-templates.js | 6 +++++- ext/mixed/js/display.js | 6 +++++- 5 files changed, 25 insertions(+), 2 deletions(-) (limited to 'ext/bg') diff --git a/README.md b/README.md index 631f5a8b..bf217679 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Flashcard fields can be configured with the following steps: `{cloze-prefix}` | Text for the containing `{sentence}` from the start up to the value of `{cloze-body}`. `{cloze-suffix}` | Text for the containing `{sentence}` from the value of `{cloze-body}` to the end. `{dictionary}` | Name of the dictionary from which the card is being created (unavailable in *grouped* mode). + `{document-title}` | Title of the web page that the term appeared in. `{expression}` | Term expressed as Kanji (will be displayed in Kana if Kanji is not available). `{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. 日本語[にほんご]). @@ -175,6 +176,7 @@ Flashcard fields can be configured with the following steps: `{cloze-prefix}` | Text for the containing `{sentence}` from the start up to the value of `{cloze-body}`. `{cloze-suffix}` | Text for the containing `{sentence}` from the value of `{cloze-body}` to the end. `{dictionary}` | Name of the dictionary from which the card is being created. + `{document-title}` | Title of the web page that the Kanji appeared in. `{glossary}` | List of definitions for the Kanji. `{kunyomi}` | Kunyomi (Japanese reading) for the Kanji expressed as Katakana. `{onyomi}` | Onyomi (Chinese reading) for the Kanji expressed as Hiragana. diff --git a/ext/bg/data/default-anki-field-templates.handlebars b/ext/bg/data/default-anki-field-templates.handlebars index 0442f7c5..6061851f 100644 --- a/ext/bg/data/default-anki-field-templates.handlebars +++ b/ext/bg/data/default-anki-field-templates.handlebars @@ -158,4 +158,8 @@ {{/inline}} +{{#*inline "document-title"}} + {{~context.document.title~}} +{{/inline}} + {{~> (lookup . "marker") ~}} \ No newline at end of file diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 5c68c403..abb054d4 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -91,6 +91,15 @@ const profileOptionsVersionUpdates = [ if (utilStringHashCode(options.anki.fieldTemplates) === 1444379824) { options.anki.fieldTemplates = null; } + }, + (options) => { + // Version 13 changes: + // Default anki field tempaltes updated to include {document-title}. + let fieldTemplates = options.anki.fieldTemplates; + if (typeof fieldTemplates === 'string') { + fieldTemplates += '\n\n{{#*inline "document-title"}}\n {{~context.document.title~}}\n{{/inline}}'; + options.anki.fieldTemplates = fieldTemplates; + } } ]; diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index 88bca024..e3852eb4 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -99,7 +99,11 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext); if (definition !== null) { const options = await apiOptionsGet(optionsContext); - const context = {}; + const context = { + document: { + title: document.title + } + }; let templates = options.anki.fieldTemplates; if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); } const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: apiTemplateRender}); diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index ecf92013..646d60e7 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -935,7 +935,11 @@ class Display { } _getNoteContext() { - return {}; + return { + document: { + title: document.title + } + }; } async _getAudioUri(definition, source) { -- cgit v1.2.3 From 1d7c86ded0a3ca559342dc394696cdc2504d28c4 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 27 Mar 2020 21:58:24 -0400 Subject: Add document-title tag to settings options --- ext/bg/js/settings/anki.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ext/bg') diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index b706cd1b..f2e1ca76 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -243,6 +243,7 @@ function ankiGetFieldMarkers(type) { 'cloze-prefix', 'cloze-suffix', 'dictionary', + 'document-title', 'expression', 'furigana', 'furigana-plain', @@ -258,6 +259,7 @@ function ankiGetFieldMarkers(type) { return [ 'character', 'dictionary', + 'document-title', 'glossary', 'kunyomi', 'onyomi', -- cgit v1.2.3 From efcdff72a3d98b90a17a0c563ef46a0a8f76bf20 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 1 Apr 2020 21:09:49 -0400 Subject: Move media injection functions into AnkiNoteBuilder --- ext/bg/js/anki-note-builder.js | 76 ++++++++++++++++++++++++++++++++++++++ ext/bg/js/backend.js | 84 +++--------------------------------------- 2 files changed, 81 insertions(+), 79 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 51022da3..d34fc66e 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -84,6 +84,82 @@ class AnkiNoteBuilder { }); } + async injectAudio(definition, fields, sources, audioSystem, optionsContext) { + let usesAudio = false; + for (const fieldValue of Object.values(fields)) { + if (fieldValue.includes('{audio}')) { + usesAudio = true; + break; + } + } + + if (!usesAudio) { + return true; + } + + try { + const expressions = definition.expressions; + const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition; + + const {uri} = await audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext}); + const filename = this._createInjectedAudioFileName(audioSourceDefinition); + if (filename !== null) { + definition.audio = {url: uri, filename}; + } + + return true; + } catch (e) { + return false; + } + } + + async injectScreenshot(definition, fields, screenshot, anki) { + let usesScreenshot = false; + for (const fieldValue of Object.values(fields)) { + if (fieldValue.includes('{screenshot}')) { + usesScreenshot = true; + break; + } + } + + if (!usesScreenshot) { + return; + } + + const dateToString = (date) => { + const year = date.getUTCFullYear(); + const month = date.getUTCMonth().toString().padStart(2, '0'); + const day = date.getUTCDate().toString().padStart(2, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; + }; + + const now = new Date(Date.now()); + const filename = `yomichan_browser_screenshot_${definition.reading}_${dateToString(now)}.${screenshot.format}`; + const data = screenshot.dataUrl.replace(/^data:[\w\W]*?,/, ''); + + try { + await anki.storeMediaFile(filename, data); + } catch (e) { + return; + } + + definition.screenshotFileName = filename; + } + + _createInjectedAudioFileName(definition) { + const {reading, expression} = definition; + if (!reading && !expression) { return null; } + + let filename = 'yomichan'; + if (reading) { filename += `_${reading}`; } + if (expression) { filename += `_${expression}`; } + filename += '.mp3'; + return filename; + } + static stringReplaceAsync(str, regex, replacer) { let match; let index = 0; diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index d4c822ca..9e02cced 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -460,19 +460,21 @@ class Backend { const templates = this.defaultAnkiFieldTemplates; if (mode !== 'kanji') { - await this._audioInject( + await this.ankiNoteBuilder.injectAudio( definition, options.anki.terms.fields, options.audio.sources, + this.audioSystem, optionsContext ); } if (details && details.screenshot) { - await this._injectScreenshot( + await this.ankiNoteBuilder.injectScreenshot( definition, options.anki.terms.fields, - details.screenshot + details.screenshot, + this.anki ); } @@ -800,86 +802,10 @@ class Backend { return await this.audioUriBuilder.getUri(definition, source, options); } - async _audioInject(definition, fields, sources, optionsContext) { - let usesAudio = false; - for (const fieldValue of Object.values(fields)) { - if (fieldValue.includes('{audio}')) { - usesAudio = true; - break; - } - } - - if (!usesAudio) { - return true; - } - - try { - const expressions = definition.expressions; - const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition; - - const {uri} = await this.audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext}); - const filename = this._createInjectedAudioFileName(audioSourceDefinition); - if (filename !== null) { - definition.audio = {url: uri, filename}; - } - - return true; - } catch (e) { - return false; - } - } - - async _injectScreenshot(definition, fields, screenshot) { - let usesScreenshot = false; - for (const fieldValue of Object.values(fields)) { - if (fieldValue.includes('{screenshot}')) { - usesScreenshot = true; - break; - } - } - - if (!usesScreenshot) { - return; - } - - const dateToString = (date) => { - const year = date.getUTCFullYear(); - const month = date.getUTCMonth().toString().padStart(2, '0'); - const day = date.getUTCDate().toString().padStart(2, '0'); - const hours = date.getUTCHours().toString().padStart(2, '0'); - const minutes = date.getUTCMinutes().toString().padStart(2, '0'); - const seconds = date.getUTCSeconds().toString().padStart(2, '0'); - return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; - }; - - const now = new Date(Date.now()); - const filename = `yomichan_browser_screenshot_${definition.reading}_${dateToString(now)}.${screenshot.format}`; - const data = screenshot.dataUrl.replace(/^data:[\w\W]*?,/, ''); - - try { - await this.anki.storeMediaFile(filename, data); - } catch (e) { - return; - } - - definition.screenshotFileName = filename; - } - async _renderTemplate(template, data) { return handlebarsRenderDynamic(template, data); } - _createInjectedAudioFileName(definition) { - const {reading, expression} = definition; - if (!reading && !expression) { return null; } - - let filename = 'yomichan'; - if (reading) { filename += `_${reading}`; } - if (expression) { filename += `_${expression}`; } - filename += '.mp3'; - return filename; - } - static _getTabUrl(tab) { return new Promise((resolve) => { chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => { -- cgit v1.2.3 From a49e061545ef1d794f5f30d609168afbea9fdf6c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 1 Apr 2020 21:11:01 -0400 Subject: Move _dateToString into a new function --- ext/bg/js/anki-note-builder.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index d34fc66e..595d56a8 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -126,18 +126,8 @@ class AnkiNoteBuilder { return; } - const dateToString = (date) => { - const year = date.getUTCFullYear(); - const month = date.getUTCMonth().toString().padStart(2, '0'); - const day = date.getUTCDate().toString().padStart(2, '0'); - const hours = date.getUTCHours().toString().padStart(2, '0'); - const minutes = date.getUTCMinutes().toString().padStart(2, '0'); - const seconds = date.getUTCSeconds().toString().padStart(2, '0'); - return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; - }; - const now = new Date(Date.now()); - const filename = `yomichan_browser_screenshot_${definition.reading}_${dateToString(now)}.${screenshot.format}`; + const filename = `yomichan_browser_screenshot_${definition.reading}_${this._dateToString(now)}.${screenshot.format}`; const data = screenshot.dataUrl.replace(/^data:[\w\W]*?,/, ''); try { @@ -160,6 +150,16 @@ class AnkiNoteBuilder { return filename; } + _dateToString(date) { + const year = date.getUTCFullYear(); + const month = date.getUTCMonth().toString().padStart(2, '0'); + const day = date.getUTCDate().toString().padStart(2, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; + } + static stringReplaceAsync(str, regex, replacer) { let match; let index = 0; -- cgit v1.2.3 From 97b7b521dd73251dcfb0799793929395e03c8328 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 1 Apr 2020 21:12:59 -0400 Subject: Create _containsMarker to reduce redundant code --- ext/bg/js/anki-note-builder.js | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 595d56a8..e396bec6 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -85,17 +85,7 @@ class AnkiNoteBuilder { } async injectAudio(definition, fields, sources, audioSystem, optionsContext) { - let usesAudio = false; - for (const fieldValue of Object.values(fields)) { - if (fieldValue.includes('{audio}')) { - usesAudio = true; - break; - } - } - - if (!usesAudio) { - return true; - } + if (!this._containsMarker(fields, 'audio')) { return; } try { const expressions = definition.expressions; @@ -114,17 +104,7 @@ class AnkiNoteBuilder { } async injectScreenshot(definition, fields, screenshot, anki) { - let usesScreenshot = false; - for (const fieldValue of Object.values(fields)) { - if (fieldValue.includes('{screenshot}')) { - usesScreenshot = true; - break; - } - } - - if (!usesScreenshot) { - return; - } + if (!this._containsMarker(fields, 'screenshot')) { return; } const now = new Date(Date.now()); const filename = `yomichan_browser_screenshot_${definition.reading}_${this._dateToString(now)}.${screenshot.format}`; @@ -160,6 +140,16 @@ class AnkiNoteBuilder { return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; } + _containsMarker(fields, marker) { + marker = `{${marker}}`; + for (const fieldValue of Object.values(fields)) { + if (fieldValue.includes(marker)) { + return true; + } + } + return false; + } + static stringReplaceAsync(str, regex, replacer) { let match; let index = 0; -- cgit v1.2.3 From 716ab99fc04e0a02e24d3de20cdf0d3a368c1bf0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 1 Apr 2020 21:13:55 -0400 Subject: Remove inconsistent/unused return value --- ext/bg/js/anki-note-builder.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index e396bec6..0e17783b 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -96,10 +96,8 @@ class AnkiNoteBuilder { if (filename !== null) { definition.audio = {url: uri, filename}; } - - return true; } catch (e) { - return false; + // NOP } } -- cgit v1.2.3 From 8a419dfa67b730d777fbefaf9c6ffa649bbb67d3 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 5 Apr 2020 19:34:31 -0400 Subject: Pass AudioSystem instance to AnkiNoteBuilder constructor --- ext/bg/js/anki-note-builder.js | 7 ++++--- ext/bg/js/backend.js | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) (limited to 'ext/bg') diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 0e17783b..1ccec20c 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -17,7 +17,8 @@ */ class AnkiNoteBuilder { - constructor({renderTemplate}) { + constructor({audioSystem, renderTemplate}) { + this._audioSystem = audioSystem; this._renderTemplate = renderTemplate; } @@ -84,14 +85,14 @@ class AnkiNoteBuilder { }); } - async injectAudio(definition, fields, sources, audioSystem, optionsContext) { + async injectAudio(definition, fields, sources, optionsContext) { if (!this._containsMarker(fields, 'audio')) { return; } try { const expressions = definition.expressions; const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition; - const {uri} = await audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext}); + const {uri} = await this.audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext}); const filename = this._createInjectedAudioFileName(audioSourceDefinition); if (filename !== null) { definition.audio = {url: uri, filename}; diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 9e02cced..1fa7ede1 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -51,12 +51,16 @@ class Backend { this.anki = new AnkiNull(); this.mecab = new Mecab(); this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); - this.ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: this._renderTemplate.bind(this)}); this.options = null; this.optionsSchema = null; this.defaultAnkiFieldTemplates = null; this.audioSystem = new AudioSystem({getAudioUri: this._getAudioUri.bind(this)}); this.audioUriBuilder = new AudioUriBuilder(); + this.ankiNoteBuilder = new AnkiNoteBuilder({ + audioSystem: this.audioSystem, + renderTemplate: this._renderTemplate.bind(this) + }); + this.optionsContext = { depth: 0, url: window.location.href @@ -464,7 +468,6 @@ class Backend { definition, options.anki.terms.fields, options.audio.sources, - this.audioSystem, optionsContext ); } -- cgit v1.2.3 From a6773e0240f9cf25913d20ac75352b42d5c4e517 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 6 Apr 2020 17:34:36 -0400 Subject: Fix field name --- ext/bg/js/anki-note-builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ext/bg') diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 1ccec20c..244aaab8 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -92,7 +92,7 @@ class AnkiNoteBuilder { const expressions = definition.expressions; const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition; - const {uri} = await this.audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext}); + const {uri} = await this._audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext}); const filename = this._createInjectedAudioFileName(audioSourceDefinition); if (filename !== null) { definition.audio = {url: uri, filename}; -- cgit v1.2.3