From 8106f4744b07833526d16acf656eda11d29b99ad Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 1 Mar 2020 22:36:42 -0500 Subject: Add support for importing and storing media files --- ext/bg/background.html | 1 + 1 file changed, 1 insertion(+) (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index afe9c5d1..f1006f8d 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -36,6 +36,7 @@ + -- cgit v1.2.3 From 3c8eb9eee009ebe265fbae3f7d7ac0d74fcbdd94 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 19 Apr 2020 14:26:44 -0400 Subject: Create background-main.js --- ext/bg/background.html | 3 ++- ext/bg/js/backend.js | 3 --- ext/bg/js/background-main.js | 27 +++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 ext/bg/js/background-main.js (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index f1006f8d..3446d9ce 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -24,6 +24,7 @@ + @@ -45,6 +46,6 @@ - + diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 8a19203f..d23fda10 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -1054,6 +1054,3 @@ class Backend { } } } - -window.yomichanBackend = new Backend(); -window.yomichanBackend.prepare(); diff --git a/ext/bg/js/background-main.js b/ext/bg/js/background-main.js new file mode 100644 index 00000000..c000c38d --- /dev/null +++ b/ext/bg/js/background-main.js @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * 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 + * Backend + */ + +async function main() { + window.yomichanBackend = new Backend(); + await window.yomichanBackend.prepare(); +} + +main(); -- cgit v1.2.3 From 4e0fc76fe1d4bf38cfbef370fcc55331108e0e0a Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Apr 2020 16:56:14 -0400 Subject: Add Yomichan icon SVG (#478) * Create Yomichan icon SVG and 32x32 PNG * Update icons * Add 32x32 icon definition to HTML pages --- ext/bg/background.html | 1 + ext/bg/context.html | 1 + ext/bg/guide.html | 3 ++- ext/bg/legal.html | 1 + ext/bg/search.html | 1 + ext/bg/settings-popup-preview.html | 1 + ext/bg/settings.html | 1 + ext/fg/float.html | 1 + ext/manifest.json | 21 ++++++++++++++-- ext/mixed/img/icon32.png | Bin 0 -> 288 bytes ext/mixed/img/yomichan-icon.svg | 5 ++++ resources/icons.svg | 49 +++++++++++++++++++++++++++++-------- 12 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 ext/mixed/img/icon32.png create mode 100644 ext/mixed/img/yomichan-icon.svg (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index 3446d9ce..ee5a1f32 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -6,6 +6,7 @@ Background + diff --git a/ext/bg/context.html b/ext/bg/context.html index 394869b1..93012d70 100644 --- a/ext/bg/context.html +++ b/ext/bg/context.html @@ -5,6 +5,7 @@ + diff --git a/ext/bg/guide.html b/ext/bg/guide.html index ff9c71ee..cde520d1 100644 --- a/ext/bg/guide.html +++ b/ext/bg/guide.html @@ -6,6 +6,7 @@ Welcome to Yomichan! + @@ -25,7 +26,7 @@

    -
  1. Click on the icon in the browser toolbar to open the Yomichan actions dialog.
  2. +
  3. Click on the icon in the browser toolbar to open the Yomichan actions dialog.
  4. Click on the monkey wrench icon in the middle to open the options page.
  5. Import the dictionaries you wish to use for term and Kanji searches.
  6. Hold down Shift key or the middle mouse button as you move your mouse over text to display definitions.
  7. diff --git a/ext/bg/legal.html b/ext/bg/legal.html index 78acf79a..1ee9a28c 100644 --- a/ext/bg/legal.html +++ b/ext/bg/legal.html @@ -6,6 +6,7 @@ Yomichan Legal + diff --git a/ext/bg/search.html b/ext/bg/search.html index 9a824776..8ed6c838 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -6,6 +6,7 @@ Yomichan Search + diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index 66475b7c..a332fb22 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -6,6 +6,7 @@ Yomichan Popup Preview + diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 96c1db82..f0236193 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -6,6 +6,7 @@ Yomichan Options + diff --git a/ext/fg/float.html b/ext/fg/float.html index 07c3c9e6..deb9e9d2 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -6,6 +6,7 @@ + diff --git a/ext/manifest.json b/ext/manifest.json index 4f35b03c..3cb634f0 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -4,9 +4,26 @@ "version": "20.4.18.0", "description": "Japanese dictionary with Anki integration", - "icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"}, + "icons": { + "16": "mixed/img/icon16.png", + "19": "mixed/img/icon19.png", + "32": "mixed/img/icon32.png", + "38": "mixed/img/icon38.png", + "48": "mixed/img/icon48.png", + "64": "mixed/img/icon48.png", + "128": "mixed/img/icon128.png" + }, "browser_action": { - "default_icon": {"19": "mixed/img/icon19.png", "38": "mixed/img/icon38.png"}, + "default_icon": { + "16": "mixed/img/icon16.png", + "19": "mixed/img/icon19.png", + "32": "mixed/img/icon32.png", + "38": "mixed/img/icon38.png", + "48": "mixed/img/icon48.png", + "64": "mixed/img/icon48.png", + "128": "mixed/img/icon128.png" + }, + "default_title": "Yomichan", "default_popup": "bg/context.html" }, diff --git a/ext/mixed/img/icon32.png b/ext/mixed/img/icon32.png new file mode 100644 index 00000000..05f2f064 Binary files /dev/null and b/ext/mixed/img/icon32.png differ diff --git a/ext/mixed/img/yomichan-icon.svg b/ext/mixed/img/yomichan-icon.svg new file mode 100644 index 00000000..f15ab0aa --- /dev/null +++ b/ext/mixed/img/yomichan-icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/icons.svg b/resources/icons.svg index 4bc46c02..f096947b 100644 --- a/resources/icons.svg +++ b/resources/icons.svg @@ -15,10 +15,11 @@ viewBox="0 0 16 16" version="1.1" id="svg8" - inkscape:version="0.92.3 (2405546, 2018-03-11)" + inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="icons.svg" - inkscape:export-xdpi="96" - inkscape:export-ydpi="96"> + inkscape:export-xdpi="192" + inkscape:export-ydpi="192" + inkscape:export-filename="../ext/mixed/img/icon32.png"> image/svg+xml - + @@ -520,7 +521,7 @@ style="image-rendering:optimizeSpeed" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlko qCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94V TUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpK tE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw 9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0Hgl WNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1V MzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8 w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9 XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVP C4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnj ahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQy u/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg== " id="image4539" - x="4.7683716e-007" + x="4.7683716e-07" y="292.76669" /> + + + + + + + + -- cgit v1.2.3 From bb2d9501afc0e406b0dacf5675cd90985238be98 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 6 May 2020 19:32:28 -0400 Subject: Add apiModifySettings (#501) * Update getProfile/getProfileFromContext to store this.options in a variable * Add useSchema parameter to options getter functions * Add apiModifySettings * Use apiModifySettings instead of apiOptionsSet * Remove apiOptionsSet * Fix incorrect deleteCount check * Require explicit scope for options * Throw on invalid scope --- ext/bg/background.html | 1 + ext/bg/js/backend.js | 138 ++++++++++++++++++++++++--------------- ext/bg/js/search-query-parser.js | 22 +++++-- ext/bg/js/search.js | 30 +++++++-- ext/mixed/js/api.js | 8 +-- 5 files changed, 132 insertions(+), 67 deletions(-) (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index ee5a1f32..9c740adf 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -46,6 +46,7 @@ + diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index d454aa22..8677e04c 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -26,6 +26,7 @@ * DictionaryImporter * JsonSchema * Mecab + * ObjectPropertyAccessor * Translator * conditionsTestValue * dictTermsSort @@ -84,7 +85,6 @@ class Backend { ['optionsSchemaGet', {handler: this._onApiOptionsSchemaGet.bind(this), async: false}], ['optionsGet', {handler: this._onApiOptionsGet.bind(this), async: false}], ['optionsGetFull', {handler: this._onApiOptionsGetFull.bind(this), async: false}], - ['optionsSet', {handler: this._onApiOptionsSet.bind(this), async: true}], ['optionsSave', {handler: this._onApiOptionsSave.bind(this), async: true}], ['kanjiFind', {handler: this._onApiKanjiFind.bind(this), async: true}], ['termsFind', {handler: this._onApiTermsFind.bind(this), async: true}], @@ -115,7 +115,8 @@ class Backend { ['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}], ['log', {handler: this._onApiLog.bind(this), async: false}], ['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}], - ['createActionPort', {handler: this._onApiCreateActionPort.bind(this), async: false}] + ['createActionPort', {handler: this._onApiCreateActionPort.bind(this), async: false}], + ['modifySettings', {handler: this._onApiModifySettings.bind(this), async: true}] ]); this._messageHandlersWithProgress = new Map([ ['importDictionaryArchive', {handler: this._onApiImportDictionaryArchive.bind(this), async: true}], @@ -258,8 +259,9 @@ class Backend { return this.optionsSchema; } - getFullOptions() { - return this.options; + getFullOptions(useSchema=false) { + const options = this.options; + return useSchema ? JsonSchema.createProxy(options, this.optionsSchema) : options; } setFullOptions(options) { @@ -271,21 +273,22 @@ class Backend { } } - getOptions(optionsContext) { - return this.getProfile(optionsContext).options; + getOptions(optionsContext, useSchema=false) { + return this.getProfile(optionsContext, useSchema).options; } - getProfile(optionsContext) { - const profiles = this.options.profiles; + getProfile(optionsContext, useSchema=false) { + const options = this.getFullOptions(useSchema); + const profiles = options.profiles; if (typeof optionsContext.index === 'number') { return profiles[optionsContext.index]; } - const profile = this.getProfileFromContext(optionsContext); - return profile !== null ? profile : this.options.profiles[this.options.profileCurrent]; + const profile = this.getProfileFromContext(options, optionsContext); + return profile !== null ? profile : options.profiles[options.profileCurrent]; } - getProfileFromContext(optionsContext) { - for (const profile of this.options.profiles) { + getProfileFromContext(options, optionsContext) { + for (const profile of options.profiles) { const conditionGroups = profile.conditionGroups; if (conditionGroups.length > 0 && Backend.testConditionGroups(conditionGroups, optionsContext)) { return profile; @@ -413,46 +416,6 @@ class Backend { return this.getFullOptions(); } - async _onApiOptionsSet({changedOptions, optionsContext, source}) { - const options = this.getOptions(optionsContext); - - function getValuePaths(obj) { - const valuePaths = []; - const nodes = [{obj, path: []}]; - while (nodes.length > 0) { - const node = nodes.pop(); - for (const key of Object.keys(node.obj)) { - const path = node.path.concat(key); - const obj2 = node.obj[key]; - if (obj2 !== null && typeof obj2 === 'object') { - nodes.unshift({obj: obj2, path}); - } else { - valuePaths.push([obj2, path]); - } - } - } - return valuePaths; - } - - function modifyOption(path, value) { - let pivot = options; - for (const key of path.slice(0, -1)) { - if (!hasOwn(pivot, key)) { - return false; - } - pivot = pivot[key]; - } - pivot[path[path.length - 1]] = value; - return true; - } - - for (const [value, path] of getValuePaths(changedOptions)) { - modifyOption(path, value); - } - - await this._onApiOptionsSave({source}); - } - async _onApiOptionsSave({source}) { const options = this.getFullOptions(); await optionsSave(options); @@ -829,6 +792,20 @@ class Backend { await this.database.deleteDictionary(dictionaryName, {rate: 1000}, onProgress); } + async _onApiModifySettings({targets, source}) { + const results = []; + for (const target of targets) { + try { + this._modifySetting(target); + results.push({result: true}); + } catch (e) { + results.push({error: errorToJson(e)}); + } + } + await this._onApiOptionsSave({source}); + return results; + } + // Command handlers _createActionListenerPort(port, sender, handlers) { @@ -988,6 +965,63 @@ class Backend { // Utilities + _getModifySettingObject(target) { + const scope = target.scope; + switch (scope) { + case 'profile': + if (!isObject(target.optionsContext)) { throw new Error('Invalid optionsContext'); } + return this.getOptions(target.optionsContext, true); + case 'global': + return this.getFullOptions(true); + default: + throw new Error(`Invalid scope: ${scope}`); + } + } + + async _modifySetting(target) { + const options = this._getModifySettingObject(target); + const accessor = new ObjectPropertyAccessor(options); + const action = target.action; + switch (action) { + case 'set': + { + const {path, value} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + accessor.set(ObjectPropertyAccessor.getPathArray(path), value); + } + break; + case 'delete': + { + const {path} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + accessor.delete(ObjectPropertyAccessor.getPathArray(path)); + } + break; + case 'swap': + { + const {path1, path2} = target; + if (typeof path1 !== 'string') { throw new Error('Invalid path1'); } + if (typeof path2 !== 'string') { throw new Error('Invalid path2'); } + accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2)); + } + break; + case 'splice': + { + const {path, start, deleteCount, items} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); } + if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); } + if (!Array.isArray(items)) { throw new Error('Invalid items'); } + const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); + if (!Array.isArray(array)) { throw new Error('Invalid target type'); } + array.splice(start, deleteCount, ...items); + } + break; + default: + throw new Error(`Unknown action: ${action}`); + } + } + _validatePrivilegedMessageSender(sender) { const url = sender.url; if (!(typeof url === 'string' && yomichan.isExtensionUrl(url))) { diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 935f01f2..1c89583f 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -18,7 +18,7 @@ /* global * QueryParserGenerator * TextScanner - * apiOptionsSet + * apiModifySettings * apiTermsFind * apiTextParse * docSentenceExtract @@ -72,8 +72,14 @@ class QueryParser extends TextScanner { } onParserChange(e) { - const selectedParser = e.target.value; - apiOptionsSet({parsing: {selectedParser}}, this.getOptionsContext()); + const value = e.target.value; + apiModifySettings([{ + action: 'set', + path: 'parsing.selectedParser', + value, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } getMouseEventListeners() { @@ -92,8 +98,14 @@ class QueryParser extends TextScanner { refreshSelectedParser() { if (this.parseResults.length > 0) { if (!this.getParseResult()) { - const selectedParser = this.parseResults[0].id; - apiOptionsSet({parsing: {selectedParser}}, this.getOptionsContext()); + const value = this.parseResults[0].id; + apiModifySettings([{ + action: 'set', + path: 'parsing.selectedParser', + value, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } } } diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index d69daea6..96e8a70b 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -21,7 +21,7 @@ * Display * QueryParser * apiClipboardGet - * apiOptionsSet + * apiModifySettings * apiTermsFind * wanakana */ @@ -252,13 +252,19 @@ class DisplaySearch extends Display { } onWanakanaEnableChange(e) { - const enableWanakana = e.target.checked; - if (enableWanakana) { + const value = e.target.checked; + if (value) { wanakana.bind(this.query); } else { wanakana.unbind(this.query); } - apiOptionsSet({general: {enableWanakana}}, this.getOptionsContext()); + apiModifySettings([{ + action: 'set', + path: 'general.enableWanakana', + value, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } onClipboardMonitorEnableChange(e) { @@ -268,7 +274,13 @@ class DisplaySearch extends Display { (granted) => { if (granted) { this.clipboardMonitor.start(); - apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext()); + apiModifySettings([{ + action: 'set', + path: 'general.enableClipboardMonitor', + value: true, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } else { e.target.checked = false; } @@ -276,7 +288,13 @@ class DisplaySearch extends Display { ); } else { this.clipboardMonitor.stop(); - apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext()); + apiModifySettings([{ + action: 'set', + path: 'general.enableClipboardMonitor', + value: false, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } } diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index af97ac3d..0bc91759 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -28,10 +28,6 @@ function apiOptionsGetFull() { return _apiInvoke('optionsGetFull'); } -function apiOptionsSet(changedOptions, optionsContext, source) { - return _apiInvoke('optionsSet', {changedOptions, optionsContext, source}); -} - function apiOptionsSave(source) { return _apiInvoke('optionsSave', {source}); } @@ -160,6 +156,10 @@ function apiDeleteDictionary(dictionaryName, onProgress) { return _apiInvokeWithProgress('deleteDictionary', {dictionaryName}, onProgress); } +function apiModifySettings(targets, source) { + return _apiInvoke('modifySettings', {targets, source}); +} + function _apiCreateActionPort(timeout=5000) { return new Promise((resolve, reject) => { let timer = null; -- cgit v1.2.3 From f361139d744e58a6c33841cee227d13d1970bb98 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 6 May 2020 19:37:36 -0400 Subject: Japanese util refactor (#510) * Convert mixed japanese.js to utility class * Copy functions from bg/js/japanese.js into mixed/js/japanese.js * Remove bg/js/japanese.js * Make wanakana dependency optional * Update tests --- ext/bg/background.html | 1 - ext/bg/js/japanese.js | 426 --------------------------------------- ext/bg/search.html | 1 - ext/bg/settings.html | 1 - ext/mixed/js/japanese.js | 507 ++++++++++++++++++++++++++++++++++++++++++----- test/test-japanese.js | 3 +- 6 files changed, 453 insertions(+), 486 deletions(-) delete mode 100644 ext/bg/js/japanese.js (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index 9c740adf..7cb76ec3 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -36,7 +36,6 @@ - diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js deleted file mode 100644 index ac81acb5..00000000 --- a/ext/bg/js/japanese.js +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright (C) 2016-2020 Yomichan Authors - * - * 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 - * jp - * wanakana - */ - -(() => { - const HALFWIDTH_KATAKANA_MAPPING = new Map([ - ['ヲ', 'ヲヺ-'], - ['ァ', 'ァ--'], - ['ィ', 'ィ--'], - ['ゥ', 'ゥ--'], - ['ェ', 'ェ--'], - ['ォ', 'ォ--'], - ['ャ', 'ャ--'], - ['ュ', 'ュ--'], - ['ョ', 'ョ--'], - ['ッ', 'ッ--'], - ['ー', 'ー--'], - ['ア', 'ア--'], - ['イ', 'イ--'], - ['ウ', 'ウヴ-'], - ['エ', 'エ--'], - ['オ', 'オ--'], - ['カ', 'カガ-'], - ['キ', 'キギ-'], - ['ク', 'クグ-'], - ['ケ', 'ケゲ-'], - ['コ', 'コゴ-'], - ['サ', 'サザ-'], - ['シ', 'シジ-'], - ['ス', 'スズ-'], - ['セ', 'セゼ-'], - ['ソ', 'ソゾ-'], - ['タ', 'タダ-'], - ['チ', 'チヂ-'], - ['ツ', 'ツヅ-'], - ['テ', 'テデ-'], - ['ト', 'トド-'], - ['ナ', 'ナ--'], - ['ニ', 'ニ--'], - ['ヌ', 'ヌ--'], - ['ネ', 'ネ--'], - ['ノ', 'ノ--'], - ['ハ', 'ハバパ'], - ['ヒ', 'ヒビピ'], - ['フ', 'フブプ'], - ['ヘ', 'ヘベペ'], - ['ホ', 'ホボポ'], - ['マ', 'マ--'], - ['ミ', 'ミ--'], - ['ム', 'ム--'], - ['メ', 'メ--'], - ['モ', 'モ--'], - ['ヤ', 'ヤ--'], - ['ユ', 'ユ--'], - ['ヨ', 'ヨ--'], - ['ラ', 'ラ--'], - ['リ', 'リ--'], - ['ル', 'ル--'], - ['レ', 'レ--'], - ['ロ', 'ロ--'], - ['ワ', 'ワ--'], - ['ン', 'ン--'] - ]); - - const ITERATION_MARK_CODE_POINT = 0x3005; - - const HIRAGANA_SMALL_TSU_CODE_POINT = 0x3063; - const KATAKANA_SMALL_TSU_CODE_POINT = 0x30c3; - const KANA_PROLONGED_SOUND_MARK_CODE_POINT = 0x30fc; - - // Existing functions - - const isCodePointKanji = jp.isCodePointKanji; - const isStringEntirelyKana = jp.isStringEntirelyKana; - - - // Conversion functions - - function convertKatakanaToHiragana(text) { - let result = ''; - for (const c of text) { - if (wanakana.isKatakana(c)) { - result += wanakana.toHiragana(c); - } else { - result += c; - } - } - - return result; - } - - function convertHiraganaToKatakana(text) { - let result = ''; - for (const c of text) { - if (wanakana.isHiragana(c)) { - result += wanakana.toKatakana(c); - } else { - result += c; - } - } - - return result; - } - - function convertToRomaji(text) { - return wanakana.toRomaji(text); - } - - function convertReading(expression, reading, readingMode) { - switch (readingMode) { - case 'hiragana': - return convertKatakanaToHiragana(reading); - case 'katakana': - return convertHiraganaToKatakana(reading); - case 'romaji': - if (reading) { - return convertToRomaji(reading); - } else { - if (isStringEntirelyKana(expression)) { - return convertToRomaji(expression); - } - } - return reading; - case 'none': - return ''; - default: - return reading; - } - } - - 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; - } - } - return result; - } - - function convertHalfWidthKanaToFullWidth(text, sourceMap=null) { - let result = ''; - - // 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; - } - } - - if (sourceMap !== null && index > 0) { - sourceMap.combine(result.length, 1); - } - result += c2; - } - - return result; - } - - function convertAlphabeticToKana(text, sourceMap=null) { - let part = ''; - let result = ''; - - 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 += convertAlphabeticPartToKana(part, sourceMap, result.length); - part = ''; - } - result += char; - continue; - } - part += String.fromCodePoint(c); - } - - if (part.length > 0) { - result += convertAlphabeticPartToKana(part, sourceMap, result.length); - } - return result; - } - - function convertAlphabeticPartToKana(text, sourceMap, sourceMapStart) { - const result = wanakana.toHiragana(text); - - // Generate source mapping - if (sourceMap !== null) { - 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; - } - - // Merge characters - const removals = iNext - i - 1; - if (removals > 0) { - sourceMap.combine(sourceMapStart, removals); - } - ++sourceMapStart; - - // Empty elements - const additions = resultPosNext - resultPos - 1; - for (let j = 0; j < additions; ++j) { - sourceMap.insert(sourceMapStart, 0); - ++sourceMapStart; - } - - i = iNext; - resultPos = resultPosNext; - } - } - - return result; - } - - - // Furigana distribution - - 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 []; - } - - 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, furigana: ''}].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); - } - - if (stemLength !== source.length) { - output.push({text: source.substring(stemLength), furigana: ''}); - } - - return output; - } - - - // Miscellaneous - - function collapseEmphaticSequences(text, fullCollapse, sourceMap=null) { - let result = ''; - let collapseCodePoint = -1; - const hasSourceMap = (sourceMap !== null); - for (const char of text) { - const c = char.codePointAt(0); - if ( - c === HIRAGANA_SMALL_TSU_CODE_POINT || - c === KATAKANA_SMALL_TSU_CODE_POINT || - c === KANA_PROLONGED_SOUND_MARK_CODE_POINT - ) { - if (collapseCodePoint !== c) { - collapseCodePoint = c; - if (!fullCollapse) { - result += char; - continue; - } - } - } else { - collapseCodePoint = -1; - result += char; - continue; - } - - if (hasSourceMap) { - sourceMap.combine(Math.max(0, result.length - 1), 1); - } - } - return result; - } - - - // Exports - - Object.assign(jp, { - convertKatakanaToHiragana, - convertHiraganaToKatakana, - convertToRomaji, - convertReading, - convertNumericToFullWidth, - convertHalfWidthKanaToFullWidth, - convertAlphabeticToKana, - distributeFurigana, - distributeFuriganaInflected, - collapseEmphaticSequences - }); -})(); diff --git a/ext/bg/search.html b/ext/bg/search.html index 52915b76..a30b1d60 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -79,7 +79,6 @@ - diff --git a/ext/bg/settings.html b/ext/bg/settings.html index b8477e46..a0981687 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1139,7 +1139,6 @@ - diff --git a/ext/mixed/js/japanese.js b/ext/mixed/js/japanese.js index ced486dd..801dec84 100644 --- a/ext/mixed/js/japanese.js +++ b/ext/mixed/js/japanese.js @@ -16,6 +16,11 @@ */ const jp = (() => { + const ITERATION_MARK_CODE_POINT = 0x3005; + const HIRAGANA_SMALL_TSU_CODE_POINT = 0x3063; + const KATAKANA_SMALL_TSU_CODE_POINT = 0x30c3; + const KANA_PROLONGED_SOUND_MARK_CODE_POINT = 0x30fc; + const HIRAGANA_RANGE = [0x3040, 0x309f]; const KATAKANA_RANGE = [0x30a0, 0x30ff]; const KANA_RANGES = [HIRAGANA_RANGE, KATAKANA_RANGE]; @@ -65,20 +70,65 @@ const jp = (() => { const SMALL_KANA_SET = new Set(Array.from('ぁぃぅぇぉゃゅょゎァィゥェォャュョヮ')); + const HALFWIDTH_KATAKANA_MAPPING = new Map([ + ['ヲ', 'ヲヺ-'], + ['ァ', 'ァ--'], + ['ィ', 'ィ--'], + ['ゥ', 'ゥ--'], + ['ェ', 'ェ--'], + ['ォ', 'ォ--'], + ['ャ', 'ャ--'], + ['ュ', 'ュ--'], + ['ョ', 'ョ--'], + ['ッ', 'ッ--'], + ['ー', 'ー--'], + ['ア', 'ア--'], + ['イ', 'イ--'], + ['ウ', 'ウヴ-'], + ['エ', 'エ--'], + ['オ', 'オ--'], + ['カ', 'カガ-'], + ['キ', 'キギ-'], + ['ク', 'クグ-'], + ['ケ', 'ケゲ-'], + ['コ', 'コゴ-'], + ['サ', 'サザ-'], + ['シ', 'シジ-'], + ['ス', 'スズ-'], + ['セ', 'セゼ-'], + ['ソ', 'ソゾ-'], + ['タ', 'タダ-'], + ['チ', 'チヂ-'], + ['ツ', 'ツヅ-'], + ['テ', 'テデ-'], + ['ト', 'トド-'], + ['ナ', 'ナ--'], + ['ニ', 'ニ--'], + ['ヌ', 'ヌ--'], + ['ネ', 'ネ--'], + ['ノ', 'ノ--'], + ['ハ', 'ハバパ'], + ['ヒ', 'ヒビピ'], + ['フ', 'フブプ'], + ['ヘ', 'ヘベペ'], + ['ホ', 'ホボポ'], + ['マ', 'マ--'], + ['ミ', 'ミ--'], + ['ム', 'ム--'], + ['メ', 'メ--'], + ['モ', 'モ--'], + ['ヤ', 'ヤ--'], + ['ユ', 'ユ--'], + ['ヨ', 'ヨ--'], + ['ラ', 'ラ--'], + ['リ', 'リ--'], + ['ル', 'ル--'], + ['レ', 'レ--'], + ['ロ', 'ロ--'], + ['ワ', 'ワ--'], + ['ン', 'ン--'] + ]); - // 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) { @@ -89,63 +139,410 @@ const jp = (() => { return false; } + function getWanakana() { + try { + if (typeof wanakana !== 'undefined') { + // eslint-disable-next-line no-undef + return wanakana; + } + } catch (e) { + // NOP + } + return null; + } + - // String testing functions + class JapaneseUtil { + constructor(wanakana=null) { + this._wanakana = wanakana; + } - function isStringEntirelyKana(str) { - if (str.length === 0) { return false; } - for (const c of str) { - if (!isCodePointKana(c.codePointAt(0))) { - return false; + // Character code testing functions + + isCodePointKanji(codePoint) { + return isCodePointInRanges(codePoint, CJK_UNIFIED_IDEOGRAPHS_RANGES); + } + + isCodePointKana(codePoint) { + return isCodePointInRanges(codePoint, KANA_RANGES); + } + + isCodePointJapanese(codePoint) { + return isCodePointInRanges(codePoint, JAPANESE_RANGES); + } + + // String testing functions + + isStringEntirelyKana(str) { + if (str.length === 0) { return false; } + for (const c of str) { + if (!isCodePointInRanges(c.codePointAt(0), KANA_RANGES)) { + return false; + } } + return true; } - return true; - } - function isStringPartiallyJapanese(str) { - if (str.length === 0) { return false; } - for (const c of str) { - if (isCodePointJapanese(c.codePointAt(0))) { - return true; + isStringPartiallyJapanese(str) { + if (str.length === 0) { return false; } + for (const c of str) { + if (isCodePointInRanges(c.codePointAt(0), JAPANESE_RANGES)) { + return true; + } } + return false; } - return false; - } + // Mora functions - // Mora functions + isMoraPitchHigh(moraIndex, pitchAccentPosition) { + switch (pitchAccentPosition) { + case 0: return (moraIndex > 0); + case 1: return (moraIndex < 1); + default: return (moraIndex > 0 && moraIndex < pitchAccentPosition); + } + } - function isMoraPitchHigh(moraIndex, pitchAccentPosition) { - switch (pitchAccentPosition) { - case 0: return (moraIndex > 0); - case 1: return (moraIndex < 1); - default: return (moraIndex > 0 && moraIndex < pitchAccentPosition); + getKanaMorae(text) { + const morae = []; + let i; + for (const c of text) { + if (SMALL_KANA_SET.has(c) && (i = morae.length) > 0) { + morae[i - 1] += c; + } else { + morae.push(c); + } + } + return morae; } - } - function getKanaMorae(text) { - const morae = []; - let i; - for (const c of text) { - if (SMALL_KANA_SET.has(c) && (i = morae.length) > 0) { - morae[i - 1] += c; - } else { - morae.push(c); + // Conversion functions + + convertKatakanaToHiragana(text) { + const wanakana = this._getWanakana(); + let result = ''; + for (const c of text) { + if (wanakana.isKatakana(c)) { + result += wanakana.toHiragana(c); + } else { + result += c; + } } + + return result; } - return morae; - } + convertHiraganaToKatakana(text) { + const wanakana = this._getWanakana(); + let result = ''; + for (const c of text) { + if (wanakana.isHiragana(c)) { + result += wanakana.toKatakana(c); + } else { + result += c; + } + } + + return result; + } + + convertToRomaji(text) { + const wanakana = this._getWanakana(); + return wanakana.toRomaji(text); + } + + convertReading(expression, reading, readingMode) { + switch (readingMode) { + case 'hiragana': + return this.convertKatakanaToHiragana(reading); + case 'katakana': + return this.convertHiraganaToKatakana(reading); + case 'romaji': + if (reading) { + return this.convertToRomaji(reading); + } else { + if (this.isStringEntirelyKana(expression)) { + return this.convertToRomaji(expression); + } + } + return reading; + case 'none': + return ''; + default: + return reading; + } + } + + 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; + } + } + return result; + } + + convertHalfWidthKanaToFullWidth(text, sourceMap=null) { + let result = ''; + + // 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; + } + } + + if (sourceMap !== null && index > 0) { + sourceMap.combine(result.length, 1); + } + result += c2; + } + + return result; + } + + convertAlphabeticToKana(text, sourceMap=null) { + let part = ''; + let result = ''; + + 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 += this._convertAlphabeticPartToKana(part, sourceMap, result.length); + part = ''; + } + result += char; + continue; + } + part += String.fromCodePoint(c); + } + + if (part.length > 0) { + result += this._convertAlphabeticPartToKana(part, sourceMap, result.length); + } + return result; + } + + // Furigana distribution + + 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 []; + } + + const group = groups[0]; + if (group.mode === 'kana') { + if (this.convertKatakanaToHiragana(reading2).startsWith(this.convertKatakanaToHiragana(group.text))) { + const readingLeft = reading2.substring(group.text.length); + const segs = segmentize(readingLeft, groups.splice(1)); + if (segs) { + return [{text: group.text, furigana: ''}].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 = this.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; + } + + distributeFuriganaInflected(expression, reading, source) { + const output = []; + + let stemLength = 0; + const shortest = Math.min(source.length, expression.length); + const sourceHiragana = this.convertKatakanaToHiragana(source); + const expressionHiragana = this.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 this.distributeFurigana(stemExpression, stemReading)) { + output.push(segment); + } + + if (stemLength !== source.length) { + output.push({text: source.substring(stemLength), furigana: ''}); + } + + return output; + } + + // Miscellaneous + + collapseEmphaticSequences(text, fullCollapse, sourceMap=null) { + let result = ''; + let collapseCodePoint = -1; + const hasSourceMap = (sourceMap !== null); + for (const char of text) { + const c = char.codePointAt(0); + if ( + c === HIRAGANA_SMALL_TSU_CODE_POINT || + c === KATAKANA_SMALL_TSU_CODE_POINT || + c === KANA_PROLONGED_SOUND_MARK_CODE_POINT + ) { + if (collapseCodePoint !== c) { + collapseCodePoint = c; + if (!fullCollapse) { + result += char; + continue; + } + } + } else { + collapseCodePoint = -1; + result += char; + continue; + } + + if (hasSourceMap) { + sourceMap.combine(Math.max(0, result.length - 1), 1); + } + } + return result; + } + + // Private + + _getWanakana() { + const wanakana = this._wanakana; + if (wanakana === null) { throw new Error('Functions which use WanaKana are not supported in this context'); } + return wanakana; + } + + _convertAlphabeticPartToKana(text, sourceMap, sourceMapStart) { + const wanakana = this._getWanakana(); + const result = wanakana.toHiragana(text); + + // Generate source mapping + if (sourceMap !== null) { + 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; + } + + // Merge characters + const removals = iNext - i - 1; + if (removals > 0) { + sourceMap.combine(sourceMapStart, removals); + } + ++sourceMapStart; + + // Empty elements + const additions = resultPosNext - resultPos - 1; + for (let j = 0; j < additions; ++j) { + sourceMap.insert(sourceMapStart, 0); + ++sourceMapStart; + } + + i = iNext; + resultPos = resultPosNext; + } + } + + return result; + } + } - // Exports - return { - isCodePointKanji, - isCodePointKana, - isCodePointJapanese, - isStringEntirelyKana, - isStringPartiallyJapanese, - isMoraPitchHigh, - getKanaMorae - }; + return new JapaneseUtil(getWanakana()); })(); diff --git a/test/test-japanese.js b/test/test-japanese.js index 321861d5..39004128 100644 --- a/test/test-japanese.js +++ b/test/test-japanese.js @@ -22,8 +22,7 @@ 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' + 'bg/js/text-source-map.js' ]); const jp = vm.get('jp'); const TextSourceMap = vm.get('TextSourceMap'); -- cgit v1.2.3 From d6a3825a383e13b34c03c0b36e393da52bf8cf89 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 9 May 2020 18:36:00 +0300 Subject: Modifier key platform names (#519) * wip * add environment class * use Environment class * use Environment for scanning modifier options * remove Environment in favor of API * await promise * use modifier symbols on macOS * fix key separator issues * if else to switch * simplify variable names --- ext/bg/background.html | 1 + ext/bg/js/backend.js | 40 ++---- ext/bg/js/profile-conditions.js | 256 +++++++++++++++++++----------------- ext/bg/js/settings/conditions-ui.js | 6 +- ext/bg/js/settings/main.js | 19 +++ ext/bg/js/settings/profiles.js | 4 +- ext/bg/settings.html | 9 +- ext/mixed/js/environment.js | 114 ++++++++++++++++ 8 files changed, 286 insertions(+), 163 deletions(-) create mode 100644 ext/mixed/js/environment.js (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index 7cb76ec3..ca35a3c6 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -21,6 +21,7 @@ + diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 9936baf8..557ceb29 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -24,6 +24,7 @@ * ClipboardMonitor * Database * DictionaryImporter + * Environment * JsonSchema * Mecab * ObjectPropertyAccessor @@ -35,6 +36,7 @@ * optionsLoad * optionsSave * profileConditionsDescriptor + * profileConditionsDescriptorPromise * requestJson * requestText * utilIsolate @@ -42,6 +44,7 @@ class Backend { constructor() { + this.environment = new Environment(); this.database = new Database(); this.dictionaryImporter = new DictionaryImporter(); this.translator = new Translator(this.database); @@ -100,7 +103,7 @@ class Backend { ['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}], ['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}], ['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}], - ['getEnvironmentInfo', {async: true, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}], + ['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}], ['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}], ['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}], ['getQueryParserTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetQueryParserTemplatesHtml.bind(this)}], @@ -140,9 +143,12 @@ class Backend { }, 1000); this._updateBadge(); + await this.environment.prepare(); await this.database.prepare(); await this.translator.prepare(); + await profileConditionsDescriptorPromise; + this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET'); this.defaultAnkiFieldTemplates = await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET'); this.options = await optionsLoad(); @@ -635,15 +641,8 @@ class Backend { }); } - async _onApiGetEnvironmentInfo() { - const browser = await Backend._getBrowser(); - const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve)); - return { - browser, - platform: { - os: platform.os - } - }; + _onApiGetEnvironmentInfo() { + return this.environment.getInfo(); } async _onApiClipboardGet() { @@ -659,7 +658,7 @@ class Backend { being an extension with clipboard permissions. It effectively asks for the non-extension permission for clipboard access. */ - const browser = await Backend._getBrowser(); + const {browser} = this.environment.getInfo(); if (browser === 'firefox' || browser === 'firefox-mobile') { return await navigator.clipboard.readText(); } else { @@ -1211,23 +1210,4 @@ class Backend { // Edge throws exception for no reason here. } } - - static async _getBrowser() { - if (EXTENSION_IS_BROWSER_EDGE) { - return 'edge'; - } - if (typeof browser !== 'undefined') { - try { - const info = await browser.runtime.getBrowserInfo(); - if (info.name === 'Fennec') { - return 'firefox-mobile'; - } - } catch (e) { - // NOP - } - return 'firefox'; - } else { - return 'chrome'; - } - } } diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js index 32309c64..97e09f1c 100644 --- a/ext/bg/js/profile-conditions.js +++ b/ext/bg/js/profile-conditions.js @@ -15,6 +15,9 @@ * along with this program. If not, see . */ +/* global + * Environment + */ function _profileConditionTestDomain(urlDomain, domain) { return ( @@ -36,135 +39,140 @@ function _profileConditionTestDomainList(url, domainList) { return false; } -const _profileModifierKeys = [ - {optionValue: 'alt', name: 'Alt'}, - {optionValue: 'ctrl', name: 'Ctrl'}, - {optionValue: 'shift', name: 'Shift'} -]; +let profileConditionsDescriptor = null; -if (!hasOwn(window, 'netscape')) { - _profileModifierKeys.push({optionValue: 'meta', name: 'Meta'}); -} +const profileConditionsDescriptorPromise = (async () => { + const environment = new Environment(); + await environment.prepare(); -const _profileModifierValueToName = new Map( - _profileModifierKeys.map(({optionValue, name}) => [optionValue, name]) -); + const modifiers = environment.getInfo().modifiers; + const modifierSeparator = modifiers.separator; + const modifierKeyValues = modifiers.keys.map( + ({value, name}) => ({optionValue: value, name}) + ); -const _profileModifierNameToValue = new Map( - _profileModifierKeys.map(({optionValue, name}) => [name, optionValue]) -); + const modifierValueToName = new Map( + modifierKeyValues.map(({optionValue, name}) => [optionValue, name]) + ); -const profileConditionsDescriptor = { - popupLevel: { - name: 'Popup Level', - description: 'Use profile depending on the level of the popup.', - placeholder: 'Number', - type: 'number', - step: 1, - defaultValue: 0, - defaultOperator: 'equal', - transform: (optionValue) => parseInt(optionValue, 10), - transformReverse: (transformedOptionValue) => `${transformedOptionValue}`, - validateTransformed: (transformedOptionValue) => Number.isFinite(transformedOptionValue), - operators: { - equal: { - name: '=', - test: ({depth}, optionValue) => (depth === optionValue) - }, - notEqual: { - name: '\u2260', - test: ({depth}, optionValue) => (depth !== optionValue) - }, - lessThan: { - name: '<', - test: ({depth}, optionValue) => (depth < optionValue) - }, - greaterThan: { - name: '>', - test: ({depth}, optionValue) => (depth > optionValue) - }, - lessThanOrEqual: { - name: '\u2264', - test: ({depth}, optionValue) => (depth <= optionValue) - }, - greaterThanOrEqual: { - name: '\u2265', - test: ({depth}, optionValue) => (depth >= optionValue) + const modifierNameToValue = new Map( + modifierKeyValues.map(({optionValue, name}) => [name, optionValue]) + ); + + profileConditionsDescriptor = { + popupLevel: { + name: 'Popup Level', + description: 'Use profile depending on the level of the popup.', + placeholder: 'Number', + type: 'number', + step: 1, + defaultValue: 0, + defaultOperator: 'equal', + transform: (optionValue) => parseInt(optionValue, 10), + transformReverse: (transformedOptionValue) => `${transformedOptionValue}`, + validateTransformed: (transformedOptionValue) => Number.isFinite(transformedOptionValue), + operators: { + equal: { + name: '=', + test: ({depth}, optionValue) => (depth === optionValue) + }, + notEqual: { + name: '\u2260', + test: ({depth}, optionValue) => (depth !== optionValue) + }, + lessThan: { + name: '<', + test: ({depth}, optionValue) => (depth < optionValue) + }, + greaterThan: { + name: '>', + test: ({depth}, optionValue) => (depth > optionValue) + }, + lessThanOrEqual: { + name: '\u2264', + test: ({depth}, optionValue) => (depth <= optionValue) + }, + greaterThanOrEqual: { + name: '\u2265', + test: ({depth}, optionValue) => (depth >= optionValue) + } } - } - }, - url: { - name: 'URL', - description: 'Use profile depending on the URL of the current website.', - defaultOperator: 'matchDomain', - operators: { - matchDomain: { - name: 'Matches Domain', - placeholder: 'Comma separated list of domains', - defaultValue: 'example.com', - transformCache: {}, - transform: (optionValue) => optionValue.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0), - transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '), - validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0), - test: ({url}, transformedOptionValue) => _profileConditionTestDomainList(url, transformedOptionValue) - }, - matchRegExp: { - name: 'Matches RegExp', - placeholder: 'Regular expression', - defaultValue: 'example\\.com', - transformCache: {}, - transform: (optionValue) => new RegExp(optionValue, 'i'), - transformReverse: (transformedOptionValue) => transformedOptionValue.source, - test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url)) + }, + url: { + name: 'URL', + description: 'Use profile depending on the URL of the current website.', + defaultOperator: 'matchDomain', + operators: { + matchDomain: { + name: 'Matches Domain', + placeholder: 'Comma separated list of domains', + defaultValue: 'example.com', + transformCache: {}, + transform: (optionValue) => optionValue.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0), + transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '), + validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0), + test: ({url}, transformedOptionValue) => _profileConditionTestDomainList(url, transformedOptionValue) + }, + matchRegExp: { + name: 'Matches RegExp', + placeholder: 'Regular expression', + defaultValue: 'example\\.com', + transformCache: {}, + transform: (optionValue) => new RegExp(optionValue, 'i'), + transformReverse: (transformedOptionValue) => transformedOptionValue.source, + test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url)) + } } - } - }, - modifierKeys: { - name: 'Modifier Keys', - description: 'Use profile depending on the active modifier keys.', - values: _profileModifierKeys, - defaultOperator: 'are', - operators: { - are: { - name: 'are', - placeholder: 'Press one or more modifier keys here', - defaultValue: [], - type: 'keyMulti', - transformInput: (optionValue) => optionValue - .split(' + ') - .filter((v) => v.length > 0) - .map((v) => _profileModifierNameToValue.get(v)), - transformReverse: (transformedOptionValue) => transformedOptionValue - .map((v) => _profileModifierValueToName.get(v)) - .join(' + '), - test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue)) - }, - areNot: { - name: 'are not', - placeholder: 'Press one or more modifier keys here', - defaultValue: [], - type: 'keyMulti', - transformInput: (optionValue) => optionValue - .split(' + ') - .filter((v) => v.length > 0) - .map((v) => _profileModifierNameToValue.get(v)), - transformReverse: (transformedOptionValue) => transformedOptionValue - .map((v) => _profileModifierValueToName.get(v)) - .join(' + '), - test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue)) - }, - include: { - name: 'include', - type: 'select', - defaultValue: 'alt', - test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue) - }, - notInclude: { - name: 'don\'t include', - type: 'select', - defaultValue: 'alt', - test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue) + }, + modifierKeys: { + name: 'Modifier Keys', + description: 'Use profile depending on the active modifier keys.', + values: modifierKeyValues, + defaultOperator: 'are', + operators: { + are: { + name: 'are', + placeholder: 'Press one or more modifier keys here', + defaultValue: [], + type: 'keyMulti', + keySeparator: modifierSeparator, + transformInput: (optionValue) => optionValue + .split(modifierSeparator) + .filter((v) => v.length > 0) + .map((v) => modifierNameToValue.get(v)), + transformReverse: (transformedOptionValue) => transformedOptionValue + .map((v) => modifierValueToName.get(v)) + .join(modifierSeparator), + test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue)) + }, + areNot: { + name: 'are not', + placeholder: 'Press one or more modifier keys here', + defaultValue: [], + type: 'keyMulti', + keySeparator: modifierSeparator, + transformInput: (optionValue) => optionValue + .split(modifierSeparator) + .filter((v) => v.length > 0) + .map((v) => modifierNameToValue.get(v)), + transformReverse: (transformedOptionValue) => transformedOptionValue + .map((v) => modifierValueToName.get(v)) + .join(modifierSeparator), + test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue)) + }, + include: { + name: 'include', + type: 'select', + defaultValue: 'alt', + test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue) + }, + notInclude: { + name: 'don\'t include', + type: 'select', + defaultValue: 'alt', + test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue) + } } } - } -}; + }; +})(); diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js index 0670de5a..031689a7 100644 --- a/ext/bg/js/settings/conditions-ui.js +++ b/ext/bg/js/settings/conditions-ui.js @@ -310,10 +310,14 @@ ConditionsUI.Condition = class Condition { inputInner.prop('readonly', true); let values = []; + let keySeparator = ' + '; for (const object of objects) { if (hasOwn(object, 'values')) { values = object.values; } + if (hasOwn(object, 'keySeparator')) { + keySeparator = object.keySeparator; + } } const pressedKeyIndices = new Set(); @@ -347,7 +351,7 @@ ConditionsUI.Condition = class Condition { } } - const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(' + '); + const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(keySeparator); inputInner.val(inputValue); inputInner.change(); }; diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index cf75d629..61395b1c 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -22,6 +22,7 @@ * ankiTemplatesInitialize * ankiTemplatesUpdateValue * apiForwardLogsToBackend + * apiGetEnvironmentInfo * apiOptionsSave * appearanceInitialize * audioSettingsInitialize @@ -285,6 +286,23 @@ function showExtensionInformation() { node.textContent = `${manifest.name} v${manifest.version}`; } +async function settingsPopulateModifierKeys() { + const scanModifierKeySelect = document.querySelector('#scan-modifier-key'); + scanModifierKeySelect.textContent = ''; + + const environment = await apiGetEnvironmentInfo(); + const modifierKeys = [ + {value: 'none', name: 'None'}, + ...environment.modifiers.keys + ]; + for (const {value, name} of modifierKeys) { + const option = document.createElement('option'); + option.value = value; + option.textContent = name; + scanModifierKeySelect.appendChild(option); + } +} + async function onReady() { apiForwardLogsToBackend(); @@ -292,6 +310,7 @@ async function onReady() { showExtensionInformation(); + await settingsPopulateModifierKeys(); formSetupEventListeners(); appearanceInitialize(); await audioSettingsInitialize(); diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index 3f4b1da7..bdf5a13d 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -23,6 +23,7 @@ * getOptionsFullMutable * getOptionsMutable * profileConditionsDescriptor + * profileConditionsDescriptorPromise * settingsSaveOptions * utilBackgroundIsolate */ @@ -98,6 +99,7 @@ async function profileFormWrite(optionsFull) { profileConditionsContainer.cleanup(); } + await profileConditionsDescriptorPromise; profileConditionsContainer = new ConditionsUI.Container( profileConditionsDescriptor, 'popupLevel', @@ -128,7 +130,7 @@ function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndi } async function profileOptionsUpdateTarget(optionsFull) { - profileFormWrite(optionsFull); + await profileFormWrite(optionsFull); const optionsContext = getOptionsContext(); const options = await getOptionsMutable(optionsContext); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index a0981687..3ce91f12 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -412,13 +412,7 @@
    - +
    @@ -1131,6 +1125,7 @@ + diff --git a/ext/mixed/js/environment.js b/ext/mixed/js/environment.js new file mode 100644 index 00000000..e5bc20a7 --- /dev/null +++ b/ext/mixed/js/environment.js @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * 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 Environment { + constructor() { + this._cachedEnvironmentInfo = null; + } + + async prepare() { + this._cachedEnvironmentInfo = await this._loadEnvironmentInfo(); + } + + getInfo() { + if (this._cachedEnvironmentInfo === null) { throw new Error('Not prepared'); } + return this._cachedEnvironmentInfo; + } + + async _loadEnvironmentInfo() { + const browser = await this._getBrowser(); + const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve)); + const modifierInfo = this._getModifierInfo(browser, platform.os); + return { + browser, + platform: { + os: platform.os + }, + modifiers: modifierInfo + }; + } + + async _getBrowser() { + if (EXTENSION_IS_BROWSER_EDGE) { + return 'edge'; + } + if (typeof browser !== 'undefined') { + try { + const info = await browser.runtime.getBrowserInfo(); + if (info.name === 'Fennec') { + return 'firefox-mobile'; + } + } catch (e) { + // NOP + } + return 'firefox'; + } else { + return 'chrome'; + } + } + + _getModifierInfo(browser, os) { + let osKeys; + let separator; + switch (os) { + case 'win': + separator = ' + '; + osKeys = [ + ['alt', 'Alt'], + ['ctrl', 'Ctrl'], + ['shift', 'Shift'], + ['meta', 'Windows'] + ]; + break; + case 'mac': + separator = ''; + osKeys = [ + ['alt', '⌥'], + ['ctrl', '⌃'], + ['shift', '⇧'], + ['meta', '⌘'] + ]; + break; + case 'linux': + case 'openbsd': + case 'cros': + case 'android': + separator = ' + '; + osKeys = [ + ['alt', 'Alt'], + ['ctrl', 'Ctrl'], + ['shift', 'Shift'], + ['meta', 'Super'] + ]; + break; + default: + throw new Error(`Invalid OS: ${os}`); + } + + const isFirefox = (browser === 'firefox' || browser === 'firefox-mobile'); + const keys = []; + + for (const [value, name] of osKeys) { + // Firefox doesn't support event.metaKey on platforms other than macOS + if (value === 'meta' && isFirefox && os !== 'mac') { continue; } + keys.push({value, name}); + } + + return {keys, separator}; + } +} -- cgit v1.2.3