aboutsummaryrefslogtreecommitdiff
path: root/ext/bg
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2020-02-24 21:31:14 -0800
committerAlex Yatskov <alex@foosoft.net>2020-02-24 21:31:14 -0800
commitd32f4def0eeed1599857bc04c973337a2a13dd8b (patch)
tree61149656f361dd2d9998d67d68249dc184b73fbb /ext/bg
parent0c5b9b1fa1599cbf769d96cdebc226310f9dd8bc (diff)
parent706c3edcffb0078d71fd5b58775f16cf5fc1205b (diff)
Merge branch 'master' into testing
Diffstat (limited to 'ext/bg')
-rw-r--r--ext/bg/background.html4
-rw-r--r--ext/bg/css/settings.css14
-rw-r--r--ext/bg/data/dictionary-index-schema.json69
-rw-r--r--ext/bg/data/dictionary-kanji-bank-v1-schema.json33
-rw-r--r--ext/bg/data/dictionary-kanji-bank-v3-schema.json44
-rw-r--r--ext/bg/data/dictionary-kanji-meta-bank-v3-schema.json25
-rw-r--r--ext/bg/data/dictionary-tag-bank-v3-schema.json32
-rw-r--r--ext/bg/data/dictionary-term-bank-v1-schema.json36
-rw-r--r--ext/bg/data/dictionary-term-bank-v3-schema.json48
-rw-r--r--ext/bg/data/dictionary-term-meta-bank-v3-schema.json25
-rw-r--r--ext/bg/data/options-schema.json12
-rw-r--r--ext/bg/js/anki.js1
-rw-r--r--ext/bg/js/api.js8
-rw-r--r--ext/bg/js/audio.js11
-rw-r--r--ext/bg/js/backend.js237
-rw-r--r--ext/bg/js/clipboard-monitor.js81
-rw-r--r--ext/bg/js/context.js5
-rw-r--r--ext/bg/js/database.js604
-rw-r--r--ext/bg/js/deinflector.js20
-rw-r--r--ext/bg/js/dictionary.js245
-rw-r--r--ext/bg/js/handlebars.js6
-rw-r--r--ext/bg/js/japanese.js455
-rw-r--r--ext/bg/js/json-schema.js335
-rw-r--r--ext/bg/js/options.js5
-rw-r--r--ext/bg/js/page-exit-prevention.js74
-rw-r--r--ext/bg/js/search-frontend.js1
-rw-r--r--ext/bg/js/search-query-parser-generator.js78
-rw-r--r--ext/bg/js/search-query-parser.js69
-rw-r--r--ext/bg/js/search.js182
-rw-r--r--ext/bg/js/settings/anki-templates.js3
-rw-r--r--ext/bg/js/settings/anki.js28
-rw-r--r--ext/bg/js/settings/audio-ui.js1
-rw-r--r--ext/bg/js/settings/audio.js2
-rw-r--r--ext/bg/js/settings/backup.js5
-rw-r--r--ext/bg/js/settings/conditions-ui.js1
-rw-r--r--ext/bg/js/settings/dictionaries.js125
-rw-r--r--ext/bg/js/settings/main.js35
-rw-r--r--ext/bg/js/settings/popup-preview-frame.js34
-rw-r--r--ext/bg/js/settings/popup-preview.js8
-rw-r--r--ext/bg/js/settings/profiles.js4
-rw-r--r--ext/bg/js/settings/storage.js1
-rw-r--r--ext/bg/js/templates.js55
-rw-r--r--ext/bg/js/translator.js148
-rw-r--r--ext/bg/js/util.js28
-rw-r--r--ext/bg/query-parser-templates.html11
-rw-r--r--ext/bg/search.html44
-rw-r--r--ext/bg/settings.html22
47 files changed, 2292 insertions, 1022 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html
index af87eddb..7fd1c477 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -26,20 +26,20 @@
<script src="/bg/js/mecab.js"></script>
<script src="/bg/js/audio.js"></script>
<script src="/bg/js/backend-api-forwarder.js"></script>
+ <script src="/bg/js/clipboard-monitor.js"></script>
<script src="/bg/js/conditions.js"></script>
<script src="/bg/js/database.js"></script>
<script src="/bg/js/deinflector.js"></script>
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
+ <script src="/bg/js/japanese.js"></script>
<script src="/bg/js/json-schema.js"></script>
<script src="/bg/js/options.js"></script>
<script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/request.js"></script>
- <script src="/bg/js/templates.js"></script>
<script src="/bg/js/translator.js"></script>
<script src="/bg/js/util.js"></script>
<script src="/mixed/js/audio.js"></script>
- <script src="/mixed/js/japanese.js"></script>
<script src="/bg/js/backend.js"></script>
</body>
diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css
index 815a88fa..d686e8f8 100644
--- a/ext/bg/css/settings.css
+++ b/ext/bg/css/settings.css
@@ -222,6 +222,20 @@ html:root[data-operating-system=openbsd] [data-show-for-operating-system~=openbs
display: initial;
}
+html:root[data-browser=edge] [data-hide-for-browser~=edge],
+html:root[data-browser=chrome] [data-hide-for-browser~=chrome],
+html:root[data-browser=firefox] [data-hide-for-browser~=firefox],
+html:root[data-browser=firefox-mobile] [data-hide-for-browser~=firefox-mobile],
+html:root[data-operating-system=mac] [data-hide-for-operating-system~=mac],
+html:root[data-operating-system=win] [data-hide-for-operating-system~=win],
+html:root[data-operating-system=android] [data-hide-for-operating-system~=android],
+html:root[data-operating-system=cros] [data-hide-for-operating-system~=cros],
+html:root[data-operating-system=linux] [data-hide-for-operating-system~=linux],
+html:root[data-operating-system=openbsd] [data-hide-for-operating-system~=openbsd] {
+ display: none;
+}
+
+
@media screen and (max-width: 740px) {
.col-xs-6 {
float: none;
diff --git a/ext/bg/data/dictionary-index-schema.json b/ext/bg/data/dictionary-index-schema.json
new file mode 100644
index 00000000..9311f14c
--- /dev/null
+++ b/ext/bg/data/dictionary-index-schema.json
@@ -0,0 +1,69 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "description": "Index file containing information about the data contained in the dictionary.",
+ "required": [
+ "title",
+ "revision"
+ ],
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Title of the dictionary."
+ },
+ "revision": {
+ "type": "string",
+ "description": "Revision of the dictionary. This value is only used for displaying information."
+ },
+ "sequenced": {
+ "type": "boolean",
+ "default": false,
+ "description": "Whether or not this dictionary can be used as the primary dictionary. Primary dictionaries typically contain term/expression definitions."
+ },
+ "format": {
+ "type": "integer",
+ "description": "Format of data found in the JSON data files.",
+ "enum": [1, 2, 3]
+ },
+ "version": {
+ "type": "integer",
+ "description": "Alias for format.",
+ "enum": [1, 2, 3]
+ },
+ "tagMeta": {
+ "type": "object",
+ "description": "Tag information for terms and kanji. This object is obsolete and individual tag files should be used instead.",
+ "additionalProperties": {
+ "type": "object",
+ "description": "Information about a single tag. The object key is the name of the tag.",
+ "properties": {
+ "category": {
+ "type": "string",
+ "description": "Category for the tag."
+ },
+ "order": {
+ "type": "number",
+ "description": "Sorting order for the tag."
+ },
+ "notes": {
+ "type": "string",
+ "description": "Notes for the tag."
+ },
+ "score": {
+ "type": "number",
+ "description": "Score used to determine popularity. Negative values are more rare and positive values are more frequent. This score is also used to sort search results."
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "anyOf": [
+ {
+ "required": ["format"]
+ },
+ {
+ "required": ["version"]
+ }
+ ]
+} \ No newline at end of file
diff --git a/ext/bg/data/dictionary-kanji-bank-v1-schema.json b/ext/bg/data/dictionary-kanji-bank-v1-schema.json
new file mode 100644
index 00000000..6dad5a7a
--- /dev/null
+++ b/ext/bg/data/dictionary-kanji-bank-v1-schema.json
@@ -0,0 +1,33 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "array",
+ "description": "Data file containing kanji information.",
+ "additionalItems": {
+ "type": "array",
+ "description": "Information about a single kanji character.",
+ "minItems": 4,
+ "items": [
+ {
+ "type": "string",
+ "description": "Kanji character.",
+ "minLength": 1
+ },
+ {
+ "type": "string",
+ "description": "String of space-separated onyomi readings for the kanji character. An empty string is treated as no readings."
+ },
+ {
+ "type": "string",
+ "description": "String of space-separated kunyomi readings for the kanji character. An empty string is treated as no readings."
+ },
+ {
+ "type": "string",
+ "description": "String of space-separated tags for the kanji character. An empty string is treated as no tags."
+ }
+ ],
+ "additionalItems": {
+ "type": "string",
+ "description": "A meaning for the kanji character."
+ }
+ }
+} \ No newline at end of file
diff --git a/ext/bg/data/dictionary-kanji-bank-v3-schema.json b/ext/bg/data/dictionary-kanji-bank-v3-schema.json
new file mode 100644
index 00000000..a5b82039
--- /dev/null
+++ b/ext/bg/data/dictionary-kanji-bank-v3-schema.json
@@ -0,0 +1,44 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "array",
+ "description": "Data file containing kanji information.",
+ "additionalItems": {
+ "type": "array",
+ "description": "Information about a single kanji character.",
+ "minItems": 6,
+ "items": [
+ {
+ "type": "string",
+ "description": "Kanji character.",
+ "minLength": 1
+ },
+ {
+ "type": "string",
+ "description": "String of space-separated onyomi readings for the kanji character. An empty string is treated as no readings."
+ },
+ {
+ "type": "string",
+ "description": "String of space-separated kunyomi readings for the kanji character. An empty string is treated as no readings."
+ },
+ {
+ "type": "string",
+ "description": "String of space-separated tags for the kanji character. An empty string is treated as no tags."
+ },
+ {
+ "type": "array",
+ "description": "Array of meanings for the kanji character.",
+ "items": {
+ "type": "string",
+ "description": "A meaning for the kanji character."
+ }
+ },
+ {
+ "type": "object",
+ "description": "Various stats for the kanji character.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/ext/bg/data/dictionary-kanji-meta-bank-v3-schema.json b/ext/bg/data/dictionary-kanji-meta-bank-v3-schema.json
new file mode 100644
index 00000000..62479026
--- /dev/null
+++ b/ext/bg/data/dictionary-kanji-meta-bank-v3-schema.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "array",
+ "description": "Custom metadata for kanji characters.",
+ "additionalItems": {
+ "type": "array",
+ "description": "Metadata about a single kanji character.",
+ "minItems": 3,
+ "items": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "string",
+ "enum": ["freq"],
+ "description": "Type of data. \"freq\" corresponds to frequency information."
+ },
+ {
+ "type": ["string", "number"],
+ "description": "Data for the character."
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/ext/bg/data/dictionary-tag-bank-v3-schema.json b/ext/bg/data/dictionary-tag-bank-v3-schema.json
new file mode 100644
index 00000000..ee5ca64d
--- /dev/null
+++ b/ext/bg/data/dictionary-tag-bank-v3-schema.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "array",
+ "description": "Data file containing tag information for terms and kanji.",
+ "additionalItems": {
+ "type": "array",
+ "description": "Information about a single tag.",
+ "minItems": 5,
+ "items": [
+ {
+ "type": "string",
+ "description": "Tag name."
+ },
+ {
+ "type": "string",
+ "description": "Category for the tag."
+ },
+ {
+ "type": "number",
+ "description": "Sorting order for the tag."
+ },
+ {
+ "type": "string",
+ "description": "Notes for the tag."
+ },
+ {
+ "type": "number",
+ "description": "Score used to determine popularity. Negative values are more rare and positive values are more frequent. This score is also used to sort search results."
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/ext/bg/data/dictionary-term-bank-v1-schema.json b/ext/bg/data/dictionary-term-bank-v1-schema.json
new file mode 100644
index 00000000..6ffb26e6
--- /dev/null
+++ b/ext/bg/data/dictionary-term-bank-v1-schema.json
@@ -0,0 +1,36 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "array",
+ "description": "Data file containing term and expression information.",
+ "additionalItems": {
+ "type": "array",
+ "description": "Information about a single term/expression.",
+ "minItems": 5,
+ "items": [
+ {
+ "type": "string",
+ "description": "Term or expression."
+ },
+ {
+ "type": "string",
+ "description": "Reading of the term/expression, or an empty string if the reading is the same as the term/expression."
+ },
+ {
+ "type": ["string", "null"],
+ "description": "String of space-separated tags for the definition. An empty string is treated as no tags."
+ },
+ {
+ "type": "string",
+ "description": "String of space-separated rule identifiers for the definition which is used to validate delinflection. Valid rule identifiers are: v1: ichidan verb; v5: godan verb; vs: suru verb; vk: kuru verb; adj-i: i-adjective. An empty string corresponds to words which aren't inflected, such as nouns."
+ },
+ {
+ "type": "number",
+ "description": "Score used to determine popularity. Negative values are more rare and positive values are more frequent. This score is also used to sort search results."
+ }
+ ],
+ "additionalItems": {
+ "type": "string",
+ "description": "Single definition for the term/expression."
+ }
+ }
+} \ No newline at end of file
diff --git a/ext/bg/data/dictionary-term-bank-v3-schema.json b/ext/bg/data/dictionary-term-bank-v3-schema.json
new file mode 100644
index 00000000..bb982e36
--- /dev/null
+++ b/ext/bg/data/dictionary-term-bank-v3-schema.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "array",
+ "description": "Data file containing term and expression information.",
+ "additionalItems": {
+ "type": "array",
+ "description": "Information about a single term/expression.",
+ "minItems": 8,
+ "items": [
+ {
+ "type": "string",
+ "description": "Term or expression."
+ },
+ {
+ "type": "string",
+ "description": "Reading of the term/expression, or an empty string if the reading is the same as the term/expression."
+ },
+ {
+ "type": ["string", "null"],
+ "description": "String of space-separated tags for the definition. An empty string is treated as no tags."
+ },
+ {
+ "type": "string",
+ "description": "String of space-separated rule identifiers for the definition which is used to validate delinflection. Valid rule identifiers are: v1: ichidan verb; v5: godan verb; vs: suru verb; vk: kuru verb; adj-i: i-adjective. An empty string corresponds to words which aren't inflected, such as nouns."
+ },
+ {
+ "type": "number",
+ "description": "Score used to determine popularity. Negative values are more rare and positive values are more frequent. This score is also used to sort search results."
+ },
+ {
+ "type": "array",
+ "description": "Array of definitions for the term/expression.",
+ "items": {
+ "type": "string",
+ "description": "Single definition for the term/expression."
+ }
+ },
+ {
+ "type": "integer",
+ "description": "Sequence number for the term/expression. Terms/expressions with the same sequence number can be shown together when the \"resultOutputMode\" option is set to \"merge\"."
+ },
+ {
+ "type": "string",
+ "description": "String of space-separated tags for the term/expression. An empty string is treated as no tags."
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/ext/bg/data/dictionary-term-meta-bank-v3-schema.json b/ext/bg/data/dictionary-term-meta-bank-v3-schema.json
new file mode 100644
index 00000000..1cc0557f
--- /dev/null
+++ b/ext/bg/data/dictionary-term-meta-bank-v3-schema.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "array",
+ "description": "Custom metadata for terms/expressions.",
+ "additionalItems": {
+ "type": "array",
+ "description": "Metadata about a single term/expression.",
+ "minItems": 3,
+ "items": [
+ {
+ "type": "string",
+ "description": "Term or expression."
+ },
+ {
+ "type": "string",
+ "enum": ["freq"],
+ "description": "Type of data. \"freq\" corresponds to frequency information."
+ },
+ {
+ "type": ["string", "number"],
+ "description": "Data for the term/expression."
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json
index a20a0619..d6207952 100644
--- a/ext/bg/data/options-schema.json
+++ b/ext/bg/data/options-schema.json
@@ -79,6 +79,7 @@
"type": "object",
"required": [
"enable",
+ "enableClipboardPopups",
"resultOutputMode",
"debugInfo",
"maxResults",
@@ -111,6 +112,10 @@
"type": "boolean",
"default": true
},
+ "enableClipboardPopups": {
+ "type": "boolean",
+ "default": false
+ },
"resultOutputMode": {
"type": "string",
"enum": ["group", "merge", "split"],
@@ -290,7 +295,8 @@
"popupNestingMaxDepth",
"enablePopupSearch",
"enableOnPopupExpressions",
- "enableOnSearchPage"
+ "enableOnSearchPage",
+ "enableSearchTags"
],
"properties": {
"middleMouse": {
@@ -348,6 +354,10 @@
"enableOnSearchPage": {
"type": "boolean",
"default": true
+ },
+ "enableSearchTags": {
+ "type": "boolean",
+ "default": false
}
}
},
diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js
index 10a07061..39c6ad51 100644
--- a/ext/bg/js/anki.js
+++ b/ext/bg/js/anki.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global requestJson*/
/*
* AnkiConnect
diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js
index 285b8016..0c244ffa 100644
--- a/ext/bg/js/api.js
+++ b/ext/bg/js/api.js
@@ -17,16 +17,16 @@
*/
-function apiTemplateRender(template, data, dynamic) {
- return _apiInvoke('templateRender', {data, template, dynamic});
+function apiTemplateRender(template, data) {
+ return _apiInvoke('templateRender', {data, template});
}
function apiAudioGetUrl(definition, source, optionsContext) {
return _apiInvoke('audioGetUrl', {definition, source, optionsContext});
}
-function apiGetDisplayTemplatesHtml() {
- return _apiInvoke('getDisplayTemplatesHtml');
+function apiClipboardGet() {
+ return _apiInvoke('clipboardGet');
}
function _apiInvoke(action, params={}) {
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js
index 36ac413b..d300570b 100644
--- a/ext/bg/js/audio.js
+++ b/ext/bg/js/audio.js
@@ -16,13 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global jpIsStringEntirelyKana, audioGetFromSources*/
const audioUrlBuilders = new Map([
['jpod101', async (definition) => {
let kana = definition.reading;
let kanji = definition.expression;
- if (!kana && wanakana.isHiragana(kanji)) {
+ if (!kana && jpIsStringEntirelyKana(kanji)) {
kana = kanji;
kanji = null;
}
@@ -51,7 +52,7 @@ const audioUrlBuilders = new Map([
for (const row of dom.getElementsByClassName('dc-result-row')) {
try {
const url = row.querySelector('audio>source[src]').getAttribute('src');
- const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText;
+ const reading = row.getElementsByClassName('dc-vocab_kana').item(0).textContent;
if (url && reading && (!definition.reading || definition.reading === reading)) {
return audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/');
}
@@ -167,10 +168,8 @@ async function audioInject(definition, fields, sources, optionsContext) {
}
try {
- let audioSourceDefinition = definition;
- if (hasOwn(definition, 'expressions')) {
- audioSourceDefinition = definition.expressions[0];
- }
+ const expressions = definition.expressions;
+ const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition;
const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, true);
if (url !== null) {
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index eeab68a5..e3bf7bda 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -16,12 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global optionsSave, utilIsolate
+conditionsTestValue, profileConditionsDescriptor, profileOptionsGetDefaultFieldTemplates
+handlebarsRenderDynamic
+requestText, requestJson, optionsLoad
+dictConfigured, dictTermsSort, dictEnabledSet, dictNoteFormat
+audioGetUrl, audioInject
+jpConvertReading, jpDistributeFuriganaInflected, jpKatakanaToHiragana
+Translator, AnkiConnect, AnkiNull, Mecab, BackendApiForwarder, JsonSchema, ClipboardMonitor*/
class Backend {
constructor() {
this.translator = new Translator();
this.anki = new AnkiNull();
this.mecab = new Mecab();
+ this.clipboardMonitor = new ClipboardMonitor();
this.options = null;
this.optionsSchema = null;
this.optionsContext = {
@@ -34,7 +43,11 @@ class Backend {
this.clipboardPasteTarget = document.querySelector('#clipboard-paste-target');
+ this.popupWindow = null;
+
this.apiForwarder = new BackendApiForwarder();
+
+ this.messageToken = yomichan.generateId(16);
}
async prepare() {
@@ -67,6 +80,8 @@ class Backend {
this.isPreparedResolve();
this.isPreparedResolve = null;
this.isPreparedPromise = null;
+
+ this.clipboardMonitor.onClipboardText = (text) => this._onClipboardText(text);
}
onOptionsUpdated(source) {
@@ -75,7 +90,7 @@ class Backend {
const callback = () => this.checkLastError(chrome.runtime.lastError);
chrome.tabs.query({}, (tabs) => {
for (const tab of tabs) {
- chrome.tabs.sendMessage(tab.id, {action: 'optionsUpdate', params: {source}}, callback);
+ chrome.tabs.sendMessage(tab.id, {action: 'optionsUpdated', params: {source}}, callback);
}
});
}
@@ -97,6 +112,10 @@ class Backend {
}
}
+ _onClipboardText(text) {
+ this._onCommandSearch({mode: 'popup', query: text});
+ }
+
_onZoomChange({tabId, oldZoomFactor, newZoomFactor}) {
const callback = () => this.checkLastError(chrome.runtime.lastError);
chrome.tabs.sendMessage(tabId, {action: 'zoomChanged', params: {oldZoomFactor, newZoomFactor}}, callback);
@@ -121,6 +140,12 @@ class Backend {
} else {
this.mecab.stopListener();
}
+
+ if (options.general.enableClipboardPopups) {
+ this.clipboardMonitor.start();
+ } else {
+ this.clipboardMonitor.stop();
+ }
}
async getOptionsSchema() {
@@ -249,18 +274,18 @@ class Backend {
const node = nodes.pop();
for (const key of Object.keys(node.obj)) {
const path = node.path.concat(key);
- const obj = node.obj[key];
- if (obj !== null && typeof obj === 'object') {
- nodes.unshift({obj, path});
+ const obj2 = node.obj[key];
+ if (obj2 !== null && typeof obj2 === 'object') {
+ nodes.unshift({obj: obj2, path});
} else {
- valuePaths.push([obj, path]);
+ valuePaths.push([obj2, path]);
}
}
}
return valuePaths;
}
- function modifyOption(path, value, options) {
+ function modifyOption(path, value) {
let pivot = options;
for (const key of path.slice(0, -1)) {
if (!hasOwn(pivot, key)) {
@@ -273,7 +298,7 @@ class Backend {
}
for (const [value, path] of getValuePaths(changedOptions)) {
- modifyOption(path, value, options);
+ modifyOption(path, value);
}
await this._onApiOptionsSave({source});
@@ -294,7 +319,8 @@ class Backend {
async _onApiTermsFind({text, details, optionsContext}) {
const options = await this.getOptions(optionsContext);
- const [definitions, length] = await this.translator.findTerms(text, details, options);
+ const mode = options.general.resultOutputMode;
+ const [definitions, length] = await this.translator.findTerms(mode, text, details, options);
definitions.splice(options.general.maxResults);
return {length, definitions};
}
@@ -304,9 +330,9 @@ class Backend {
const results = [];
while (text.length > 0) {
const term = [];
- const [definitions, sourceLength] = await this.translator.findTermsInternal(
+ const [definitions, sourceLength] = await this.translator.findTerms(
+ 'simple',
text.substring(0, options.scanning.length),
- dictEnabledSet(options),
{},
options
);
@@ -314,9 +340,9 @@ class Backend {
dictTermsSort(definitions);
const {expression, reading} = definitions[0];
const source = text.substring(0, sourceLength);
- for (const {text, furigana} of jpDistributeFuriganaInflected(expression, reading, source)) {
- const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
- term.push({text, reading});
+ for (const {text: text2, furigana} of jpDistributeFuriganaInflected(expression, reading, source)) {
+ const reading2 = jpConvertReading(text2, furigana, options.parsing.readingMode);
+ term.push({text: text2, reading: reading2});
}
text = text.substring(source.length);
} else {
@@ -339,17 +365,17 @@ class Backend {
for (const {expression, reading, source} of parsedLine) {
const term = [];
if (expression !== null && reading !== null) {
- for (const {text, furigana} of jpDistributeFuriganaInflected(
+ for (const {text: text2, furigana} of jpDistributeFuriganaInflected(
expression,
jpKatakanaToHiragana(reading),
source
)) {
- const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
- term.push({text, reading});
+ const reading2 = jpConvertReading(text2, furigana, options.parsing.readingMode);
+ term.push({text: text2, reading: reading2});
}
} else {
- const reading = jpConvertReading(source, null, options.parsing.readingMode);
- term.push({text: source, reading});
+ const reading2 = jpConvertReading(source, null, options.parsing.readingMode);
+ term.push({text: source, reading: reading2});
}
result.push(term);
}
@@ -436,12 +462,8 @@ class Backend {
return this.anki.guiBrowse(`nid:${noteId}`);
}
- async _onApiTemplateRender({template, data, dynamic}) {
- return (
- dynamic ?
- handlebarsRenderDynamic(template, data) :
- handlebarsRenderStatic(template, data)
- );
+ async _onApiTemplateRender({template, data}) {
+ return handlebarsRenderDynamic(template, data);
}
async _onApiCommandExec({command, params}) {
@@ -480,19 +502,30 @@ class Backend {
return Promise.resolve({frameId});
}
- _onApiInjectStylesheet({css}, sender) {
+ _onApiInjectStylesheet({type, value}, sender) {
if (!sender.tab) {
return Promise.reject(new Error('Invalid tab'));
}
const tabId = sender.tab.id;
const frameId = sender.frameId;
- const details = {
- code: css,
- runAt: 'document_start',
- cssOrigin: 'user',
- allFrames: false
- };
+ const details = (
+ type === 'file' ?
+ {
+ file: value,
+ runAt: 'document_start',
+ cssOrigin: 'author',
+ allFrames: false,
+ matchAboutBlank: true
+ } :
+ {
+ code: value,
+ runAt: 'document_start',
+ cssOrigin: 'user',
+ allFrames: false,
+ matchAboutBlank: true
+ }
+ );
if (typeof frameId === 'number') {
details.frameId = frameId;
}
@@ -521,13 +554,30 @@ class Backend {
}
async _onApiClipboardGet() {
- const clipboardPasteTarget = this.clipboardPasteTarget;
- clipboardPasteTarget.value = '';
- clipboardPasteTarget.focus();
- document.execCommand('paste');
- const result = clipboardPasteTarget.value;
- clipboardPasteTarget.value = '';
- return result;
+ /*
+ Notes:
+ document.execCommand('paste') doesn't work on Firefox.
+ This may be a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
+ Therefore, navigator.clipboard.readText() is used on Firefox.
+
+ navigator.clipboard.readText() can't be used in Chrome for two reasons:
+ * Requires page to be focused, else it rejects with an exception.
+ * When the page is focused, Chrome will request clipboard permission, despite already
+ being an extension with clipboard permissions. It effectively asks for the
+ non-extension permission for clipboard access.
+ */
+ const browser = await Backend._getBrowser();
+ if (browser === 'firefox' || browser === 'firefox-mobile') {
+ return await navigator.clipboard.readText();
+ } else {
+ const clipboardPasteTarget = this.clipboardPasteTarget;
+ clipboardPasteTarget.value = '';
+ clipboardPasteTarget.focus();
+ document.execCommand('paste');
+ const result = clipboardPasteTarget.value;
+ clipboardPasteTarget.value = '';
+ return result;
+ }
}
async _onApiGetDisplayTemplatesHtml() {
@@ -535,6 +585,11 @@ class Backend {
return await requestText(url, 'GET');
}
+ async _onApiGetQueryParserTemplatesHtml() {
+ const url = chrome.runtime.getURL('/bg/query-parser-templates.html');
+ return await requestText(url, 'GET');
+ }
+
_onApiGetZoom(params, sender) {
if (!sender || !sender.tab) {
return Promise.reject(new Error('Invalid tab'));
@@ -562,26 +617,75 @@ class Backend {
});
}
+ async _onApiGetMessageToken() {
+ return this.messageToken;
+ }
+
// Command handlers
async _onCommandSearch(params) {
- const url = chrome.runtime.getURL('/bg/search.html');
- if (!(params && params.newTab)) {
- try {
- const tab = await Backend._findTab(1000, (url2) => (
- url2 !== null &&
- url2.startsWith(url) &&
- (url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#')
- ));
- if (tab !== null) {
- await Backend._focusTab(tab);
- return;
+ const {mode='existingOrNewTab', query} = params || {};
+
+ const options = await this.getOptions(this.optionsContext);
+ const {popupWidth, popupHeight} = options.general;
+
+ const baseUrl = chrome.runtime.getURL('/bg/search.html');
+ const queryParams = {mode};
+ if (query && query.length > 0) { queryParams.query = query; }
+ const queryString = new URLSearchParams(queryParams).toString();
+ const url = `${baseUrl}?${queryString}`;
+
+ const isTabMatch = (url2) => {
+ if (url2 === null || !url2.startsWith(baseUrl)) { return false; }
+ const {baseUrl: baseUrl2, queryParams: queryParams2} = parseUrl(url2);
+ return baseUrl2 === baseUrl && (queryParams2.mode === mode || (!queryParams2.mode && mode === 'existingOrNewTab'));
+ };
+
+ const openInTab = async () => {
+ const tab = await Backend._findTab(1000, isTabMatch);
+ if (tab !== null) {
+ await Backend._focusTab(tab);
+ if (queryParams.query) {
+ await new Promise((resolve) => chrome.tabs.sendMessage(
+ tab.id, {action: 'searchQueryUpdate', params: {query: queryParams.query}}, resolve
+ ));
}
- } catch (e) {
- // NOP
+ return true;
}
+ };
+
+ switch (mode) {
+ case 'existingOrNewTab':
+ try {
+ if (await openInTab()) { return; }
+ } catch (e) {
+ // NOP
+ }
+ chrome.tabs.create({url});
+ return;
+ case 'newTab':
+ chrome.tabs.create({url});
+ return;
+ case 'popup':
+ try {
+ // chrome.windows not supported (e.g. on Firefox mobile)
+ if (!isObject(chrome.windows)) { return; }
+ if (await openInTab()) { return; }
+ // if the previous popup is open in an invalid state, close it
+ if (this.popupWindow !== null) {
+ const callback = () => this.checkLastError(chrome.runtime.lastError);
+ chrome.windows.remove(this.popupWindow.id, callback);
+ }
+ // open new popup
+ this.popupWindow = await new Promise((resolve) => chrome.windows.create(
+ {url, width: popupWidth, height: popupHeight, type: 'popup'},
+ resolve
+ ));
+ } catch (e) {
+ // NOP
+ }
+ return;
}
- chrome.tabs.create({url});
}
_onCommandHelp() {
@@ -697,8 +801,11 @@ class Backend {
await new Promise((resolve, reject) => {
chrome.tabs.update(tab.id, {active: true}, () => {
const e = chrome.runtime.lastError;
- if (e) { reject(e); }
- else { resolve(); }
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
+ }
});
});
@@ -708,19 +815,25 @@ class Backend {
}
try {
- const tabWindow = await new Promise((resolve) => {
- chrome.windows.get(tab.windowId, {}, (tabWindow) => {
+ const tabWindow = await new Promise((resolve, reject) => {
+ chrome.windows.get(tab.windowId, {}, (value) => {
const e = chrome.runtime.lastError;
- if (e) { reject(e); }
- else { resolve(tabWindow); }
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve(value);
+ }
});
});
if (!tabWindow.focused) {
await new Promise((resolve, reject) => {
chrome.windows.update(tab.windowId, {focused: true}, () => {
const e = chrome.runtime.lastError;
- if (e) { reject(e); }
- else { resolve(); }
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
+ }
});
});
}
@@ -777,7 +890,9 @@ Backend._messageHandlers = new Map([
['getEnvironmentInfo', (self, ...args) => self._onApiGetEnvironmentInfo(...args)],
['clipboardGet', (self, ...args) => self._onApiClipboardGet(...args)],
['getDisplayTemplatesHtml', (self, ...args) => self._onApiGetDisplayTemplatesHtml(...args)],
- ['getZoom', (self, ...args) => self._onApiGetZoom(...args)]
+ ['getQueryParserTemplatesHtml', (self, ...args) => self._onApiGetQueryParserTemplatesHtml(...args)],
+ ['getZoom', (self, ...args) => self._onApiGetZoom(...args)],
+ ['getMessageToken', (self, ...args) => self._onApiGetMessageToken(...args)]
]);
Backend._commandHandlers = new Map([
diff --git a/ext/bg/js/clipboard-monitor.js b/ext/bg/js/clipboard-monitor.js
new file mode 100644
index 00000000..c2f41385
--- /dev/null
+++ b/ext/bg/js/clipboard-monitor.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+/*global apiClipboardGet, jpIsStringPartiallyJapanese*/
+
+class ClipboardMonitor {
+ constructor() {
+ this.timerId = null;
+ this.timerToken = null;
+ this.interval = 250;
+ this.previousText = null;
+ }
+
+ onClipboardText(_text) {
+ throw new Error('Override me');
+ }
+
+ start() {
+ this.stop();
+
+ // The token below is used as a unique identifier to ensure that a new clipboard monitor
+ // hasn't been started during the await call. The check below the await apiClipboardGet()
+ // call will exit early if the reference has changed.
+ const token = {};
+ const intervalCallback = async () => {
+ this.timerId = null;
+
+ let text = null;
+ try {
+ text = await apiClipboardGet();
+ } catch (e) {
+ // NOP
+ }
+ if (this.timerToken !== token) { return; }
+
+ if (
+ typeof text === 'string' &&
+ (text = text.trim()).length > 0 &&
+ text !== this.previousText
+ ) {
+ this.previousText = text;
+ if (jpIsStringPartiallyJapanese(text)) {
+ this.onClipboardText(text);
+ }
+ }
+
+ this.timerId = setTimeout(intervalCallback, this.interval);
+ };
+
+ this.timerToken = token;
+
+ intervalCallback();
+ }
+
+ stop() {
+ this.timerToken = null;
+ if (this.timerId !== null) {
+ clearTimeout(this.timerId);
+ this.timerId = null;
+ }
+ }
+
+ setPreviousText(text) {
+ this.previousText = text;
+ }
+}
diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js
index 834174bf..bec964fb 100644
--- a/ext/bg/js/context.js
+++ b/ext/bg/js/context.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiCommandExec, apiGetEnvironmentInfo, apiOptionsGet*/
function showExtensionInfo() {
const node = document.getElementById('extension-info');
@@ -30,12 +31,12 @@ function setupButtonEvents(selector, command, url) {
for (const node of nodes) {
node.addEventListener('click', (e) => {
if (e.button !== 0) { return; }
- apiCommandExec(command, {newTab: e.ctrlKey});
+ apiCommandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'});
e.preventDefault();
}, false);
node.addEventListener('auxclick', (e) => {
if (e.button !== 1) { return; }
- apiCommandExec(command, {newTab: true});
+ apiCommandExec(command, {mode: 'newTab'});
e.preventDefault();
}, false);
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index e87cc64b..558f3ceb 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -16,20 +16,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global dictFieldSplit, requestJson, JsonSchema, JSZip*/
class Database {
constructor() {
this.db = null;
+ this._schemas = new Map();
}
+ // Public
+
async prepare() {
if (this.db !== null) {
throw new Error('Database already initialized');
}
try {
- this.db = await Database.open('dict', 5, (db, transaction, oldVersion) => {
- Database.upgrade(db, transaction, oldVersion, [
+ this.db = await Database._open('dict', 5, (db, transaction, oldVersion) => {
+ Database._upgrade(db, transaction, oldVersion, [
{
version: 2,
stores: {
@@ -95,18 +99,24 @@ class Database {
}
}
+ async close() {
+ this._validate();
+ this.db.close();
+ this.db = null;
+ }
+
async purge() {
- this.validate();
+ this._validate();
this.db.close();
- await Database.deleteDatabase(this.db.name);
+ await Database._deleteDatabase(this.db.name);
this.db = null;
await this.prepare();
}
async deleteDictionary(dictionaryName, onProgress, progressSettings) {
- this.validate();
+ this._validate();
const targets = [
['dictionaries', 'title'],
@@ -133,22 +143,22 @@ class Database {
const dbObjectStore = dbTransaction.objectStore(objectStoreName);
const dbIndex = dbObjectStore.index(index);
const only = IDBKeyRange.only(dictionaryName);
- promises.push(Database.deleteValues(dbObjectStore, dbIndex, only, onProgress, progressData, progressRate));
+ promises.push(Database._deleteValues(dbObjectStore, dbIndex, only, onProgress, progressData, progressRate));
}
await Promise.all(promises);
}
- async findTermsBulk(termList, titles, wildcard) {
- this.validate();
+ async findTermsBulk(termList, dictionaries, wildcard) {
+ this._validate();
const promises = [];
- const visited = {};
+ const visited = new Set();
const results = [];
const processRow = (row, index) => {
- if (titles.includes(row.dictionary) && !hasOwn(visited, row.id)) {
- visited[row.id] = true;
- results.push(Database.createTerm(row, index));
+ if (dictionaries.has(row.dictionary) && !visited.has(row.id)) {
+ visited.add(row.id);
+ results.push(Database._createTerm(row, index));
}
};
@@ -164,8 +174,8 @@ class Database {
const term = prefixWildcard ? stringReverse(termList[i]) : termList[i];
const query = useWildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term);
promises.push(
- Database.getAll(dbIndex1, query, i, processRow),
- Database.getAll(dbIndex2, query, i, processRow)
+ Database._getAll(dbIndex1, query, i, processRow),
+ Database._getAll(dbIndex2, query, i, processRow)
);
}
@@ -174,14 +184,14 @@ class Database {
return results;
}
- async findTermsExactBulk(termList, readingList, titles) {
- this.validate();
+ async findTermsExactBulk(termList, readingList, dictionaries) {
+ this._validate();
const promises = [];
const results = [];
const processRow = (row, index) => {
- if (row.reading === readingList[index] && titles.includes(row.dictionary)) {
- results.push(Database.createTerm(row, index));
+ if (row.reading === readingList[index] && dictionaries.has(row.dictionary)) {
+ results.push(Database._createTerm(row, index));
}
};
@@ -191,7 +201,7 @@ class Database {
for (let i = 0; i < termList.length; ++i) {
const only = IDBKeyRange.only(termList[i]);
- promises.push(Database.getAll(dbIndex, only, i, processRow));
+ promises.push(Database._getAll(dbIndex, only, i, processRow));
}
await Promise.all(promises);
@@ -200,13 +210,13 @@ class Database {
}
async findTermsBySequenceBulk(sequenceList, mainDictionary) {
- this.validate();
+ this._validate();
const promises = [];
const results = [];
const processRow = (row, index) => {
if (row.dictionary === mainDictionary) {
- results.push(Database.createTerm(row, index));
+ results.push(Database._createTerm(row, index));
}
};
@@ -216,7 +226,7 @@ class Database {
for (let i = 0; i < sequenceList.length; ++i) {
const only = IDBKeyRange.only(sequenceList[i]);
- promises.push(Database.getAll(dbIndex, only, i, processRow));
+ promises.push(Database._getAll(dbIndex, only, i, processRow));
}
await Promise.all(promises);
@@ -224,52 +234,27 @@ class Database {
return results;
}
- async findTermMetaBulk(termList, titles) {
- return this.findGenericBulk('termMeta', 'expression', termList, titles, Database.createTermMeta);
+ async findTermMetaBulk(termList, dictionaries) {
+ return this._findGenericBulk('termMeta', 'expression', termList, dictionaries, Database._createTermMeta);
}
- async findKanjiBulk(kanjiList, titles) {
- return this.findGenericBulk('kanji', 'character', kanjiList, titles, Database.createKanji);
+ async findKanjiBulk(kanjiList, dictionaries) {
+ return this._findGenericBulk('kanji', 'character', kanjiList, dictionaries, Database._createKanji);
}
- async findKanjiMetaBulk(kanjiList, titles) {
- return this.findGenericBulk('kanjiMeta', 'character', kanjiList, titles, Database.createKanjiMeta);
- }
-
- async findGenericBulk(tableName, indexName, indexValueList, titles, createResult) {
- this.validate();
-
- const promises = [];
- const results = [];
- const processRow = (row, index) => {
- if (titles.includes(row.dictionary)) {
- results.push(createResult(row, index));
- }
- };
-
- const dbTransaction = this.db.transaction([tableName], 'readonly');
- const dbTerms = dbTransaction.objectStore(tableName);
- const dbIndex = dbTerms.index(indexName);
-
- for (let i = 0; i < indexValueList.length; ++i) {
- const only = IDBKeyRange.only(indexValueList[i]);
- promises.push(Database.getAll(dbIndex, only, i, processRow));
- }
-
- await Promise.all(promises);
-
- return results;
+ async findKanjiMetaBulk(kanjiList, dictionaries) {
+ return this._findGenericBulk('kanjiMeta', 'character', kanjiList, dictionaries, Database._createKanjiMeta);
}
async findTagForTitle(name, title) {
- this.validate();
+ this._validate();
let result = null;
const dbTransaction = this.db.transaction(['tagMeta'], 'readonly');
const dbTerms = dbTransaction.objectStore('tagMeta');
const dbIndex = dbTerms.index('name');
const only = IDBKeyRange.only(name);
- await Database.getAll(dbIndex, only, null, (row) => {
+ await Database._getAll(dbIndex, only, null, (row) => {
if (title === row.dictionary) {
result = row;
}
@@ -279,19 +264,19 @@ class Database {
}
async getDictionaryInfo() {
- this.validate();
+ this._validate();
const results = [];
const dbTransaction = this.db.transaction(['dictionaries'], 'readonly');
const dbDictionaries = dbTransaction.objectStore('dictionaries');
- await Database.getAll(dbDictionaries, null, null, (info) => results.push(info));
+ await Database._getAll(dbDictionaries, null, null, (info) => results.push(info));
return results;
}
async getDictionaryCounts(dictionaryNames, getTotal) {
- this.validate();
+ this._validate();
const objectStoreNames = [
'kanji',
@@ -312,7 +297,7 @@ class Database {
// Query is required for Edge, otherwise index.count throws an exception.
const query1 = IDBKeyRange.lowerBound('', false);
- const totalPromise = getTotal ? Database.getCounts(targets, query1) : null;
+ const totalPromise = getTotal ? Database._getCounts(targets, query1) : null;
const counts = [];
const countPromises = [];
@@ -320,7 +305,7 @@ class Database {
counts.push(null);
const index = i;
const query2 = IDBKeyRange.only(dictionaryNames[i]);
- const countPromise = Database.getCounts(targets, query2).then((v) => counts[index] = v);
+ const countPromise = Database._getCounts(targets, query2).then((v) => counts[index] = v);
countPromises.push(countPromise);
}
await Promise.all(countPromises);
@@ -332,278 +317,287 @@ class Database {
return result;
}
- async importDictionary(archive, progressCallback, details) {
- this.validate();
+ async importDictionary(archiveSource, onProgress, details) {
+ this._validate();
+ const db = this.db;
+ const hasOnProgress = (typeof onProgress === 'function');
- const errors = [];
- const prefixWildcardsSupported = details.prefixWildcardsSupported;
+ // Read archive
+ const archive = await JSZip.loadAsync(archiveSource);
- const maxTransactionLength = 1000;
- const bulkAdd = async (objectStoreName, items, total, current) => {
- const db = this.db;
- for (let i = 0; i < items.length; i += maxTransactionLength) {
- if (progressCallback) {
- progressCallback(total, current + i / items.length);
- }
+ // Read and validate index
+ const indexFileName = 'index.json';
+ const indexFile = archive.files[indexFileName];
+ if (!indexFile) {
+ throw new Error('No dictionary index found in archive');
+ }
- try {
- const count = Math.min(maxTransactionLength, items.length - i);
- const transaction = db.transaction([objectStoreName], 'readwrite');
- const objectStore = transaction.objectStore(objectStoreName);
- await Database.bulkAdd(objectStore, items, i, count);
- } catch (e) {
- errors.push(e);
- }
- }
- };
+ const index = JSON.parse(await indexFile.async('string'));
- const indexDataLoaded = async (summary) => {
- if (summary.version > 3) {
- throw new Error('Unsupported dictionary version');
- }
+ const indexSchema = await this._getSchema('/bg/data/dictionary-index-schema.json');
+ Database._validateJsonSchema(index, indexSchema, indexFileName);
- const db = this.db;
- const dbCountTransaction = db.transaction(['dictionaries'], 'readonly');
- const dbIndex = dbCountTransaction.objectStore('dictionaries').index('title');
- const only = IDBKeyRange.only(summary.title);
- const count = await Database.getCount(dbIndex, only);
+ const dictionaryTitle = index.title;
+ const version = index.format || index.version;
- if (count > 0) {
- throw new Error('Dictionary is already imported');
- }
+ if (!dictionaryTitle || !index.revision) {
+ throw new Error('Unrecognized dictionary format');
+ }
- const transaction = db.transaction(['dictionaries'], 'readwrite');
- const objectStore = transaction.objectStore('dictionaries');
- await Database.bulkAdd(objectStore, [summary], 0, 1);
- };
+ // Verify database is not already imported
+ if (await this._dictionaryExists(dictionaryTitle)) {
+ throw new Error('Dictionary is already imported');
+ }
- const termDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- if (summary.version === 1) {
- for (const [expression, reading, definitionTags, rules, score, ...glossary] of entries) {
- rows.push({
- expression,
- reading,
- definitionTags,
- rules,
- score,
- glossary,
- dictionary: summary.title
- });
- }
+ // 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 {
- for (const [expression, reading, definitionTags, rules, score, glossary, sequence, termTags] of entries) {
- rows.push({
- expression,
- reading,
- definitionTags,
- rules,
- score,
- glossary,
- sequence,
- termTags,
- dictionary: summary.title
- });
- }
- }
-
- if (prefixWildcardsSupported) {
- for (const row of rows) {
- row.expressionReverse = stringReverse(row.expression);
- row.readingReverse = stringReverse(row.reading);
- }
+ const [expression, reading, definitionTags, rules, score, glossary, sequence, termTags] = entry;
+ return {expression, reading, definitionTags, rules, score, glossary, sequence, termTags};
}
+ };
- await bulkAdd('terms', rows, total, current);
+ const convertTermMetaBankEntry = (entry) => {
+ const [expression, mode, data] = entry;
+ return {expression, mode, data};
};
- const termMetaDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- for (const [expression, mode, data] of entries) {
- rows.push({
- expression,
- mode,
- data,
- dictionary: summary.title
- });
+ 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};
}
+ };
- await bulkAdd('termMeta', rows, total, current);
+ const convertKanjiMetaBankEntry = (entry) => {
+ const [character, mode, data] = entry;
+ return {character, mode, data};
};
- const kanjiDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- if (summary.version === 1) {
- for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) {
- rows.push({
- character,
- onyomi,
- kunyomi,
- tags,
- meanings,
- dictionary: summary.title
- });
- }
- } else {
- for (const [character, onyomi, kunyomi, tags, meanings, stats] of entries) {
- rows.push({
- character,
- onyomi,
- kunyomi,
- tags,
- meanings,
- stats,
- dictionary: summary.title
- });
+ 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);
}
}
-
- await bulkAdd('kanji', rows, total, current);
+ return results;
};
- const kanjiMetaDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- for (const [character, mode, data] of entries) {
- rows.push({
- character,
- mode,
- data,
- dictionary: summary.title
- });
+ // 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});
}
+ }
- await bulkAdd('kanjiMeta', rows, total, current);
- };
-
- const tagDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- for (const [name, category, order, notes, score] of entries) {
- const row = dictTagSanitize({
- name,
- category,
- order,
- notes,
- score,
- dictionary: summary.title
- });
-
- rows.push(row);
+ // Prefix wildcard support
+ const prefixWildcardsSupported = !!details.prefixWildcardsSupported;
+ if (prefixWildcardsSupported) {
+ for (const entry of termList) {
+ entry.expressionReverse = stringReverse(entry.expression);
+ entry.readingReverse = stringReverse(entry.reading);
}
+ }
- await bulkAdd('tagMeta', rows, total, current);
+ // Add dictionary
+ const summary = {
+ title: dictionaryTitle,
+ revision: index.revision,
+ sequenced: index.sequenced,
+ version,
+ prefixWildcardsSupported
};
- const result = await Database.importDictionaryZip(
- archive,
- indexDataLoaded,
- termDataLoaded,
- termMetaDataLoaded,
- kanjiDataLoaded,
- kanjiMetaDataLoaded,
- tagDataLoaded,
- details
+ {
+ const transaction = db.transaction(['dictionaries'], 'readwrite');
+ const objectStore = transaction.objectStore('dictionaries');
+ await Database._bulkAdd(objectStore, [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 {
+ const transaction = db.transaction([objectStoreName], 'readwrite');
+ const objectStore = transaction.objectStore(objectStoreName);
+ await Database._bulkAdd(objectStore, entries, i, count);
+ } catch (e) {
+ errors.push(e);
+ }
- return {result, errors};
+ 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};
}
- validate() {
+ // Private
+
+ _validate() {
if (this.db === null) {
throw new Error('Database not initialized');
}
}
- static async importDictionaryZip(
- archive,
- indexDataLoaded,
- termDataLoaded,
- termMetaDataLoaded,
- kanjiDataLoaded,
- kanjiMetaDataLoaded,
- tagDataLoaded,
- details
- ) {
- const zip = await JSZip.loadAsync(archive);
-
- const indexFile = zip.files['index.json'];
- if (!indexFile) {
- throw new Error('No dictionary index found in archive');
+ async _getSchema(fileName) {
+ let schemaPromise = this._schemas.get(fileName);
+ if (typeof schemaPromise !== 'undefined') {
+ return schemaPromise;
}
- const index = JSON.parse(await indexFile.async('string'));
- if (!index.title || !index.revision) {
- throw new Error('Unrecognized dictionary format');
+ 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);
}
+ }
- const summary = {
- title: index.title,
- revision: index.revision,
- sequenced: index.sequenced,
- version: index.format || index.version,
- prefixWildcardsSupported: !!details.prefixWildcardsSupported
- };
+ static _formatSchemaError(e, fileName) {
+ const valuePathString = Database._getSchemaErrorPathString(e.info.valuePath, 'dictionary');
+ const schemaPathString = Database._getSchemaErrorPathString(e.info.schemaPath, 'schema');
- await indexDataLoaded(summary);
+ const e2 = new Error(`Dictionary has invalid data in '${fileName}' for value '${valuePathString}', validated against '${schemaPathString}': ${e.message}`);
+ e2.data = e;
- const buildTermBankName = (index) => `term_bank_${index + 1}.json`;
- const buildTermMetaBankName = (index) => `term_meta_bank_${index + 1}.json`;
- const buildKanjiBankName = (index) => `kanji_bank_${index + 1}.json`;
- const buildKanjiMetaBankName = (index) => `kanji_meta_bank_${index + 1}.json`;
- const buildTagBankName = (index) => `tag_bank_${index + 1}.json`;
+ return e2;
+ }
- const countBanks = (namer) => {
- let count = 0;
- while (zip.files[namer(count)]) {
- ++count;
+ 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;
+ }
- return count;
- };
+ 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';
- const termBankCount = countBanks(buildTermBankName);
- const termMetaBankCount = countBanks(buildTermMetaBankName);
- const kanjiBankCount = countBanks(buildKanjiBankName);
- const kanjiMetaBankCount = countBanks(buildKanjiMetaBankName);
- const tagBankCount = countBanks(buildTagBankName);
-
- let bankLoadedCount = 0;
- let bankTotalCount =
- termBankCount +
- termMetaBankCount +
- kanjiBankCount +
- kanjiMetaBankCount +
- tagBankCount;
-
- if (tagDataLoaded && index.tagMeta) {
- const bank = [];
- for (const name in index.tagMeta) {
- const tag = index.tagMeta[name];
- bank.push([name, tag.category, tag.order, tag.notes, tag.score]);
- }
+ return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank];
+ }
- tagDataLoaded(summary, bank, ++bankTotalCount, bankLoadedCount++);
- }
+ 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;
+ }
- const loadBank = async (summary, namer, count, callback) => {
- if (callback) {
- for (let i = 0; i < count; ++i) {
- const bankFile = zip.files[namer(i)];
- const bank = JSON.parse(await bankFile.async('string'));
- await callback(summary, bank, bankTotalCount, bankLoadedCount++);
- }
+ async _findGenericBulk(tableName, indexName, indexValueList, dictionaries, createResult) {
+ this._validate();
+
+ const promises = [];
+ const results = [];
+ const processRow = (row, index) => {
+ if (dictionaries.has(row.dictionary)) {
+ results.push(createResult(row, index));
}
};
- await loadBank(summary, buildTermBankName, termBankCount, termDataLoaded);
- await loadBank(summary, buildTermMetaBankName, termMetaBankCount, termMetaDataLoaded);
- await loadBank(summary, buildKanjiBankName, kanjiBankCount, kanjiDataLoaded);
- await loadBank(summary, buildKanjiMetaBankName, kanjiMetaBankCount, kanjiMetaDataLoaded);
- await loadBank(summary, buildTagBankName, tagBankCount, tagDataLoaded);
+ const dbTransaction = this.db.transaction([tableName], 'readonly');
+ const dbTerms = dbTransaction.objectStore(tableName);
+ const dbIndex = dbTerms.index(indexName);
+
+ for (let i = 0; i < indexValueList.length; ++i) {
+ const only = IDBKeyRange.only(indexValueList[i]);
+ promises.push(Database._getAll(dbIndex, only, i, processRow));
+ }
+
+ await Promise.all(promises);
- return summary;
+ return results;
}
- static createTerm(row, index) {
+ static _createTerm(row, index) {
return {
index,
expression: row.expression,
@@ -619,7 +613,7 @@ class Database {
};
}
- static createKanji(row, index) {
+ static _createKanji(row, index) {
return {
index,
character: row.character,
@@ -632,20 +626,20 @@ class Database {
};
}
- static createTermMeta({expression, mode, data, dictionary}, index) {
+ static _createTermMeta({expression, mode, data, dictionary}, index) {
return {expression, mode, data, dictionary, index};
}
- static createKanjiMeta({character, mode, data, dictionary}, index) {
+ static _createKanjiMeta({character, mode, data, dictionary}, index) {
return {character, mode, data, dictionary, index};
}
- static getAll(dbIndex, query, context, processRow) {
- const fn = typeof dbIndex.getAll === 'function' ? Database.getAllFast : Database.getAllUsingCursor;
+ static _getAll(dbIndex, query, context, processRow) {
+ const fn = typeof dbIndex.getAll === 'function' ? Database._getAllFast : Database._getAllUsingCursor;
return fn(dbIndex, query, context, processRow);
}
- static getAllFast(dbIndex, query, context, processRow) {
+ static _getAllFast(dbIndex, query, context, processRow) {
return new Promise((resolve, reject) => {
const request = dbIndex.getAll(query);
request.onerror = (e) => reject(e);
@@ -658,7 +652,7 @@ class Database {
});
}
- static getAllUsingCursor(dbIndex, query, context, processRow) {
+ static _getAllUsingCursor(dbIndex, query, context, processRow) {
return new Promise((resolve, reject) => {
const request = dbIndex.openCursor(query, 'next');
request.onerror = (e) => reject(e);
@@ -674,18 +668,18 @@ class Database {
});
}
- static getCounts(targets, query) {
+ static _getCounts(targets, query) {
const countPromises = [];
const counts = {};
for (const [objectStoreName, index] of targets) {
const n = objectStoreName;
- const countPromise = Database.getCount(index, query).then((count) => counts[n] = count);
+ const countPromise = Database._getCount(index, query).then((count) => counts[n] = count);
countPromises.push(countPromise);
}
return Promise.all(countPromises).then(() => counts);
}
- static getCount(dbIndex, query) {
+ static _getCount(dbIndex, query) {
return new Promise((resolve, reject) => {
const request = dbIndex.count(query);
request.onerror = (e) => reject(e);
@@ -693,12 +687,12 @@ class Database {
});
}
- static getAllKeys(dbIndex, query) {
- const fn = typeof dbIndex.getAllKeys === 'function' ? Database.getAllKeysFast : Database.getAllKeysUsingCursor;
+ static _getAllKeys(dbIndex, query) {
+ const fn = typeof dbIndex.getAllKeys === 'function' ? Database._getAllKeysFast : Database._getAllKeysUsingCursor;
return fn(dbIndex, query);
}
- static getAllKeysFast(dbIndex, query) {
+ static _getAllKeysFast(dbIndex, query) {
return new Promise((resolve, reject) => {
const request = dbIndex.getAllKeys(query);
request.onerror = (e) => reject(e);
@@ -706,7 +700,7 @@ class Database {
});
}
- static getAllKeysUsingCursor(dbIndex, query) {
+ static _getAllKeysUsingCursor(dbIndex, query) {
return new Promise((resolve, reject) => {
const primaryKeys = [];
const request = dbIndex.openKeyCursor(query, 'next');
@@ -723,9 +717,9 @@ class Database {
});
}
- static async deleteValues(dbObjectStore, dbIndex, query, onProgress, progressData, progressRate) {
+ static async _deleteValues(dbObjectStore, dbIndex, query, onProgress, progressData, progressRate) {
const hasProgress = (typeof onProgress === 'function');
- const count = await Database.getCount(dbIndex, query);
+ const count = await Database._getCount(dbIndex, query);
++progressData.storesProcesed;
progressData.count += count;
if (hasProgress) {
@@ -744,16 +738,16 @@ class Database {
);
const promises = [];
- const primaryKeys = await Database.getAllKeys(dbIndex, query);
+ const primaryKeys = await Database._getAllKeys(dbIndex, query);
for (const key of primaryKeys) {
- const promise = Database.deleteValue(dbObjectStore, key).then(onValueDeleted);
+ const promise = Database._deleteValue(dbObjectStore, key).then(onValueDeleted);
promises.push(promise);
}
await Promise.all(promises);
}
- static deleteValue(dbObjectStore, key) {
+ static _deleteValue(dbObjectStore, key) {
return new Promise((resolve, reject) => {
const request = dbObjectStore.delete(key);
request.onerror = (e) => reject(e);
@@ -761,7 +755,7 @@ class Database {
});
}
- static bulkAdd(objectStore, items, start, count) {
+ static _bulkAdd(objectStore, items, start, count) {
return new Promise((resolve, reject) => {
if (start + count > items.length) {
count = items.length - start;
@@ -789,7 +783,7 @@ class Database {
});
}
- static open(name, version, onUpgradeNeeded) {
+ static _open(name, version, onUpgradeNeeded) {
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(name, version * 10);
@@ -807,7 +801,7 @@ class Database {
});
}
- static upgrade(db, transaction, oldVersion, upgrades) {
+ static _upgrade(db, transaction, oldVersion, upgrades) {
for (const {version, stores} of upgrades) {
if (oldVersion >= version) { continue; }
@@ -815,15 +809,15 @@ class Database {
for (const objectStoreName of objectStoreNames) {
const {primaryKey, indices} = stores[objectStoreName];
- const objectStoreNames = transaction.objectStoreNames || db.objectStoreNames;
+ const objectStoreNames2 = transaction.objectStoreNames || db.objectStoreNames;
const objectStore = (
- Database.listContains(objectStoreNames, objectStoreName) ?
+ Database._listContains(objectStoreNames2, objectStoreName) ?
transaction.objectStore(objectStoreName) :
db.createObjectStore(objectStoreName, primaryKey)
);
for (const indexName of indices) {
- if (Database.listContains(objectStore.indexNames, indexName)) { continue; }
+ if (Database._listContains(objectStore.indexNames, indexName)) { continue; }
objectStore.createIndex(indexName, indexName, {});
}
@@ -831,7 +825,7 @@ class Database {
}
}
- static deleteDatabase(dbName) {
+ static _deleteDatabase(dbName) {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(dbName);
request.onerror = (e) => reject(e);
@@ -839,7 +833,7 @@ class Database {
});
}
- static listContains(list, value) {
+ static _listContains(list, value) {
for (let i = 0, ii = list.length; i < ii; ++i) {
if (list[i] === value) { return true; }
}
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
index 33b2a8b3..e2ced965 100644
--- a/ext/bg/js/deinflector.js
+++ b/ext/bg/js/deinflector.js
@@ -76,17 +76,19 @@ class Deinflector {
const ruleTypes = Deinflector.ruleTypes;
let value = 0;
for (const rule of rules) {
- value |= ruleTypes[rule];
+ const ruleBits = ruleTypes.get(rule);
+ if (typeof ruleBits === 'undefined') { continue; }
+ value |= ruleBits;
}
return value;
}
}
-Deinflector.ruleTypes = {
- 'v1': 0b0000001, // Verb ichidan
- 'v5': 0b0000010, // Verb godan
- 'vs': 0b0000100, // Verb suru
- 'vk': 0b0001000, // Verb kuru
- 'adj-i': 0b0010000, // Adjective i
- 'iru': 0b0100000 // Intermediate -iru endings for progressive or perfect tense
-};
+Deinflector.ruleTypes = new Map([
+ ['v1', 0b0000001], // Verb ichidan
+ ['v5', 0b0000010], // Verb godan
+ ['vs', 0b0000100], // Verb suru
+ ['vk', 0b0001000], // Verb kuru
+ ['adj-i', 0b0010000], // Adjective i
+ ['iru', 0b0100000] // Intermediate -iru endings for progressive or perfect tense
+]);
diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js
index 67128725..f5c5b21b 100644
--- a/ext/bg/js/dictionary.js
+++ b/ext/bg/js/dictionary.js
@@ -16,17 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiTemplateRender*/
function dictEnabledSet(options) {
- const dictionaries = {};
- for (const title in options.dictionaries) {
- const dictionary = options.dictionaries[title];
- if (dictionary.enabled) {
- dictionaries[title] = dictionary;
- }
+ const enabledDictionaryMap = new Map();
+ const optionsDictionaries = options.dictionaries;
+ for (const title in optionsDictionaries) {
+ if (!hasOwn(optionsDictionaries, title)) { continue; }
+ const dictionary = optionsDictionaries[title];
+ if (!dictionary.enabled) { continue; }
+ enabledDictionaryMap.set(title, {
+ priority: dictionary.priority || 0,
+ allowSecondarySearches: !!dictionary.allowSecondarySearches
+ });
}
-
- return dictionaries;
+ return enabledDictionaryMap;
}
function dictConfigured(options) {
@@ -39,28 +43,15 @@ function dictConfigured(options) {
return false;
}
-function dictRowsSort(rows, options) {
- return rows.sort((ra, rb) => {
- const pa = (options.dictionaries[ra.title] || {}).priority || 0;
- const pb = (options.dictionaries[rb.title] || {}).priority || 0;
- if (pa > pb) {
- return -1;
- } else if (pa < pb) {
- return 1;
- } else {
- return 0;
- }
- });
-}
-
function dictTermsSort(definitions, dictionaries=null) {
return definitions.sort((v1, v2) => {
let i;
if (dictionaries !== null) {
- i = (
- ((dictionaries[v2.dictionary] || {}).priority || 0) -
- ((dictionaries[v1.dictionary] || {}).priority || 0)
- );
+ const dictionaryInfo1 = dictionaries.get(v1.dictionary);
+ const dictionaryInfo2 = dictionaries.get(v2.dictionary);
+ const priority1 = typeof dictionaryInfo1 !== 'undefined' ? dictionaryInfo1.priority : 0;
+ const priority2 = typeof dictionaryInfo2 !== 'undefined' ? dictionaryInfo2.priority : 0;
+ i = priority2 - priority1;
if (i !== 0) { return i; }
}
@@ -78,20 +69,16 @@ function dictTermsSort(definitions, dictionaries=null) {
}
function dictTermsUndupe(definitions) {
- const definitionGroups = {};
+ const definitionGroups = new Map();
for (const definition of definitions) {
- const definitionExisting = definitionGroups[definition.id];
- if (!hasOwn(definitionGroups, definition.id) || definition.expression.length > definitionExisting.expression.length) {
- definitionGroups[definition.id] = definition;
+ const id = definition.id;
+ const definitionExisting = definitionGroups.get(id);
+ if (typeof definitionExisting === 'undefined' || definition.expression.length > definitionExisting.expression.length) {
+ definitionGroups.set(id, definition);
}
}
- const definitionsUnique = [];
- for (const key in definitionGroups) {
- definitionsUnique.push(definitionGroups[key]);
- }
-
- return definitionsUnique;
+ return [...definitionGroups.values()];
}
function dictTermsCompressTags(definitions) {
@@ -122,35 +109,35 @@ function dictTermsCompressTags(definitions) {
}
function dictTermsGroup(definitions, dictionaries) {
- const groups = {};
+ const groups = new Map();
for (const definition of definitions) {
- const key = [definition.source, definition.expression];
- key.push(...definition.reasons);
+ const key = [definition.source, definition.expression, ...definition.reasons];
if (definition.reading) {
key.push(definition.reading);
}
const keyString = key.toString();
- if (hasOwn(groups, keyString)) {
- groups[keyString].push(definition);
- } else {
- groups[keyString] = [definition];
+ let groupDefinitions = groups.get(keyString);
+ if (typeof groupDefinitions === 'undefined') {
+ groupDefinitions = [];
+ groups.set(keyString, groupDefinitions);
}
+
+ groupDefinitions.push(definition);
}
const results = [];
- for (const key in groups) {
- const groupDefs = groups[key];
- const firstDef = groupDefs[0];
- dictTermsSort(groupDefs, dictionaries);
+ for (const groupDefinitions of groups.values()) {
+ const firstDef = groupDefinitions[0];
+ dictTermsSort(groupDefinitions, dictionaries);
results.push({
- definitions: groupDefs,
+ definitions: groupDefinitions,
expression: firstDef.expression,
reading: firstDef.reading,
furiganaSegments: firstDef.furiganaSegments,
reasons: firstDef.reasons,
termTags: firstDef.termTags,
- score: groupDefs.reduce((p, v) => v.score > p ? v.score : p, Number.MIN_SAFE_INTEGER),
+ score: groupDefinitions.reduce((p, v) => v.score > p ? v.score : p, Number.MIN_SAFE_INTEGER),
source: firstDef.source
});
}
@@ -158,14 +145,41 @@ 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 definitionsBySequence = {'-1': []};
+ const sequencedDefinitions = new Map();
+ const nonSequencedDefinitions = [];
for (const definition of definitions) {
- if (mainDictionary === definition.dictionary && definition.sequence >= 0) {
- if (!definitionsBySequence[definition.sequence]) {
- definitionsBySequence[definition.sequence] = {
+ const sequence = definition.sequence;
+ if (mainDictionary === definition.dictionary && sequence >= 0) {
+ let sequencedDefinition = sequencedDefinitions.get(sequence);
+ if (typeof sequencedDefinition === 'undefined') {
+ sequencedDefinition = {
reasons: definition.reasons,
- score: Number.MIN_SAFE_INTEGER,
+ score: definition.score,
expression: new Set(),
reading: new Set(),
expressions: new Map(),
@@ -173,100 +187,115 @@ function dictTermsMergeBySequence(definitions, mainDictionary) {
dictionary: definition.dictionary,
definitions: []
};
+ sequencedDefinitions.set(sequence, sequencedDefinition);
+ } else {
+ sequencedDefinition.score = Math.max(sequencedDefinition.score, definition.score);
}
- const score = Math.max(definitionsBySequence[definition.sequence].score, definition.score);
- definitionsBySequence[definition.sequence].score = score;
} else {
- definitionsBySequence['-1'].push(definition);
+ nonSequencedDefinitions.push(definition);
}
}
- return definitionsBySequence;
+ return [sequencedDefinitions, nonSequencedDefinitions];
}
-function dictTermsMergeByGloss(result, definitions, appendTo, mergedIndices) {
- const definitionsByGloss = appendTo || {};
- for (const [index, definition] of definitions.entries()) {
- if (appendTo) {
- let match = false;
- for (const expression of result.expressions.keys()) {
- if (definition.expression === expression) {
- for (const reading of result.expressions.get(expression).keys()) {
- if (definition.reading === reading) {
- match = true;
- break;
- }
- }
- }
- if (match) {
- break;
- }
- }
+function dictTermsMergeByGloss(result, definitions, appendTo=null, mergedIndices=null) {
+ const definitionsByGloss = appendTo !== null ? appendTo : new Map();
- if (!match) {
- continue;
- } else if (mergedIndices) {
+ const resultExpressionsMap = result.expressions;
+ const resultExpressionSet = result.expression;
+ const resultReadingSet = result.reading;
+ const resultSource = result.source;
+
+ for (const [index, definition] of definitions.entries()) {
+ const {expression, reading} = definition;
+
+ if (mergedIndices !== null) {
+ const expressionMap = resultExpressionsMap.get(expression);
+ if (
+ typeof expressionMap !== 'undefined' &&
+ typeof expressionMap.get(reading) !== 'undefined'
+ ) {
mergedIndices.add(index);
+ } else {
+ continue;
}
}
const gloss = JSON.stringify(definition.glossary.concat(definition.dictionary));
- if (!definitionsByGloss[gloss]) {
- definitionsByGloss[gloss] = {
+ let glossDefinition = definitionsByGloss.get(gloss);
+ if (typeof glossDefinition === 'undefined') {
+ glossDefinition = {
expression: new Set(),
reading: new Set(),
definitionTags: [],
glossary: definition.glossary,
- source: result.source,
+ source: resultSource,
reasons: [],
score: definition.score,
id: definition.id,
dictionary: definition.dictionary
};
+ definitionsByGloss.set(gloss, glossDefinition);
}
- definitionsByGloss[gloss].expression.add(definition.expression);
- definitionsByGloss[gloss].reading.add(definition.reading);
+ glossDefinition.expression.add(expression);
+ glossDefinition.reading.add(reading);
- result.expression.add(definition.expression);
- result.reading.add(definition.reading);
+ resultExpressionSet.add(expression);
+ resultReadingSet.add(reading);
for (const tag of definition.definitionTags) {
- if (!definitionsByGloss[gloss].definitionTags.find((existingTag) => existingTag.name === tag.name)) {
- definitionsByGloss[gloss].definitionTags.push(tag);
+ if (!glossDefinition.definitionTags.find((existingTag) => existingTag.name === tag.name)) {
+ glossDefinition.definitionTags.push(tag);
}
}
- if (!appendTo) {
- // result->expressions[ Expression1[ Reading1[ Tag1, Tag2 ] ], Expression2, ... ]
- if (!result.expressions.has(definition.expression)) {
- result.expressions.set(definition.expression, new Map());
+ if (appendTo === null) {
+ /*
+ Data layout:
+ resultExpressionsMap = new Map([
+ [expression, new Map([
+ [reading, new Map([
+ [tagName, tagInfo],
+ ...
+ ])],
+ ...
+ ])],
+ ...
+ ]);
+ */
+ let readingMap = resultExpressionsMap.get(expression);
+ if (typeof readingMap === 'undefined') {
+ readingMap = new Map();
+ resultExpressionsMap.set(expression, readingMap);
}
- if (!result.expressions.get(definition.expression).has(definition.reading)) {
- result.expressions.get(definition.expression).set(definition.reading, []);
+
+ let termTagsMap = readingMap.get(reading);
+ if (typeof termTagsMap === 'undefined') {
+ termTagsMap = new Map();
+ readingMap.set(reading, termTagsMap);
}
for (const tag of definition.termTags) {
- if (!result.expressions.get(definition.expression).get(definition.reading).find((existingTag) => existingTag.name === tag.name)) {
- result.expressions.get(definition.expression).get(definition.reading).push(tag);
+ if (!termTagsMap.has(tag.name)) {
+ termTagsMap.set(tag.name, tag);
}
}
}
}
- for (const gloss in definitionsByGloss) {
- const definition = definitionsByGloss[gloss];
- definition.only = [];
- if (!utilSetEqual(definition.expression, result.expression)) {
- for (const expression of utilSetIntersection(definition.expression, result.expression)) {
- definition.only.push(expression);
- }
+ for (const definition of definitionsByGloss.values()) {
+ const only = [];
+ const expressionSet = definition.expression;
+ const readingSet = definition.reading;
+ if (!dictAreSetsEqual(expressionSet, resultExpressionSet)) {
+ only.push(...dictGetSetIntersection(expressionSet, resultExpressionSet));
}
- if (!utilSetEqual(definition.reading, result.reading)) {
- for (const reading of utilSetIntersection(definition.reading, result.reading)) {
- definition.only.push(reading);
- }
+ if (!dictAreSetsEqual(readingSet, resultReadingSet)) {
+ only.push(...dictGetSetIntersection(readingSet, resultReadingSet));
}
+ definition.only = only;
}
return definitionsByGloss;
@@ -330,7 +359,7 @@ async function dictFieldFormat(field, definition, mode, options, templates, exce
}
data.marker = marker;
try {
- return await apiTemplateRender(templates, data, true);
+ return await apiTemplateRender(templates, data);
} catch (e) {
if (exceptions) { exceptions.push(e); }
return `{${marker}-render-error}`;
diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js
index 62f89ee4..b1443447 100644
--- a/ext/bg/js/handlebars.js
+++ b/ext/bg/js/handlebars.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global jpIsCharCodeKanji, jpDistributeFurigana, Handlebars*/
function handlebarsEscape(text) {
return Handlebars.Utils.escapeExpression(text);
@@ -134,11 +135,6 @@ function handlebarsRegisterHelpers() {
}
}
-function handlebarsRenderStatic(name, data) {
- handlebarsRegisterHelpers();
- return Handlebars.templates[name](data).trim();
-}
-
function handlebarsRenderDynamic(template, data) {
handlebarsRegisterHelpers();
const cache = handlebarsRenderDynamic._cache;
diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js
new file mode 100644
index 00000000..abb32da4
--- /dev/null
+++ b/ext/bg/js/japanese.js
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+/*global 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 _jpIsCharCodeInRanges(charCode, ranges) {
+ for (const [min, max] of ranges) {
+ if (charCode >= min && charCode <= max) {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+// Character code testing functions
+
+function jpIsCharCodeKanji(charCode) {
+ return _jpIsCharCodeInRanges(charCode, JP_CJK_RANGES);
+}
+
+function jpIsCharCodeKana(charCode) {
+ return _jpIsCharCodeInRanges(charCode, JP_KANA_RANGES);
+}
+
+function jpIsCharCodeJapanese(charCode) {
+ return _jpIsCharCodeInRanges(charCode, JP_JAPANESE_RANGES);
+}
+
+
+// String testing functions
+
+function jpIsStringEntirelyKana(str) {
+ if (str.length === 0) { return false; }
+ for (let i = 0, ii = str.length; i < ii; ++i) {
+ if (!jpIsCharCodeKana(str.charCodeAt(i))) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function jpIsStringPartiallyJapanese(str) {
+ if (str.length === 0) { return false; }
+ for (let i = 0, ii = str.length; i < ii; ++i) {
+ if (jpIsCharCodeJapanese(str.charCodeAt(i))) {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+// Conversion functions
+
+function jpKatakanaToHiragana(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;
+}
+
+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);
+ } else {
+ if (jpIsStringEntirelyKana(expressionFragment)) {
+ return jpToRomaji(expressionFragment);
+ }
+ }
+ return readingFragment;
+ case 'none':
+ return null;
+ default:
+ return readingFragment;
+ }
+}
+
+function jpDistributeFurigana(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 (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);
+ }
+ }
+ } 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 charCode = c.charCodeAt(0);
+ const modeCurr = jpIsCharCodeKanji(charCode) || charCode === 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;
+ }
+ }
+
+ 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 output;
+}
+
+function jpConvertHalfWidthKanaToFullWidth(text, sourceMapping) {
+ let result = '';
+ const ii = text.length;
+ const hasSourceMapping = Array.isArray(sourceMapping);
+
+ for (let i = 0; i < ii; ++i) {
+ const c = text[i];
+ const mapping = JP_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 (hasSourceMapping && index > 0) {
+ index = result.length;
+ const v = sourceMapping.splice(index + 1, 1)[0];
+ sourceMapping[index] += v;
+ }
+ result += c2;
+ }
+
+ return result;
+}
+
+function jpConvertNumericTofullWidth(text) {
+ let result = '';
+ for (let i = 0, ii = text.length; i < ii; ++i) {
+ let c = text.charCodeAt(i);
+ if (c >= 0x30 && c <= 0x39) { // ['0', '9']
+ c += 0xff10 - 0x30; // 0xff10 = '0' full width
+ result += String.fromCharCode(c);
+ } else {
+ result += text[i];
+ }
+ }
+ return result;
+}
+
+function jpConvertAlphabeticToKana(text, sourceMapping) {
+ let part = '';
+ let result = '';
+ const ii = text.length;
+
+ if (sourceMapping.length === ii) {
+ sourceMapping.length = ii;
+ sourceMapping.fill(1);
+ }
+
+ for (let i = 0; i < ii; ++i) {
+ // Note: 0x61 is the character code for 'a'
+ let c = text.charCodeAt(i);
+ 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 = '';
+ }
+ result += text[i];
+ continue;
+ }
+ part += String.fromCharCode(c);
+ }
+
+ if (part.length > 0) {
+ result += jpToHiragana(part, sourceMapping, result.length);
+ }
+ 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;
+ }
+
+ // 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;
+
+ // Empty elements
+ const additions = resultPosNext - resultPos - 1;
+ for (let j = 0; j < additions; ++j) {
+ sourceMapping.splice(sourceMappingStart, 0, 0);
+ ++sourceMappingStart;
+ }
+
+ i = iNext;
+ resultPos = resultPosNext;
+ }
+ }
+
+ return result;
+}
diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js
index 5d596a8b..58f804fd 100644
--- a/ext/bg/js/json-schema.js
+++ b/ext/bg/js/json-schema.js
@@ -64,7 +64,7 @@ class JsonSchemaProxyHandler {
}
}
- const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property);
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property, target);
if (propertySchema === null) {
return;
}
@@ -86,17 +86,14 @@ class JsonSchemaProxyHandler {
}
}
- const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property);
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property, target);
if (propertySchema === null) {
throw new Error(`Property ${property} not supported`);
}
value = JsonSchema.isolate(value);
- const error = JsonSchemaProxyHandler.validate(value, propertySchema);
- if (error !== null) {
- throw new Error(`Invalid value: ${error}`);
- }
+ JsonSchemaProxyHandler.validate(value, propertySchema, new JsonSchemaTraversalInfo(value, propertySchema));
target[property] = value;
return true;
@@ -122,151 +119,329 @@ class JsonSchemaProxyHandler {
throw new Error('construct not supported');
}
- static getPropertySchema(schema, property) {
- const type = schema.type;
- if (Array.isArray(type)) {
- throw new Error(`Ambiguous property type for ${property}`);
- }
+ static getPropertySchema(schema, property, value, path=null) {
+ const type = JsonSchemaProxyHandler.getSchemaOrValueType(schema, value);
switch (type) {
case 'object':
{
const properties = schema.properties;
- if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
- if (Object.prototype.hasOwnProperty.call(properties, property)) {
- return properties[property];
+ if (JsonSchemaProxyHandler.isObject(properties)) {
+ const propertySchema = properties[property];
+ if (JsonSchemaProxyHandler.isObject(propertySchema)) {
+ if (path !== null) { path.push(['properties', properties], [property, propertySchema]); }
+ return propertySchema;
}
}
const additionalProperties = schema.additionalProperties;
- return (additionalProperties !== null && typeof additionalProperties === 'object' && !Array.isArray(additionalProperties)) ? additionalProperties : null;
+ if (additionalProperties === false) {
+ return null;
+ } else if (JsonSchemaProxyHandler.isObject(additionalProperties)) {
+ if (path !== null) { path.push(['additionalProperties', additionalProperties]); }
+ return additionalProperties;
+ } else {
+ const result = JsonSchemaProxyHandler._unconstrainedSchema;
+ if (path !== null) { path.push([null, result]); }
+ return result;
+ }
}
case 'array':
{
const items = schema.items;
- return (items !== null && typeof items === 'object' && !Array.isArray(items)) ? items : null;
+ if (JsonSchemaProxyHandler.isObject(items)) {
+ return items;
+ }
+ if (Array.isArray(items)) {
+ if (property >= 0 && property < items.length) {
+ const propertySchema = items[property];
+ if (JsonSchemaProxyHandler.isObject(propertySchema)) {
+ if (path !== null) { path.push(['items', items], [property, propertySchema]); }
+ return propertySchema;
+ }
+ }
+ }
+
+ const additionalItems = schema.additionalItems;
+ if (additionalItems === false) {
+ return null;
+ } else if (JsonSchemaProxyHandler.isObject(additionalItems)) {
+ if (path !== null) { path.push(['additionalItems', additionalItems]); }
+ return additionalItems;
+ } else {
+ const result = JsonSchemaProxyHandler._unconstrainedSchema;
+ if (path !== null) { path.push([null, result]); }
+ return result;
+ }
}
default:
return null;
}
}
- static validate(value, schema) {
+ static getSchemaOrValueType(schema, value) {
+ const type = schema.type;
+
+ if (Array.isArray(type)) {
+ if (typeof value !== 'undefined') {
+ const valueType = JsonSchemaProxyHandler.getValueType(value);
+ if (type.indexOf(valueType) >= 0) {
+ return valueType;
+ }
+ }
+ return null;
+ }
+
+ if (typeof type === 'undefined') {
+ if (typeof value !== 'undefined') {
+ return JsonSchemaProxyHandler.getValueType(value);
+ }
+ return null;
+ }
+
+ return type;
+ }
+
+ static validate(value, schema, info) {
+ JsonSchemaProxyHandler.validateSingleSchema(value, schema, info);
+ JsonSchemaProxyHandler.validateConditional(value, schema, info);
+ JsonSchemaProxyHandler.validateAllOf(value, schema, info);
+ JsonSchemaProxyHandler.validateAnyOf(value, schema, info);
+ JsonSchemaProxyHandler.validateOneOf(value, schema, info);
+ JsonSchemaProxyHandler.validateNoneOf(value, schema, info);
+ }
+
+ static validateConditional(value, schema, info) {
+ const ifSchema = schema.if;
+ if (!JsonSchemaProxyHandler.isObject(ifSchema)) { return; }
+
+ let okay = true;
+ info.schemaPush('if', ifSchema);
+ try {
+ JsonSchemaProxyHandler.validate(value, ifSchema, info);
+ } catch (e) {
+ okay = false;
+ }
+ info.schemaPop();
+
+ const nextSchema = okay ? schema.then : schema.else;
+ if (JsonSchemaProxyHandler.isObject(nextSchema)) {
+ info.schemaPush(okay ? 'then' : 'else', nextSchema);
+ JsonSchemaProxyHandler.validate(value, nextSchema, info);
+ info.schemaPop();
+ }
+ }
+
+ static validateAllOf(value, schema, info) {
+ const subSchemas = schema.allOf;
+ if (!Array.isArray(subSchemas)) { return; }
+
+ info.schemaPush('allOf', subSchemas);
+ for (let i = 0; i < subSchemas.length; ++i) {
+ const subSchema = subSchemas[i];
+ info.schemaPush(i, subSchema);
+ JsonSchemaProxyHandler.validate(value, subSchema, info);
+ info.schemaPop();
+ }
+ info.schemaPop();
+ }
+
+ static validateAnyOf(value, schema, info) {
+ const subSchemas = schema.anyOf;
+ if (!Array.isArray(subSchemas)) { return; }
+
+ info.schemaPush('anyOf', subSchemas);
+ for (let i = 0; i < subSchemas.length; ++i) {
+ const subSchema = subSchemas[i];
+ info.schemaPush(i, subSchema);
+ try {
+ JsonSchemaProxyHandler.validate(value, subSchema, info);
+ return;
+ } catch (e) {
+ // NOP
+ }
+ info.schemaPop();
+ }
+
+ throw new JsonSchemaValidationError('0 anyOf schemas matched', value, schema, info);
+ // info.schemaPop(); // Unreachable
+ }
+
+ static validateOneOf(value, schema, info) {
+ const subSchemas = schema.oneOf;
+ if (!Array.isArray(subSchemas)) { return; }
+
+ info.schemaPush('oneOf', subSchemas);
+ let count = 0;
+ for (let i = 0; i < subSchemas.length; ++i) {
+ const subSchema = subSchemas[i];
+ info.schemaPush(i, subSchema);
+ try {
+ JsonSchemaProxyHandler.validate(value, subSchema, info);
+ ++count;
+ } catch (e) {
+ // NOP
+ }
+ info.schemaPop();
+ }
+
+ if (count !== 1) {
+ throw new JsonSchemaValidationError(`${count} oneOf schemas matched`, value, schema, info);
+ }
+
+ info.schemaPop();
+ }
+
+ static validateNoneOf(value, schema, info) {
+ const subSchemas = schema.not;
+ if (!Array.isArray(subSchemas)) { return; }
+
+ info.schemaPush('not', subSchemas);
+ for (let i = 0; i < subSchemas.length; ++i) {
+ const subSchema = subSchemas[i];
+ info.schemaPush(i, subSchema);
+ try {
+ JsonSchemaProxyHandler.validate(value, subSchema, info);
+ } catch (e) {
+ info.schemaPop();
+ continue;
+ }
+ throw new JsonSchemaValidationError(`not[${i}] schema matched`, value, schema, info);
+ }
+ info.schemaPop();
+ }
+
+ static validateSingleSchema(value, schema, info) {
const type = JsonSchemaProxyHandler.getValueType(value);
const schemaType = schema.type;
if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) {
- return `Value type ${type} does not match schema type ${schemaType}`;
+ throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema, info);
}
const schemaEnum = schema.enum;
if (Array.isArray(schemaEnum) && !JsonSchemaProxyHandler.valuesAreEqualAny(value, schemaEnum)) {
- return 'Invalid enum value';
+ throw new JsonSchemaValidationError('Invalid enum value', value, schema, info);
}
switch (type) {
case 'number':
- return JsonSchemaProxyHandler.validateNumber(value, schema);
+ JsonSchemaProxyHandler.validateNumber(value, schema, info);
+ break;
case 'string':
- return JsonSchemaProxyHandler.validateString(value, schema);
+ JsonSchemaProxyHandler.validateString(value, schema, info);
+ break;
case 'array':
- return JsonSchemaProxyHandler.validateArray(value, schema);
+ JsonSchemaProxyHandler.validateArray(value, schema, info);
+ break;
case 'object':
- return JsonSchemaProxyHandler.validateObject(value, schema);
- default:
- return null;
+ JsonSchemaProxyHandler.validateObject(value, schema, info);
+ break;
}
}
- static validateNumber(value, schema) {
+ static validateNumber(value, schema, info) {
const multipleOf = schema.multipleOf;
if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {
- return `Number is not a multiple of ${multipleOf}`;
+ throw new JsonSchemaValidationError(`Number is not a multiple of ${multipleOf}`, value, schema, info);
}
const minimum = schema.minimum;
if (typeof minimum === 'number' && value < minimum) {
- return `Number is less than ${minimum}`;
+ throw new JsonSchemaValidationError(`Number is less than ${minimum}`, value, schema, info);
}
const exclusiveMinimum = schema.exclusiveMinimum;
if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) {
- return `Number is less than or equal to ${exclusiveMinimum}`;
+ throw new JsonSchemaValidationError(`Number is less than or equal to ${exclusiveMinimum}`, value, schema, info);
}
const maximum = schema.maximum;
if (typeof maximum === 'number' && value > maximum) {
- return `Number is greater than ${maximum}`;
+ throw new JsonSchemaValidationError(`Number is greater than ${maximum}`, value, schema, info);
}
const exclusiveMaximum = schema.exclusiveMaximum;
if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) {
- return `Number is greater than or equal to ${exclusiveMaximum}`;
+ throw new JsonSchemaValidationError(`Number is greater than or equal to ${exclusiveMaximum}`, value, schema, info);
}
-
- return null;
}
- static validateString(value, schema) {
+ static validateString(value, schema, info) {
const minLength = schema.minLength;
if (typeof minLength === 'number' && value.length < minLength) {
- return 'String length too short';
+ throw new JsonSchemaValidationError('String length too short', value, schema, info);
}
- const maxLength = schema.minLength;
+ const maxLength = schema.maxLength;
if (typeof maxLength === 'number' && value.length > maxLength) {
- return 'String length too long';
+ throw new JsonSchemaValidationError('String length too long', value, schema, info);
}
-
- return null;
}
- static validateArray(value, schema) {
+ static validateArray(value, schema, info) {
const minItems = schema.minItems;
if (typeof minItems === 'number' && value.length < minItems) {
- return 'Array length too short';
+ throw new JsonSchemaValidationError('Array length too short', value, schema, info);
}
const maxItems = schema.maxItems;
if (typeof maxItems === 'number' && value.length > maxItems) {
- return 'Array length too long';
+ throw new JsonSchemaValidationError('Array length too long', value, schema, info);
}
- return null;
+ for (let i = 0, ii = value.length; i < ii; ++i) {
+ const schemaPath = [];
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value, schemaPath);
+ if (propertySchema === null) {
+ throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema, info);
+ }
+
+ const propertyValue = value[i];
+
+ for (const [p, s] of schemaPath) { info.schemaPush(p, s); }
+ info.valuePush(i, propertyValue);
+ JsonSchemaProxyHandler.validate(propertyValue, propertySchema, info);
+ info.valuePop();
+ for (let j = 0, jj = schemaPath.length; j < jj; ++j) { info.schemaPop(); }
+ }
}
- static validateObject(value, schema) {
+ static validateObject(value, schema, info) {
const properties = new Set(Object.getOwnPropertyNames(value));
const required = schema.required;
if (Array.isArray(required)) {
for (const property of required) {
if (!properties.has(property)) {
- return `Missing property ${property}`;
+ throw new JsonSchemaValidationError(`Missing property ${property}`, value, schema, info);
}
}
}
const minProperties = schema.minProperties;
if (typeof minProperties === 'number' && properties.length < minProperties) {
- return 'Not enough object properties';
+ throw new JsonSchemaValidationError('Not enough object properties', value, schema, info);
}
const maxProperties = schema.maxProperties;
if (typeof maxProperties === 'number' && properties.length > maxProperties) {
- return 'Too many object properties';
+ throw new JsonSchemaValidationError('Too many object properties', value, schema, info);
}
for (const property of properties) {
- const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property);
+ const schemaPath = [];
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value, schemaPath);
if (propertySchema === null) {
- return `No schema found for ${property}`;
- }
- const error = JsonSchemaProxyHandler.validate(value[property], propertySchema);
- if (error !== null) {
- return error;
+ throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema, info);
}
- }
- return null;
+ const propertyValue = value[property];
+
+ for (const [p, s] of schemaPath) { info.schemaPush(p, s); }
+ info.valuePush(property, propertyValue);
+ JsonSchemaProxyHandler.validate(propertyValue, propertySchema, info);
+ info.valuePop();
+ for (let i = 0; i < schemaPath.length; ++i) { info.schemaPop(); }
+ }
}
static isValueTypeAny(value, type, schemaTypes) {
@@ -372,14 +547,14 @@ class JsonSchemaProxyHandler {
for (const property of required) {
properties.delete(property);
- const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property);
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value);
if (propertySchema === null) { continue; }
value[property] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[property]);
}
}
for (const property of properties) {
- const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property);
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value);
if (propertySchema === null) {
Reflect.deleteProperty(value, property);
} else {
@@ -392,13 +567,53 @@ class JsonSchemaProxyHandler {
static populateArrayDefaults(value, schema) {
for (let i = 0, ii = value.length; i < ii; ++i) {
- const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i);
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value);
if (propertySchema === null) { continue; }
value[i] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[i]);
}
return value;
}
+
+ static isObject(value) {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+ }
+}
+
+JsonSchemaProxyHandler._unconstrainedSchema = {};
+
+class JsonSchemaTraversalInfo {
+ constructor(value, schema) {
+ this.valuePath = [];
+ this.schemaPath = [];
+ this.valuePush(null, value);
+ this.schemaPush(null, schema);
+ }
+
+ valuePush(path, value) {
+ this.valuePath.push([path, value]);
+ }
+
+ valuePop() {
+ this.valuePath.pop();
+ }
+
+ schemaPush(path, schema) {
+ this.schemaPath.push([path, schema]);
+ }
+
+ schemaPop() {
+ this.schemaPath.pop();
+ }
+}
+
+class JsonSchemaValidationError extends Error {
+ constructor(message, value, schema, info) {
+ super(message);
+ this.value = value;
+ this.schema = schema;
+ this.info = info;
+ }
}
class JsonSchema {
@@ -406,6 +621,10 @@ class JsonSchema {
return new Proxy(target, new JsonSchemaProxyHandler(schema));
}
+ static validate(value, schema) {
+ return JsonSchemaProxyHandler.validate(value, schema, new JsonSchemaTraversalInfo(value, schema));
+ }
+
static getValidValueOrDefault(schema, value) {
return JsonSchemaProxyHandler.getValidValueOrDefault(schema, value);
}
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index d93862bf..f9db99a2 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global utilStringHashCode*/
/*
* Generic options functions
@@ -266,6 +267,7 @@ function profileOptionsCreateDefaults() {
return {
general: {
enable: true,
+ enableClipboardPopups: false,
resultOutputMode: 'group',
debugInfo: false,
maxResults: 32,
@@ -316,7 +318,8 @@ function profileOptionsCreateDefaults() {
popupNestingMaxDepth: 0,
enablePopupSearch: false,
enableOnPopupExpressions: false,
- enableOnSearchPage: true
+ enableOnSearchPage: true,
+ enableSearchTags: false
},
translation: {
diff --git a/ext/bg/js/page-exit-prevention.js b/ext/bg/js/page-exit-prevention.js
index 3a320db3..be06c495 100644
--- a/ext/bg/js/page-exit-prevention.js
+++ b/ext/bg/js/page-exit-prevention.js
@@ -18,43 +18,43 @@
class PageExitPrevention {
- constructor() {
- }
-
- start() {
- PageExitPrevention._addInstance(this);
- }
-
- end() {
- PageExitPrevention._removeInstance(this);
- }
-
- static _addInstance(instance) {
- const size = PageExitPrevention._instances.size;
- PageExitPrevention._instances.set(instance, true);
- if (size === 0) {
- window.addEventListener('beforeunload', PageExitPrevention._onBeforeUnload);
- }
- }
-
- static _removeInstance(instance) {
- if (
- PageExitPrevention._instances.delete(instance) &&
- PageExitPrevention._instances.size === 0
- ) {
- window.removeEventListener('beforeunload', PageExitPrevention._onBeforeUnload);
- }
- }
-
- static _onBeforeUnload(e) {
- if (PageExitPrevention._instances.size === 0) {
- return;
- }
-
- e.preventDefault();
- e.returnValue = '';
- return '';
- }
+ constructor() {
+ }
+
+ start() {
+ PageExitPrevention._addInstance(this);
+ }
+
+ end() {
+ PageExitPrevention._removeInstance(this);
+ }
+
+ static _addInstance(instance) {
+ const size = PageExitPrevention._instances.size;
+ PageExitPrevention._instances.set(instance, true);
+ if (size === 0) {
+ window.addEventListener('beforeunload', PageExitPrevention._onBeforeUnload);
+ }
+ }
+
+ static _removeInstance(instance) {
+ if (
+ PageExitPrevention._instances.delete(instance) &&
+ PageExitPrevention._instances.size === 0
+ ) {
+ window.removeEventListener('beforeunload', PageExitPrevention._onBeforeUnload);
+ }
+ }
+
+ static _onBeforeUnload(e) {
+ if (PageExitPrevention._instances.size === 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.returnValue = '';
+ return '';
+ }
}
PageExitPrevention._instances = new Map();
diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js
index e453ccef..509c4009 100644
--- a/ext/bg/js/search-frontend.js
+++ b/ext/bg/js/search-frontend.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiOptionsGet*/
async function searchFrontendSetup() {
const optionsContext = {
diff --git a/ext/bg/js/search-query-parser-generator.js b/ext/bg/js/search-query-parser-generator.js
new file mode 100644
index 00000000..1ab23a82
--- /dev/null
+++ b/ext/bg/js/search-query-parser-generator.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+/*global apiGetQueryParserTemplatesHtml, TemplateHandler*/
+
+class QueryParserGenerator {
+ constructor() {
+ this._templateHandler = null;
+ }
+
+ async prepare() {
+ const html = await apiGetQueryParserTemplatesHtml();
+ this._templateHandler = new TemplateHandler(html);
+ }
+
+ createParseResult(terms, preview=false) {
+ const fragment = document.createDocumentFragment();
+ for (const term of terms) {
+ const termContainer = this._templateHandler.instantiate(preview ? 'term-preview' : 'term');
+ for (const segment of term) {
+ if (!segment.text.trim()) { continue; }
+ if (!segment.reading || !segment.reading.trim()) {
+ termContainer.appendChild(this.createSegmentText(segment.text));
+ } else {
+ termContainer.appendChild(this.createSegment(segment));
+ }
+ }
+ fragment.appendChild(termContainer);
+ }
+ return fragment;
+ }
+
+ createSegment(segment) {
+ const segmentContainer = this._templateHandler.instantiate('segment');
+ const segmentTextContainer = segmentContainer.querySelector('.query-parser-segment-text');
+ const segmentReadingContainer = segmentContainer.querySelector('.query-parser-segment-reading');
+ segmentTextContainer.appendChild(this.createSegmentText(segment.text));
+ segmentReadingContainer.textContent = segment.reading;
+ return segmentContainer;
+ }
+
+ createSegmentText(text) {
+ const fragment = document.createDocumentFragment();
+ for (const chr of text) {
+ const charContainer = this._templateHandler.instantiate('char');
+ charContainer.textContent = chr;
+ fragment.appendChild(charContainer);
+ }
+ return fragment;
+ }
+
+ createParserSelect(parseResults, selectedParser) {
+ const selectContainer = this._templateHandler.instantiate('select');
+ for (const parseResult of parseResults) {
+ const optionContainer = this._templateHandler.instantiate('select-option');
+ optionContainer.value = parseResult.id;
+ optionContainer.textContent = parseResult.name;
+ optionContainer.defaultSelected = selectedParser === parseResult.id;
+ selectContainer.appendChild(optionContainer);
+ }
+ return selectContainer;
+ }
+}
diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js
index e8e6d11f..0d4aaa50 100644
--- a/ext/bg/js/search-query-parser.js
+++ b/ext/bg/js/search-query-parser.js
@@ -16,17 +16,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiTermsFind, apiOptionsSet, apiTextParse, apiTextParseMecab, TextScanner, QueryParserGenerator*/
class QueryParser extends TextScanner {
constructor(search) {
- super(document.querySelector('#query-parser'), [], [], []);
+ super(document.querySelector('#query-parser-content'), [], [], []);
this.search = search;
this.parseResults = [];
this.selectedParser = null;
- this.queryParser = document.querySelector('#query-parser');
- this.queryParserSelect = document.querySelector('#query-parser-select');
+ this.queryParser = document.querySelector('#query-parser-content');
+ this.queryParserSelect = document.querySelector('#query-parser-select-container');
+
+ this.queryParserGenerator = new QueryParserGenerator();
+ }
+
+ async prepare() {
+ await this.queryParserGenerator.prepare();
}
onError(error) {
@@ -52,7 +59,7 @@ class QueryParser extends TextScanner {
this.search.setContent('terms', {definitions, context: {
focus: false,
- disableHistory: cause === 'mouse' ? true : false,
+ disableHistory: cause === 'mouse',
sentence: {text: searchText, offset: 0},
url: window.location.href
}});
@@ -64,7 +71,7 @@ class QueryParser extends TextScanner {
const selectedParser = e.target.value;
this.selectedParser = selectedParser;
apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext());
- this.renderParseResult(this.getParseResult());
+ this.renderParseResult();
}
getMouseEventListeners() {
@@ -113,13 +120,13 @@ class QueryParser extends TextScanner {
async setText(text) {
this.search.setSpinnerVisible(true);
- await this.setPreview(text);
+ this.setPreview(text);
this.parseResults = await this.parseText(text);
this.refreshSelectedParser();
this.renderParserSelect();
- await this.renderParseResult();
+ this.renderParseResult();
this.search.setSpinnerVisible(false);
}
@@ -146,57 +153,29 @@ class QueryParser extends TextScanner {
return results;
}
- async setPreview(text) {
+ setPreview(text) {
const previewTerms = [];
for (let i = 0, ii = text.length; i < ii; i += 2) {
const tempText = text.substring(i, i + 2);
- previewTerms.push([{text: tempText.split('')}]);
+ previewTerms.push([{text: tempText}]);
}
- this.queryParser.innerHTML = await apiTemplateRender('query-parser.html', {
- terms: previewTerms,
- preview: true
- });
+ this.queryParser.textContent = '';
+ this.queryParser.appendChild(this.queryParserGenerator.createParseResult(previewTerms, true));
}
renderParserSelect() {
- this.queryParserSelect.innerHTML = '';
+ this.queryParserSelect.textContent = '';
if (this.parseResults.length > 1) {
- const select = document.createElement('select');
- select.classList.add('form-control');
- for (const parseResult of this.parseResults) {
- const option = document.createElement('option');
- option.value = parseResult.id;
- option.innerText = parseResult.name;
- option.defaultSelected = this.selectedParser === parseResult.id;
- select.appendChild(option);
- }
+ const select = this.queryParserGenerator.createParserSelect(this.parseResults, this.selectedParser);
select.addEventListener('change', this.onParserChange.bind(this));
this.queryParserSelect.appendChild(select);
}
}
- async renderParseResult() {
+ renderParseResult() {
const parseResult = this.getParseResult();
- if (!parseResult) {
- this.queryParser.innerHTML = '';
- return;
- }
-
- this.queryParser.innerHTML = await apiTemplateRender(
- 'query-parser.html',
- {terms: QueryParser.processParseResultForDisplay(parseResult.parsedText)}
- );
- }
-
- static processParseResultForDisplay(result) {
- return result.map((term) => {
- return term.filter((part) => part.text.trim()).map((part) => {
- return {
- text: part.text.split(''),
- reading: part.reading,
- raw: !part.reading || !part.reading.trim()
- };
- });
- });
+ this.queryParser.textContent = '';
+ if (!parseResult) { return; }
+ this.queryParser.appendChild(this.queryParserGenerator.createParseResult(parseResult.parsedText));
}
}
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index f5c641a8..98e167ad 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiOptionsSet, apiTermsFind, Display, QueryParser, ClipboardMonitor*/
+
class DisplaySearch extends Display {
constructor() {
super(document.querySelector('#spinner'), document.querySelector('#content'));
@@ -36,12 +38,7 @@ class DisplaySearch extends Display {
this.introVisible = true;
this.introAnimationTimer = null;
- this.isFirefox = false;
-
- this.clipboardMonitorTimerId = null;
- this.clipboardMonitorTimerToken = null;
- this.clipboardInterval = 250;
- this.clipboardPreviousText = null;
+ this.clipboardMonitor = new ClipboardMonitor();
}
static create() {
@@ -52,13 +49,17 @@ class DisplaySearch extends Display {
async prepare() {
try {
- await this.initialize();
- this.isFirefox = await DisplaySearch._isFirefox();
+ const superPromise = super.prepare();
+ const queryParserPromise = this.queryParser.prepare();
+ await Promise.all([superPromise, queryParserPromise]);
+
+ const {queryParams: {query='', mode=''}} = parseUrl(window.location.href);
if (this.search !== null) {
this.search.addEventListener('click', (e) => this.onSearch(e), false);
}
if (this.query !== null) {
+ document.documentElement.dataset.searchMode = mode;
this.query.addEventListener('input', () => this.onSearchInput(), false);
if (this.wanakanaEnable !== null) {
@@ -69,34 +70,26 @@ class DisplaySearch extends Display {
this.wanakanaEnable.checked = false;
}
this.wanakanaEnable.addEventListener('change', (e) => {
- const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || '';
+ const {queryParams: {query: query2=''}} = parseUrl(window.location.href);
if (e.target.checked) {
window.wanakana.bind(this.query);
- this.setQuery(window.wanakana.toKana(query));
apiOptionsSet({general: {enableWanakana: true}}, this.getOptionsContext());
} else {
window.wanakana.unbind(this.query);
- this.setQuery(query);
apiOptionsSet({general: {enableWanakana: false}}, this.getOptionsContext());
}
+ this.setQuery(query2);
this.onSearchQueryUpdated(this.query.value, false);
});
}
- const query = DisplaySearch.getSearchQueryFromLocation(window.location.href);
- if (query !== null) {
- if (this.isWanakanaEnabled()) {
- this.setQuery(window.wanakana.toKana(query));
- } else {
- this.setQuery(query);
- }
- this.onSearchQueryUpdated(this.query.value, false);
- }
+ this.setQuery(query);
+ this.onSearchQueryUpdated(this.query.value, false);
}
- if (this.clipboardMonitorEnable !== null) {
+ if (this.clipboardMonitorEnable !== null && mode !== 'popup') {
if (this.options.general.enableClipboardMonitor === true) {
this.clipboardMonitorEnable.checked = true;
- this.startClipboardMonitor();
+ this.clipboardMonitor.start();
} else {
this.clipboardMonitorEnable.checked = false;
}
@@ -106,7 +99,7 @@ class DisplaySearch extends Display {
{permissions: ['clipboardRead']},
(granted) => {
if (granted) {
- this.startClipboardMonitor();
+ this.clipboardMonitor.start();
apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext());
} else {
e.target.checked = false;
@@ -114,16 +107,20 @@ class DisplaySearch extends Display {
}
);
} else {
- this.stopClipboardMonitor();
+ this.clipboardMonitor.stop();
apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext());
}
});
}
+ chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
+
window.addEventListener('popstate', (e) => this.onPopState(e));
+ window.addEventListener('copy', (e) => this.onCopy(e));
+
+ this.clipboardMonitor.onClipboardText = (text) => this.onExternalSearchUpdate(text);
this.updateSearchButton();
- this.initClipboardMonitor();
} catch (e) {
this.onError(e);
}
@@ -159,25 +156,32 @@ class DisplaySearch extends Display {
e.preventDefault();
const query = this.query.value;
+
this.queryParser.setText(query);
- const queryString = query.length > 0 ? `?query=${encodeURIComponent(query)}` : '';
- window.history.pushState(null, '', `${window.location.pathname}${queryString}`);
+
+ const url = new URL(window.location.href);
+ url.searchParams.set('query', query);
+ window.history.pushState(null, '', url.toString());
+
this.onSearchQueryUpdated(query, true);
}
onPopState() {
- const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || '';
- if (this.query !== null) {
- if (this.isWanakanaEnabled()) {
- this.setQuery(window.wanakana.toKana(query));
- } else {
- this.setQuery(query);
- }
- }
-
+ const {queryParams: {query='', mode=''}} = parseUrl(window.location.href);
+ document.documentElement.dataset.searchMode = mode;
+ this.setQuery(query);
this.onSearchQueryUpdated(this.query.value, false);
}
+ onRuntimeMessage({action, params}, sender, callback) {
+ const handler = DisplaySearch._runtimeMessageHandlers.get(action);
+ if (typeof handler !== 'function') { return false; }
+
+ const result = handler(this, params, sender);
+ callback(result);
+ return false;
+ }
+
onKeyDown(e) {
const key = Display.getKeyFromEvent(e);
const ignoreKeys = DisplaySearch.onKeyDownIgnoreKeys;
@@ -202,6 +206,19 @@ class DisplaySearch extends Display {
}
}
+ onCopy() {
+ // ignore copy from search page
+ this.clipboardMonitor.setPreviousText(document.getSelection().toString().trim());
+ }
+
+ onExternalSearchUpdate(text) {
+ this.setQuery(text);
+ const url = new URL(window.location.href);
+ url.searchParams.set('query', text);
+ window.history.pushState(null, '', url.toString());
+ this.onSearchQueryUpdated(this.query.value, true);
+ }
+
async onSearchQueryUpdated(query, animate) {
try {
const details = {};
@@ -241,74 +258,6 @@ class DisplaySearch extends Display {
this.queryParser.setOptions(this.options);
}
- initClipboardMonitor() {
- // ignore copy from search page
- window.addEventListener('copy', () => {
- this.clipboardPreviousText = document.getSelection().toString().trim();
- });
- }
-
- startClipboardMonitor() {
- // The token below is used as a unique identifier to ensure that a new clipboard monitor
- // hasn't been started during the await call. The check below the await this.getClipboardText()
- // call will exit early if the reference has changed.
- const token = {};
- const intervalCallback = async () => {
- this.clipboardMonitorTimerId = null;
-
- let text = await this.getClipboardText();
- if (this.clipboardMonitorTimerToken !== token) { return; }
-
- if (
- typeof text === 'string' &&
- (text = text.trim()).length > 0 &&
- text !== this.clipboardPreviousText
- ) {
- this.clipboardPreviousText = text;
- if (jpIsStringPartiallyJapanese(text)) {
- this.setQuery(this.isWanakanaEnabled() ? window.wanakana.toKana(text) : text);
- window.history.pushState(null, '', `${window.location.pathname}?query=${encodeURIComponent(text)}`);
- this.onSearchQueryUpdated(this.query.value, true);
- }
- }
-
- this.clipboardMonitorTimerId = setTimeout(intervalCallback, this.clipboardInterval);
- };
-
- this.clipboardMonitorTimerToken = token;
-
- intervalCallback();
- }
-
- stopClipboardMonitor() {
- this.clipboardMonitorTimerToken = null;
- if (this.clipboardMonitorTimerId !== null) {
- clearTimeout(this.clipboardMonitorTimerId);
- this.clipboardMonitorTimerId = null;
- }
- }
-
- async getClipboardText() {
- /*
- Notes:
- apiClipboardGet doesn't work on Firefox because document.execCommand('paste')
- results in an empty string on the web extension background page.
- This may be a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
- Therefore, navigator.clipboard.readText() is used on Firefox.
-
- navigator.clipboard.readText() can't be used in Chrome for two reasons:
- * Requires page to be focused, else it rejects with an exception.
- * When the page is focused, Chrome will request clipboard permission, despite already
- being an extension with clipboard permissions. It effectively asks for the
- non-extension permission for clipboard access.
- */
- try {
- return this.isFirefox ? await navigator.clipboard.readText() : await apiClipboardGet();
- } catch (e) {
- return null;
- }
- }
-
isWanakanaEnabled() {
return this.wanakanaEnable !== null && this.wanakanaEnable.checked;
}
@@ -318,8 +267,9 @@ class DisplaySearch extends Display {
}
setQuery(query) {
- this.query.value = query;
- this.queryParser.setText(query);
+ const interpretedQuery = this.isWanakanaEnabled() ? window.wanakana.toKana(query) : query;
+ this.query.value = interpretedQuery;
+ this.queryParser.setText(interpretedQuery);
}
setIntroVisible(visible, animate) {
@@ -394,22 +344,6 @@ class DisplaySearch extends Display {
document.title = `${text} - Yomichan Search`;
}
}
-
- static getSearchQueryFromLocation(url) {
- const match = /^[^?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url);
- return match !== null ? decodeURIComponent(match[1]) : null;
- }
-
- static async _isFirefox() {
- const {browser} = await apiGetEnvironmentInfo();
- switch (browser) {
- case 'firefox':
- case 'firefox-mobile':
- return true;
- default:
- return false;
- }
- }
}
DisplaySearch.onKeyDownIgnoreKeys = {
@@ -427,4 +361,8 @@ DisplaySearch.onKeyDownIgnoreKeys = {
'Shift': []
};
+DisplaySearch._runtimeMessageHandlers = new Map([
+ ['searchQueryUpdate', (self, {query}) => { self.onExternalSearchUpdate(query); }]
+]);
+
DisplaySearch.instance = DisplaySearch.create();
diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js
index 5e74358f..2e80e334 100644
--- a/ext/bg/js/settings/anki-templates.js
+++ b/ext/bg/js/settings/anki-templates.js
@@ -16,6 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global getOptionsContext, getOptionsMutable, settingsSaveOptions
+profileOptionsGetDefaultFieldTemplates, ankiGetFieldMarkers, ankiGetFieldMarkersHtml, dictFieldFormat
+apiOptionsGet, apiTermsFind*/
function onAnkiFieldTemplatesReset(e) {
e.preventDefault();
diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js
index 9adb2f2a..4263fc51 100644
--- a/ext/bg/js/settings/anki.js
+++ b/ext/bg/js/settings/anki.js
@@ -16,6 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global getOptionsContext, getOptionsMutable, settingsSaveOptions
+utilBackgroundIsolate, utilAnkiGetDeckNames, utilAnkiGetModelNames, utilAnkiGetModelFieldNames
+onFormOptionsChanged*/
// Private
@@ -33,14 +36,27 @@ function _ankiSpinnerShow(show) {
function _ankiSetError(error) {
const node = document.querySelector('#anki-error');
- if (!node) { return; }
+ const node2 = document.querySelector('#anki-invalid-response-error');
if (error) {
- node.hidden = false;
- node.textContent = `${error}`;
- _ankiSetErrorData(node, error);
+ const errorString = `${error}`;
+ if (node !== null) {
+ node.hidden = false;
+ node.textContent = errorString;
+ _ankiSetErrorData(node, error);
+ }
+
+ if (node2 !== null) {
+ node2.hidden = (errorString.indexOf('Invalid response') < 0);
+ }
} else {
- node.hidden = true;
- node.textContent = '';
+ if (node !== null) {
+ node.hidden = true;
+ node.textContent = '';
+ }
+
+ if (node2 !== null) {
+ node2.hidden = true;
+ }
}
}
diff --git a/ext/bg/js/settings/audio-ui.js b/ext/bg/js/settings/audio-ui.js
index 711c2291..555380b4 100644
--- a/ext/bg/js/settings/audio-ui.js
+++ b/ext/bg/js/settings/audio-ui.js
@@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-
class AudioSourceUI {
static instantiateTemplate(templateSelector) {
const template = document.querySelector(templateSelector);
diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js
index cff3f521..588d9a11 100644
--- a/ext/bg/js/settings/audio.js
+++ b/ext/bg/js/settings/audio.js
@@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global getOptionsContext, getOptionsMutable, settingsSaveOptions
+AudioSourceUI, audioGetTextToSpeechVoice*/
let audioSourceUI = null;
diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js
index becdc568..f4d622a4 100644
--- a/ext/bg/js/settings/backup.js
+++ b/ext/bg/js/settings/backup.js
@@ -16,6 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiOptionsGetFull, apiGetEnvironmentInfo
+utilBackend, utilIsolate, utilBackgroundIsolate, utilReadFileArrayBuffer
+optionsGetDefault, optionsUpdateVersion
+profileOptionsGetDefaultFieldTemplates*/
// Exporting
@@ -159,7 +163,6 @@ async function _showSettingsImportWarnings(warnings) {
sanitize: e.currentTarget.dataset.importSanitize === 'true'
});
modalNode.modal('hide');
-
};
const onModalHide = () => {
complete({result: false});
diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js
index 4d041451..5a271321 100644
--- a/ext/bg/js/settings/conditions-ui.js
+++ b/ext/bg/js/settings/conditions-ui.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global conditionsNormalizeOptionValue*/
class ConditionsUI {
static instantiateTemplate(templateSelector) {
diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js
index ed171ae9..70a22a16 100644
--- a/ext/bg/js/settings/dictionaries.js
+++ b/ext/bg/js/settings/dictionaries.js
@@ -16,6 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global getOptionsContext, getOptionsMutable, getOptionsFullMutable, settingsSaveOptions, apiOptionsGetFull, apiOptionsGet
+utilBackgroundIsolate, utilDatabaseDeleteDictionary, utilDatabaseGetDictionaryInfo, utilDatabaseGetDictionaryCounts
+utilDatabasePurge, utilDatabaseImport
+storageUpdateStats, storageEstimate
+PageExitPrevention*/
let dictionaryUI = null;
@@ -161,7 +166,7 @@ class SettingsDictionaryListUI {
delete n.dataset.dict;
$(n).modal('hide');
- const index = this.dictionaryEntries.findIndex((e) => e.dictionaryInfo.title === title);
+ const index = this.dictionaryEntries.findIndex((entry) => entry.dictionaryInfo.title === title);
if (index >= 0) {
this.dictionaryEntries[index].deleteDictionary();
}
@@ -174,7 +179,7 @@ class SettingsDictionaryEntryUI {
this.dictionaryInfo = dictionaryInfo;
this.optionsDictionary = optionsDictionary;
this.counts = null;
- this.eventListeners = [];
+ this.eventListeners = new EventListenerCollection();
this.isDeleting = false;
this.content = content;
@@ -193,10 +198,10 @@ class SettingsDictionaryEntryUI {
this.applyValues();
- this.addEventListener(this.enabledCheckbox, 'change', (e) => this.onEnabledChanged(e), false);
- this.addEventListener(this.allowSecondarySearchesCheckbox, 'change', (e) => this.onAllowSecondarySearchesChanged(e), false);
- this.addEventListener(this.priorityInput, 'change', (e) => this.onPriorityChanged(e), false);
- this.addEventListener(this.deleteButton, 'click', (e) => this.onDeleteButtonClicked(e), false);
+ this.eventListeners.addEventListener(this.enabledCheckbox, 'change', (e) => this.onEnabledChanged(e), false);
+ this.eventListeners.addEventListener(this.allowSecondarySearchesCheckbox, 'change', (e) => this.onAllowSecondarySearchesChanged(e), false);
+ this.eventListeners.addEventListener(this.priorityInput, 'change', (e) => this.onPriorityChanged(e), false);
+ this.eventListeners.addEventListener(this.deleteButton, 'click', (e) => this.onDeleteButtonClicked(e), false);
}
cleanup() {
@@ -207,7 +212,7 @@ class SettingsDictionaryEntryUI {
this.content = null;
}
this.dictionaryInfo = null;
- this.clearEventListeners();
+ this.eventListeners.removeAllEventListeners();
}
setCounts(counts) {
@@ -224,18 +229,6 @@ class SettingsDictionaryEntryUI {
this.parent.save();
}
- addEventListener(node, type, listener, options) {
- node.addEventListener(type, listener, options);
- this.eventListeners.push([node, type, listener, options]);
- }
-
- clearEventListeners() {
- for (const [node, type, listener, options] of this.eventListeners) {
- node.removeEventListener(type, listener, options);
- }
- this.eventListeners = [];
- }
-
applyValues() {
this.enabledCheckbox.checked = this.optionsDictionary.enabled;
this.allowSecondarySearchesCheckbox.checked = this.optionsDictionary.allowSecondarySearches;
@@ -272,9 +265,7 @@ class SettingsDictionaryEntryUI {
this.isDeleting = false;
progress.hidden = true;
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- onDatabaseUpdated(options);
+ onDatabaseUpdated();
}
}
@@ -359,28 +350,33 @@ async function dictSettingsInitialize() {
document.querySelector('#dict-main').addEventListener('change', (e) => onDictionaryMainChanged(e), false);
document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', (e) => onDatabaseEnablePrefixWildcardSearchesChanged(e), false);
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- onDictionaryOptionsChanged(options);
- onDatabaseUpdated(options);
+ await onDictionaryOptionsChanged();
+ await onDatabaseUpdated();
}
-async function onDictionaryOptionsChanged(options) {
+async function onDictionaryOptionsChanged() {
if (dictionaryUI === null) { return; }
+
+ const optionsContext = getOptionsContext();
+ const options = await getOptionsMutable(optionsContext);
+
dictionaryUI.setOptionsDictionaries(options.dictionaries);
const optionsFull = await apiOptionsGetFull();
document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
+
+ await updateMainDictionarySelectValue();
}
-async function onDatabaseUpdated(options) {
+async function onDatabaseUpdated() {
try {
const dictionaries = await utilDatabaseGetDictionaryInfo();
dictionaryUI.setDictionaries(dictionaries);
document.querySelector('#dict-warning').hidden = (dictionaries.length > 0);
- updateMainDictionarySelect(options, dictionaries);
+ updateMainDictionarySelectOptions(dictionaries);
+ await updateMainDictionarySelectValue();
const {counts, total} = await utilDatabaseGetDictionaryCounts(dictionaries.map((v) => v.title), true);
dictionaryUI.setCounts(counts, total);
@@ -389,7 +385,7 @@ async function onDatabaseUpdated(options) {
}
}
-async function updateMainDictionarySelect(options, dictionaries) {
+function updateMainDictionarySelectOptions(dictionaries) {
const select = document.querySelector('#dict-main');
select.textContent = ''; // Empty
@@ -399,8 +395,6 @@ async function updateMainDictionarySelect(options, dictionaries) {
option.textContent = 'Not selected';
select.appendChild(option);
- let value = '';
- const currentValue = options.general.mainDictionary;
for (const {title, sequenced} of toIterable(dictionaries)) {
if (!sequenced) { continue; }
@@ -408,26 +402,56 @@ async function updateMainDictionarySelect(options, dictionaries) {
option.value = title;
option.textContent = title;
select.appendChild(option);
+ }
+}
+
+async function updateMainDictionarySelectValue() {
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
- if (title === currentValue) {
- value = title;
+ const value = options.general.mainDictionary;
+
+ const select = document.querySelector('#dict-main');
+ let selectValue = null;
+ for (const child of select.children) {
+ if (child.nodeName.toUpperCase() === 'OPTION' && child.value === value) {
+ selectValue = value;
+ break;
}
}
- select.value = value;
-
- if (options.general.mainDictionary !== value) {
- options.general.mainDictionary = value;
- settingsSaveOptions();
+ let missingNodeOption = select.querySelector('option[data-not-installed=true]');
+ if (selectValue === null) {
+ if (missingNodeOption === null) {
+ missingNodeOption = document.createElement('option');
+ missingNodeOption.className = 'text-muted';
+ missingNodeOption.value = value;
+ missingNodeOption.textContent = `${value} (Not installed)`;
+ missingNodeOption.dataset.notInstalled = 'true';
+ select.appendChild(missingNodeOption);
+ }
+ } else {
+ if (missingNodeOption !== null) {
+ missingNodeOption.parentNode.removeChild(missingNodeOption);
+ }
}
+
+ select.value = value;
}
async function onDictionaryMainChanged(e) {
- const value = e.target.value;
+ const select = e.target;
+ const value = select.value;
+
+ const missingNodeOption = select.querySelector('option[data-not-installed=true]');
+ if (missingNodeOption !== null && missingNodeOption.value !== value) {
+ missingNodeOption.parentNode.removeChild(missingNodeOption);
+ }
+
const optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
options.general.mainDictionary = value;
- settingsSaveOptions();
+ await settingsSaveOptions();
}
@@ -467,15 +491,18 @@ function dictionaryErrorsShow(errors) {
dialog.textContent = '';
if (errors !== null && errors.length > 0) {
- const uniqueErrors = {};
+ const uniqueErrors = new Map();
for (let e of errors) {
console.error(e);
e = dictionaryErrorToString(e);
- uniqueErrors[e] = hasOwn(uniqueErrors, e) ? uniqueErrors[e] + 1 : 1;
+ let count = uniqueErrors.get(e);
+ if (typeof count === 'undefined') {
+ count = 0;
+ }
+ uniqueErrors.set(e, count + 1);
}
- for (const e in uniqueErrors) {
- const count = uniqueErrors[e];
+ for (const [e, count] of uniqueErrors.entries()) {
const div = document.createElement('p');
if (count > 1) {
div.textContent = `${e} `;
@@ -537,9 +564,7 @@ async function onDictionaryPurge(e) {
}
await settingsSaveOptions();
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- onDatabaseUpdated(options);
+ onDatabaseUpdated();
} catch (err) {
dictionaryErrorsShow([err]);
} finally {
@@ -611,9 +636,7 @@ async function onDictionaryImport(e) {
dictionaryErrorsShow(errors);
}
- const optionsContext = getOptionsContext();
- const options = await getOptionsMutable(optionsContext);
- onDatabaseUpdated(options);
+ onDatabaseUpdated();
}
} catch (err) {
dictionaryErrorsShow([err]);
diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
index 3bf65eda..d1ad2c6b 100644
--- a/ext/bg/js/settings/main.js
+++ b/ext/bg/js/settings/main.js
@@ -16,6 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global getOptionsContext, apiOptionsSave
+utilBackend, utilIsolate, utilBackgroundIsolate
+ankiErrorShown, ankiFieldsToDict
+ankiTemplatesUpdateValue, onAnkiOptionsChanged, onDictionaryOptionsChanged
+appearanceInitialize, audioSettingsInitialize, profileOptionsSetup, dictSettingsInitialize
+ankiInitialize, ankiTemplatesInitialize, storageInfoInitialize
+*/
+
function getOptionsMutable(optionsContext) {
return utilBackend().getOptions(
utilBackgroundIsolate(optionsContext)
@@ -28,6 +36,22 @@ function getOptionsFullMutable() {
async function formRead(options) {
options.general.enable = $('#enable').prop('checked');
+ const enableClipboardPopups = $('#enable-clipboard-popups').prop('checked');
+ if (enableClipboardPopups) {
+ options.general.enableClipboardPopups = await new Promise((resolve, _reject) => {
+ chrome.permissions.request(
+ {permissions: ['clipboardRead']},
+ (granted) => {
+ if (!granted) {
+ $('#enable-clipboard-popups').prop('checked', false);
+ }
+ resolve(granted);
+ }
+ );
+ });
+ } else {
+ options.general.enableClipboardPopups = false;
+ }
options.general.showGuide = $('#show-usage-guide').prop('checked');
options.general.compactTags = $('#compact-tags').prop('checked');
options.general.compactGlossaries = $('#compact-glossaries').prop('checked');
@@ -44,7 +68,7 @@ async function formRead(options) {
options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10);
options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0);
options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);
- options.general.popupScalingFactor = parseInt($('#popup-scaling-factor').val(), 10);
+ 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.popupTheme = $('#popup-theme').val();
@@ -67,6 +91,7 @@ async function formRead(options) {
options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked');
options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked');
options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked');
+ options.scanning.enableSearchTags = $('#enable-search-tags').prop('checked');
options.scanning.delay = parseInt($('#scan-delay').val(), 10);
options.scanning.length = parseInt($('#scan-length').val(), 10);
options.scanning.modifier = $('#scan-modifier-key').val();
@@ -103,6 +128,7 @@ async function formRead(options) {
async function formWrite(options) {
$('#enable').prop('checked', options.general.enable);
+ $('#enable-clipboard-popups').prop('checked', options.general.enableClipboardPopups);
$('#show-usage-guide').prop('checked', options.general.showGuide);
$('#compact-tags').prop('checked', options.general.compactTags);
$('#compact-glossaries').prop('checked', options.general.compactGlossaries);
@@ -142,6 +168,7 @@ async function formWrite(options) {
$('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch);
$('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions);
$('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage);
+ $('#enable-search-tags').prop('checked', options.scanning.enableSearchTags);
$('#scan-delay').val(options.scanning.delay);
$('#scan-length').val(options.scanning.length);
$('#scan-modifier-key').val(options.scanning.modifier);
@@ -167,7 +194,7 @@ async function formWrite(options) {
await ankiTemplatesUpdateValue();
await onAnkiOptionsChanged(options);
- await onDictionaryOptionsChanged(options);
+ await onDictionaryOptionsChanged();
formUpdateVisibility(options);
}
@@ -215,7 +242,7 @@ async function settingsSaveOptions() {
await apiOptionsSave(source);
}
-async function onOptionsUpdate({source}) {
+async function onOptionsUpdated({source}) {
const thisSource = await settingsGetSource();
if (source === thisSource) { return; }
@@ -247,7 +274,7 @@ async function onReady() {
storageInfoInitialize();
- yomichan.on('optionsUpdate', onOptionsUpdate);
+ yomichan.on('optionsUpdated', onOptionsUpdated);
}
$(document).ready(() => onReady());
diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js
index 37a4b416..aa2b6100 100644
--- a/ext/bg/js/settings/popup-preview-frame.js
+++ b/ext/bg/js/settings/popup-preview-frame.js
@@ -16,15 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiOptionsGet, Popup, PopupProxyHost, Frontend, TextSourceRange*/
class SettingsPopupPreview {
constructor() {
this.frontend = null;
this.apiOptionsGetOld = apiOptionsGet;
- this.popupInjectOuterStylesheetOld = Popup.injectOuterStylesheet;
+ this.popup = null;
+ this.popupSetCustomOuterCssOld = null;
this.popupShown = false;
this.themeChangeTimeout = null;
this.textSource = null;
+ this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
}
static create() {
@@ -49,18 +52,18 @@ class SettingsPopupPreview {
const popupHost = new PopupProxyHost();
await popupHost.prepare();
- const popup = popupHost.createPopup(null, 0);
- popup.setChildrenSupported(false);
+ this.popup = popupHost.getOrCreatePopup();
+ this.popup.setChildrenSupported(false);
- this.frontend = new Frontend(popup);
+ this.popupSetCustomOuterCssOld = this.popup.setCustomOuterCss;
+ this.popup.setCustomOuterCss = (...args) => this.popupSetCustomOuterCss(...args);
- this.frontend.setEnabled = function () {};
- this.frontend.searchClear = function () {};
+ this.frontend = new Frontend(this.popup);
- await this.frontend.prepare();
+ this.frontend.setEnabled = () => {};
+ this.frontend.searchClear = () => {};
- // Overwrite popup
- Popup.injectOuterStylesheet = (...args) => this.popupInjectOuterStylesheet(...args);
+ await this.frontend.prepare();
// Update search
this.updateSearch();
@@ -82,20 +85,21 @@ class SettingsPopupPreview {
return options;
}
- popupInjectOuterStylesheet(...args) {
+ async popupSetCustomOuterCss(...args) {
// This simulates the stylesheet priorities when injecting using the web extension API.
- const result = this.popupInjectOuterStylesheetOld(...args);
+ const result = await this.popupSetCustomOuterCssOld.call(this.popup, ...args);
- const outerStylesheet = Popup.outerStylesheet;
const node = document.querySelector('#client-css');
- if (node !== null && outerStylesheet !== null) {
- node.parentNode.insertBefore(outerStylesheet, node);
+ if (node !== null && result !== null) {
+ node.parentNode.insertBefore(result, node);
}
return result;
}
onMessage(e) {
+ if (e.origin !== this._targetOrigin) { return; }
+
const {action, params} = e.data;
const handler = SettingsPopupPreview._messageHandlers.get(action);
if (typeof handler !== 'function') { return; }
@@ -136,7 +140,7 @@ class SettingsPopupPreview {
setCustomOuterCss(css) {
if (this.frontend === null) { return; }
- this.frontend.popup.setCustomOuterCss(css, true);
+ this.frontend.popup.setCustomOuterCss(css, false);
}
async updateSearch() {
diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js
index 0d20471e..d1d2ff5e 100644
--- a/ext/bg/js/settings/popup-preview.js
+++ b/ext/bg/js/settings/popup-preview.js
@@ -40,20 +40,22 @@ function showAppearancePreview() {
window.wanakana.bind(text[0]);
+ const targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
+
text.on('input', () => {
const action = 'setText';
const params = {text: text.val()};
- frame.contentWindow.postMessage({action, params}, '*');
+ frame.contentWindow.postMessage({action, params}, targetOrigin);
});
customCss.on('input', () => {
const action = 'setCustomCss';
const params = {css: customCss.val()};
- frame.contentWindow.postMessage({action, params}, '*');
+ frame.contentWindow.postMessage({action, params}, targetOrigin);
});
customOuterCss.on('input', () => {
const action = 'setCustomOuterCss';
const params = {css: customOuterCss.val()};
- frame.contentWindow.postMessage({action, params}, '*');
+ frame.contentWindow.postMessage({action, params}, targetOrigin);
});
container.append(frame);
diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js
index c4e68b53..3e589809 100644
--- a/ext/bg/js/settings/profiles.js
+++ b/ext/bg/js/settings/profiles.js
@@ -16,6 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global getOptionsMutable, getOptionsFullMutable, settingsSaveOptions, apiOptionsGetFull
+utilBackgroundIsolate, formWrite
+conditionsClearCaches, ConditionsUI, profileConditionsDescriptor*/
+
let currentProfileIndex = 0;
let profileConditionsContainer = null;
diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js
index 6c10f665..cbe1bb4d 100644
--- a/ext/bg/js/settings/storage.js
+++ b/ext/bg/js/settings/storage.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiGetEnvironmentInfo*/
function storageBytesToLabeledString(size) {
const base = 1000;
diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js
deleted file mode 100644
index 2f65be31..00000000
--- a/ext/bg/js/templates.js
+++ /dev/null
@@ -1,55 +0,0 @@
-(function() {
- var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
-templates['query-parser.html'] = template({"1":function(container,depth0,helpers,partials,data) {
- var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
-
- return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.preview : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(4, data, 0),"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers.each.call(alias1,depth0,{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + "</span>";
-},"2":function(container,depth0,helpers,partials,data) {
- return "<span class=\"query-parser-term-preview\">";
-},"4":function(container,depth0,helpers,partials,data) {
- return "<span class=\"query-parser-term\">";
-},"6":function(container,depth0,helpers,partials,data) {
- var stack1;
-
- return ((stack1 = container.invokePartial(partials.part,depth0,{"name":"part","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
-},"8":function(container,depth0,helpers,partials,data) {
- var stack1;
-
- return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.raw : depth0),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.program(12, data, 0),"data":data})) != null ? stack1 : "");
-},"9":function(container,depth0,helpers,partials,data) {
- var stack1;
-
- return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"10":function(container,depth0,helpers,partials,data) {
- return "<span class=\"query-parser-char\">"
- + container.escapeExpression(container.lambda(depth0, depth0))
- + "</span>";
-},"12":function(container,depth0,helpers,partials,data) {
- var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {});
-
- return "<ruby>"
- + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + "<rt>"
- + container.escapeExpression(((helper = (helper = helpers.reading || (depth0 != null ? depth0.reading : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"reading","hash":{},"data":data}) : helper)))
- + "</rt></ruby>";
-},"14":function(container,depth0,helpers,partials,data,blockParams,depths) {
- var stack1;
-
- return ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"preview":(depths[1] != null ? depths[1].preview : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
-},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {
- var stack1;
-
- return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.terms : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"main_d": function(fn, props, container, depth0, data, blockParams, depths) {
-
- var decorators = container.decorators;
-
- fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn;
- fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(8, data, 0, blockParams, depths),"inverse":container.noop,"args":["part"],"data":data}) || fn;
- return fn;
- }
-
-,"useDecorators":true,"usePartial":true,"useData":true,"useDepths":true});
-})(); \ No newline at end of file
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index dfec54ac..a675a9f7 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -16,12 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global requestJson
+dictTermsMergeBySequence, dictTagBuildSource, dictTermsMergeByGloss, dictTermsSort, dictTagsSort
+dictEnabledSet, dictTermsGroup, dictTermsCompressTags, dictTermsUndupe, dictTagSanitize
+jpDistributeFurigana, jpConvertHalfWidthKanaToFullWidth, jpConvertNumericTofullWidth
+jpConvertAlphabeticToKana, jpHiraganaToKatakana, jpKatakanaToHiragana, jpIsCharCodeJapanese
+Database, Deinflector*/
class Translator {
constructor() {
this.database = null;
this.deinflector = null;
- this.tagCache = {};
+ this.tagCache = new Map();
}
async prepare() {
@@ -38,24 +44,24 @@ class Translator {
}
async purgeDatabase() {
- this.tagCache = {};
+ this.tagCache.clear();
await this.database.purge();
}
async deleteDictionary(dictionaryName) {
- this.tagCache = {};
+ this.tagCache.clear();
await this.database.deleteDictionary(dictionaryName);
}
async getSequencedDefinitions(definitions, mainDictionary) {
- const definitionsBySequence = dictTermsMergeBySequence(definitions, mainDictionary);
- const defaultDefinitions = definitionsBySequence['-1'];
+ const [definitionsBySequence, defaultDefinitions] = dictTermsMergeBySequence(definitions, mainDictionary);
- const sequenceList = Object.keys(definitionsBySequence).map((v) => Number(v)).filter((v) => v >= 0);
- const sequencedDefinitions = sequenceList.map((key) => ({
- definitions: definitionsBySequence[key],
- rawDefinitions: []
- }));
+ const sequenceList = [];
+ const sequencedDefinitions = [];
+ for (const [key, value] of definitionsBySequence.entries()) {
+ sequenceList.push(key);
+ sequencedDefinitions.push({definitions: value, rawDefinitions: []});
+ }
for (const definition of await this.database.findTermsBySequenceBulk(sequenceList, mainDictionary)) {
sequencedDefinitions[definition.index].rawDefinitions.push(definition);
@@ -64,8 +70,8 @@ class Translator {
return {sequencedDefinitions, defaultDefinitions};
}
- async getMergedSecondarySearchResults(text, expressionsMap, secondarySearchTitles) {
- if (secondarySearchTitles.length === 0) {
+ async getMergedSecondarySearchResults(text, expressionsMap, secondarySearchDictionaries) {
+ if (secondarySearchDictionaries.size === 0) {
return [];
}
@@ -79,7 +85,7 @@ class Translator {
}
}
- const definitions = await this.database.findTermsExactBulk(expressionList, readingList, secondarySearchTitles);
+ const definitions = await this.database.findTermsExactBulk(expressionList, readingList, secondarySearchDictionaries);
for (const definition of definitions) {
const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary);
definitionTags.push(dictTagBuildSource(definition.dictionary));
@@ -95,7 +101,7 @@ class Translator {
return definitions;
}
- async getMergedDefinition(text, dictionaries, sequencedDefinition, defaultDefinitions, secondarySearchTitles, mergedByTermIndices) {
+ async getMergedDefinition(text, dictionaries, sequencedDefinition, defaultDefinitions, secondarySearchDictionaries, mergedByTermIndices) {
const result = sequencedDefinition.definitions;
const rawDefinitionsBySequence = sequencedDefinition.rawDefinitions;
@@ -108,12 +114,11 @@ class Translator {
}
const definitionsByGloss = dictTermsMergeByGloss(result, rawDefinitionsBySequence);
- const secondarySearchResults = await this.getMergedSecondarySearchResults(text, result.expressions, secondarySearchTitles);
+ const secondarySearchResults = await this.getMergedSecondarySearchResults(text, result.expressions, secondarySearchDictionaries);
dictTermsMergeByGloss(result, defaultDefinitions.concat(secondarySearchResults), definitionsByGloss, mergedByTermIndices);
- for (const gloss in definitionsByGloss) {
- const definition = definitionsByGloss[gloss];
+ for (const definition of definitionsByGloss.values()) {
dictTagsSort(definition.definitionTags);
result.definitions.push(definition);
}
@@ -122,7 +127,8 @@ class Translator {
const expressions = [];
for (const [expression, readingMap] of result.expressions.entries()) {
- for (const [reading, termTags] of readingMap.entries()) {
+ for (const [reading, termTagsMap] of readingMap.entries()) {
+ const termTags = [...termTagsMap.values()];
const score = termTags.map((tag) => tag.score).reduce((p, v) => p + v, 0);
expressions.push(Translator.createExpression(expression, reading, dictTagsSort(termTags), Translator.scoreToTermFrequency(score)));
}
@@ -135,14 +141,16 @@ class Translator {
return result;
}
- async findTerms(text, details, options) {
- switch (options.general.resultOutputMode) {
+ async findTerms(mode, text, details, options) {
+ switch (mode) {
case 'group':
return await this.findTermsGrouped(text, details, options);
case 'merge':
return await this.findTermsMerged(text, details, options);
case 'split':
return await this.findTermsSplit(text, details, options);
+ case 'simple':
+ return await this.findTermsSimple(text, details, options);
default:
return [[], 0];
}
@@ -150,11 +158,10 @@ class Translator {
async findTermsGrouped(text, details, options) {
const dictionaries = dictEnabledSet(options);
- const titles = Object.keys(dictionaries);
const [definitions, length] = await this.findTermsInternal(text, dictionaries, details, options);
const definitionsGrouped = dictTermsGroup(definitions, dictionaries);
- await this.buildTermMeta(definitionsGrouped, titles);
+ await this.buildTermMeta(definitionsGrouped, dictionaries);
if (options.general.compactTags) {
for (const definition of definitionsGrouped) {
@@ -167,8 +174,12 @@ class Translator {
async findTermsMerged(text, details, options) {
const dictionaries = dictEnabledSet(options);
- const secondarySearchTitles = Object.keys(options.dictionaries).filter((dict) => options.dictionaries[dict].allowSecondarySearches);
- const titles = Object.keys(dictionaries);
+ const secondarySearchDictionaries = new Map();
+ for (const [title, dictionary] of dictionaries.entries()) {
+ if (!dictionary.allowSecondarySearches) { continue; }
+ secondarySearchDictionaries.set(title, dictionary);
+ }
+
const [definitions, length] = await this.findTermsInternal(text, dictionaries, details, options);
const {sequencedDefinitions, defaultDefinitions} = await this.getSequencedDefinitions(definitions, options.general.mainDictionary);
const definitionsMerged = [];
@@ -180,7 +191,7 @@ class Translator {
dictionaries,
sequencedDefinition,
defaultDefinitions,
- secondarySearchTitles,
+ secondarySearchDictionaries,
mergedByTermIndices
);
definitionsMerged.push(result);
@@ -192,7 +203,7 @@ class Translator {
definitionsMerged.push(groupedDefinition);
}
- await this.buildTermMeta(definitionsMerged, titles);
+ await this.buildTermMeta(definitionsMerged, dictionaries);
if (options.general.compactTags) {
for (const definition of definitionsMerged) {
@@ -205,25 +216,28 @@ class Translator {
async findTermsSplit(text, details, options) {
const dictionaries = dictEnabledSet(options);
- const titles = Object.keys(dictionaries);
const [definitions, length] = await this.findTermsInternal(text, dictionaries, details, options);
- await this.buildTermMeta(definitions, titles);
+ await this.buildTermMeta(definitions, dictionaries);
return [definitions, length];
}
+ async findTermsSimple(text, details, options) {
+ const dictionaries = dictEnabledSet(options);
+ return await this.findTermsInternal(text, dictionaries, details, options);
+ }
+
async findTermsInternal(text, dictionaries, details, options) {
text = Translator.getSearchableText(text, options);
if (text.length === 0) {
return [[], 0];
}
- const titles = Object.keys(dictionaries);
const deinflections = (
details.wildcard ?
- await this.findTermWildcard(text, titles, details.wildcard) :
- await this.findTermDeinflections(text, titles, options)
+ await this.findTermWildcard(text, dictionaries, details.wildcard) :
+ await this.findTermDeinflections(text, dictionaries, options)
);
let definitions = [];
@@ -265,8 +279,8 @@ class Translator {
return [definitions, length];
}
- async findTermWildcard(text, titles, wildcard) {
- const definitions = await this.database.findTermsBulk([text], titles, wildcard);
+ async findTermWildcard(text, dictionaries, wildcard) {
+ const definitions = await this.database.findTermsBulk([text], dictionaries, wildcard);
if (definitions.length === 0) {
return [];
}
@@ -281,7 +295,7 @@ class Translator {
}];
}
- async findTermDeinflections(text, titles, options) {
+ async findTermDeinflections(text, dictionaries, options) {
const deinflections = this.getAllDeinflections(text, options);
if (deinflections.length === 0) {
@@ -303,7 +317,7 @@ class Translator {
deinflectionArray.push(deinflection);
}
- const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles, null);
+ const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, dictionaries, null);
for (const definition of definitions) {
const definitionRules = Deinflector.rulesToRuleFlags(definition.rules);
@@ -393,17 +407,12 @@ class Translator {
async findKanji(text, options) {
const dictionaries = dictEnabledSet(options);
- const titles = Object.keys(dictionaries);
- const kanjiUnique = {};
- const kanjiList = [];
+ const kanjiUnique = new Set();
for (const c of text) {
- if (!hasOwn(kanjiUnique, c)) {
- kanjiList.push(c);
- kanjiUnique[c] = true;
- }
+ kanjiUnique.add(c);
}
- const definitions = await this.database.findKanjiBulk(kanjiList, titles);
+ const definitions = await this.database.findKanjiBulk([...kanjiUnique], dictionaries);
if (definitions.length === 0) {
return definitions;
}
@@ -423,12 +432,12 @@ class Translator {
definition.stats = stats;
}
- await this.buildKanjiMeta(definitions, titles);
+ await this.buildKanjiMeta(definitions, dictionaries);
return definitions;
}
- async buildTermMeta(definitions, titles) {
+ async buildTermMeta(definitions, dictionaries) {
const terms = [];
for (const definition of definitions) {
if (definition.expressions) {
@@ -454,7 +463,7 @@ class Translator {
termList = [];
expressionsUnique.push(expression);
termsUnique.push(termList);
- termsUniqueMap[expression] = termList;
+ termsUniqueMap.set(expression, termList);
}
termList.push(term);
@@ -462,7 +471,7 @@ class Translator {
term.frequencies = [];
}
- const metas = await this.database.findTermMetaBulk(expressionsUnique, titles);
+ const metas = await this.database.findTermMetaBulk(expressionsUnique, dictionaries);
for (const {expression, mode, data, dictionary, index} of metas) {
switch (mode) {
case 'freq':
@@ -474,14 +483,14 @@ class Translator {
}
}
- async buildKanjiMeta(definitions, titles) {
+ async buildKanjiMeta(definitions, dictionaries) {
const kanjiList = [];
for (const definition of definitions) {
kanjiList.push(definition.character);
definition.frequencies = [];
}
- const metas = await this.database.findKanjiMetaBulk(kanjiList, titles);
+ const metas = await this.database.findKanjiMetaBulk(kanjiList, dictionaries);
for (const {character, mode, data, dictionary, index} of metas) {
switch (mode) {
case 'freq':
@@ -504,49 +513,50 @@ class Translator {
const names = Object.keys(items);
const tagMetaList = await this.getTagMetaList(names, title);
- const stats = {};
+ const statsGroups = new Map();
for (let i = 0; i < names.length; ++i) {
const name = names[i];
const meta = tagMetaList[i];
if (meta === null) { continue; }
const category = meta.category;
- const group = (
- hasOwn(stats, category) ?
- stats[category] :
- (stats[category] = [])
- );
+ let group = statsGroups.get(category);
+ if (typeof group === 'undefined') {
+ group = [];
+ statsGroups.set(category, group);
+ }
const stat = Object.assign({}, meta, {name, value: items[name]});
group.push(dictTagSanitize(stat));
}
+ const stats = {};
const sortCompare = (a, b) => a.notes - b.notes;
- for (const category in stats) {
- stats[category].sort(sortCompare);
+ for (const [category, group] of statsGroups.entries()) {
+ group.sort(sortCompare);
+ stats[category] = group;
}
-
return stats;
}
async getTagMetaList(names, title) {
const tagMetaList = [];
- const cache = (
- hasOwn(this.tagCache, title) ?
- this.tagCache[title] :
- (this.tagCache[title] = {})
- );
+ let cache = this.tagCache.get(title);
+ if (typeof cache === 'undefined') {
+ cache = new Map();
+ this.tagCache.set(title, cache);
+ }
for (const name of names) {
const base = Translator.getNameBase(name);
- if (hasOwn(cache, base)) {
- tagMetaList.push(cache[base]);
- } else {
- const tagMeta = await this.database.findTagForTitle(base, title);
- cache[base] = tagMeta;
- tagMetaList.push(tagMeta);
+ let tagMeta = cache.get(base);
+ if (typeof tagMeta === 'undefined') {
+ tagMeta = await this.database.findTagForTitle(base, title);
+ cache.set(base, tagMeta);
}
+
+ tagMetaList.push(tagMeta);
}
return tagMetaList;
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 333e814b..5ce4b08c 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -33,7 +33,7 @@ function utilIsolate(value) {
}
function utilFunctionIsolate(func) {
- return function (...args) {
+ return function isolatedFunction(...args) {
try {
args = args.map((v) => utilIsolate(v));
return func.call(this, ...args);
@@ -59,32 +59,6 @@ function utilBackgroundFunctionIsolate(func) {
return backgroundPage.utilFunctionIsolate(func);
}
-function utilSetEqual(setA, setB) {
- if (setA.size !== setB.size) {
- return false;
- }
-
- for (const value of setA) {
- if (!setB.has(value)) {
- return false;
- }
- }
-
- return true;
-}
-
-function utilSetIntersection(setA, setB) {
- return new Set(
- [...setA].filter((value) => setB.has(value))
- );
-}
-
-function utilSetDifference(setA, setB) {
- return new Set(
- [...setA].filter((value) => !setB.has(value))
- );
-}
-
function utilStringHashCode(string) {
let hashCode = 0;
diff --git a/ext/bg/query-parser-templates.html b/ext/bg/query-parser-templates.html
new file mode 100644
index 00000000..7cab16a9
--- /dev/null
+++ b/ext/bg/query-parser-templates.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html><html><head></head><body>
+
+<template id="term-template"><span class="query-parser-term" data-type="normal"></span></template>
+<template id="term-preview-template"><span class="query-parser-term" data-type="preview"></span></template>
+<template id="segment-template"><ruby class="query-parser-segment"><span class="query-parser-segment-text"></span><rt class="query-parser-segment-reading"></rt></ruby></template>
+<template id="char-template"><span class="query-parser-char"></span></template>
+
+<template id="select-template"><select class="query-parser-select form-control"></select></template>
+<template id="select-option-template"><option class="query-parser-select-option"></option></template>
+
+</body></html>
diff --git a/ext/bg/search.html b/ext/bg/search.html
index 74afbb68..d6336826 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -25,29 +25,31 @@
<p style="margin-bottom: 0;">Search your installed dictionaries by entering a Japanese expression into the field below.</p>
</div>
- <div class="input-group" style="padding-top: 20px;">
- <span title="Enable kana input method" class="input-group-text">
- <input type="checkbox" id="wanakana-enable" class="icon-checkbox" />
- <label for="wanakana-enable" class="scan-disable">あ</label>
- </span>
- <span title="Enable clipboard monitor" class="input-group-text">
- <input type="checkbox" id="clipboard-monitor-enable" class="icon-checkbox" />
- <label for="clipboard-monitor-enable"><span class="glyphicon glyphicon-paste"></span></label>
- </span>
- </div>
+ <div class="search-input">
+ <div class="input-group" style="padding-top: 20px;">
+ <span title="Enable kana input method" class="input-group-text">
+ <input type="checkbox" id="wanakana-enable" class="icon-checkbox" />
+ <label for="wanakana-enable" class="scan-disable">あ</label>
+ </span>
+ <span title="Enable clipboard monitor" class="input-group-text">
+ <input type="checkbox" id="clipboard-monitor-enable" class="icon-checkbox" />
+ <label for="clipboard-monitor-enable"><span class="glyphicon glyphicon-paste"></span></label>
+ </span>
+ </div>
- <form class="input-group">
- <input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>
- <span class="input-group-btn">
- <input type="submit" class="btn btn-default form-control" id="search" value="Search">
- </span>
- </form>
+ <form class="input-group">
+ <input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>
+ <span class="input-group-btn">
+ <input type="submit" class="btn btn-default form-control" id="search" value="Search">
+ </span>
+ </form>
+ </div>
<div id="spinner" hidden><img src="/mixed/img/spinner.gif"></div>
<div class="scan-disable">
- <div id="query-parser-select" class="input-group"></div>
- <div id="query-parser"></div>
+ <div id="query-parser-select-container" class="input-group"></div>
+ <div id="query-parser-content"></div>
</div>
<hr>
@@ -75,18 +77,20 @@
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
- <script src="/bg/js/templates.js"></script>
+ <script src="/bg/js/japanese.js"></script>
<script src="/fg/js/document.js"></script>
<script src="/fg/js/source.js"></script>
<script src="/mixed/js/audio.js"></script>
<script src="/mixed/js/display-context.js"></script>
<script src="/mixed/js/display.js"></script>
<script src="/mixed/js/display-generator.js"></script>
- <script src="/mixed/js/japanese.js"></script>
<script src="/mixed/js/scroll.js"></script>
<script src="/mixed/js/text-scanner.js"></script>
+ <script src="/mixed/js/template-handler.js"></script>
+ <script src="/bg/js/search-query-parser-generator.js"></script>
<script src="/bg/js/search-query-parser.js"></script>
+ <script src="/bg/js/clipboard-monitor.js"></script>
<script src="/bg/js/search.js"></script>
<script src="/bg/js/search-frontend.js"></script>
</body>
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 3e06d4b5..b048a36c 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -134,6 +134,10 @@
<label><input type="checkbox" id="enable"> Enable content scanning</label>
</div>
+ <div class="checkbox" data-hide-for-browser="firefox-mobile">
+ <label><input type="checkbox" id="enable-clipboard-popups"> Enable native popups when copying Japanese text</label>
+ </div>
+
<div class="checkbox">
<label><input type="checkbox" id="show-usage-guide"> Show usage guide on startup</label>
</div>
@@ -481,7 +485,7 @@
</p>
<div class="checkbox">
- <label><input type="checkbox" id="enable-search-within-first-popup"> Enable search when clicking glossary entries</label>
+ <label><input type="checkbox" id="enable-search-within-first-popup"> Enable search when clicking glossary entries and tags</label>
</div>
<div class="checkbox">
@@ -492,6 +496,10 @@
<label><input type="checkbox" id="enable-scanning-of-popup-expressions"> Enable scanning of expressions in search results</label>
</div>
+ <div class="checkbox">
+ <label><input type="checkbox" id="enable-search-tags"> Enable clickable and scannable tags for searching expressions and their readings</label>
+ </div>
+
<div class="form-group">
<label for="popup-nesting-max-depth">Maximum number of additional popups</label>
<input type="number" min="0" step="1" id="popup-nesting-max-depth" class="form-control">
@@ -760,6 +768,13 @@
<div class="alert alert-danger" id="anki-error" hidden></div>
+ <div class="alert alert-danger" id="anki-invalid-response-error" hidden>
+ Attempting to connect to Anki can sometimes return an error message which includes "Invalid response",
+ which may indicate that the value of the <strong>Interface server</strong> option is incorrect.
+ The <strong>Show advanced options</strong> checkbox under General Options must be ticked ticked to show this option.
+ Resetting it to the default value may fix issues that are occurring.
+ </div>
+
<div class="form-group">
<label for="card-tags">Card tags <span class="label-light">(comma or space separated)</span></label>
<input type="text" id="card-tags" class="form-control">
@@ -771,7 +786,7 @@
</div>
<div class="form-group options-advanced">
- <label for="interface-server">Interface server</label>
+ <label for="interface-server">Interface server <span class="label-light">(Default: http://127.0.0.1:8765)</span></label>
<input type="text" id="interface-server" class="form-control">
</div>
@@ -1073,16 +1088,15 @@
<script src="/mixed/js/core.js"></script>
<script src="/mixed/js/dom.js"></script>
<script src="/mixed/js/api.js"></script>
- <script src="/mixed/js/japanese.js"></script>
<script src="/bg/js/anki.js"></script>
<script src="/bg/js/conditions.js"></script>
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
+ <script src="/bg/js/japanese.js"></script>
<script src="/bg/js/options.js"></script>
<script src="/bg/js/page-exit-prevention.js"></script>
<script src="/bg/js/profile-conditions.js"></script>
- <script src="/bg/js/templates.js"></script>
<script src="/bg/js/util.js"></script>
<script src="/mixed/js/audio.js"></script>