aboutsummaryrefslogtreecommitdiff
path: root/ext
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
parent0c5b9b1fa1599cbf769d96cdebc226310f9dd8bc (diff)
parent706c3edcffb0078d71fd5b58775f16cf5fc1205b (diff)
Merge branch 'master' into testing
Diffstat (limited to 'ext')
-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.js (renamed from ext/mixed/js/japanese.js)15
-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
-rw-r--r--ext/fg/float.html3
-rw-r--r--ext/fg/js/document.js8
-rw-r--r--ext/fg/js/float.js85
-rw-r--r--ext/fg/js/frontend-api-sender.js10
-rw-r--r--ext/fg/js/frontend-initialize.js12
-rw-r--r--ext/fg/js/frontend.js3
-rw-r--r--ext/fg/js/popup-nested.js1
-rw-r--r--ext/fg/js/popup-proxy-host.js65
-rw-r--r--ext/fg/js/popup-proxy.js9
-rw-r--r--ext/fg/js/popup.js262
-rw-r--r--ext/fg/js/source.js16
-rw-r--r--ext/manifest.json4
-rw-r--r--ext/mixed/css/display-dark.css1
-rw-r--r--ext/mixed/css/display-default.css1
-rw-r--r--ext/mixed/css/display.css60
-rw-r--r--ext/mixed/display-templates.html11
-rw-r--r--ext/mixed/js/api.js18
-rw-r--r--ext/mixed/js/audio.js28
-rw-r--r--ext/mixed/js/core.js59
-rw-r--r--ext/mixed/js/display-generator.js128
-rw-r--r--ext/mixed/js/display.js144
-rw-r--r--ext/mixed/js/template-handler.js47
-rw-r--r--ext/mixed/js/text-scanner.js66
70 files changed, 2497 insertions, 1418 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/mixed/js/japanese.js b/ext/bg/js/japanese.js
index 0da822d7..abb32da4 100644
--- a/ext/mixed/js/japanese.js
+++ b/ext/bg/js/japanese.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global wanakana*/
const JP_HALFWIDTH_KATAKANA_MAPPING = new Map([
['ヲ', 'ヲヺ-'],
@@ -108,7 +109,7 @@ const JP_JAPANESE_RANGES = [
[0xff1a, 0xff1f], // Fullwidth punctuation 2
[0xff3b, 0xff3f], // Fullwidth punctuation 3
[0xff5b, 0xff60], // Fullwidth punctuation 4
- [0xffe0, 0xffee], // Currency markers
+ [0xffe0, 0xffee] // Currency markers
];
@@ -223,15 +224,15 @@ function jpDistributeFurigana(expression, reading) {
}
let isAmbiguous = false;
- const segmentize = (reading, groups) => {
+ const segmentize = (reading2, groups) => {
if (groups.length === 0 || isAmbiguous) {
return [];
}
const group = groups[0];
if (group.mode === 'kana') {
- if (jpKatakanaToHiragana(reading).startsWith(jpKatakanaToHiragana(group.text))) {
- const readingLeft = reading.substring(group.text.length);
+ 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);
@@ -239,9 +240,9 @@ function jpDistributeFurigana(expression, reading) {
}
} else {
let foundSegments = null;
- for (let i = reading.length; i >= group.text.length; --i) {
- const readingUsed = reading.substring(0, i);
- const readingLeft = reading.substring(i);
+ 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) {
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>
diff --git a/ext/fg/float.html b/ext/fg/float.html
index bec5ae68..352a866a 100644
--- a/ext/fg/float.html
+++ b/ext/fg/float.html
@@ -35,7 +35,7 @@
<h1>Yomichan Updated!</h1>
<p>
The Yomichan extension has been updated to a new version! In order to continue
- viewing definitions on this page you must reload this tab or restart your browser.
+ viewing definitions on this page, you must reload this tab or restart your browser.
</p>
</div>
</div>
@@ -51,6 +51,7 @@
<script src="/mixed/js/display.js"></script>
<script src="/mixed/js/display-generator.js"></script>
<script src="/mixed/js/scroll.js"></script>
+ <script src="/mixed/js/template-handler.js"></script>
<script src="/fg/js/float.js"></script>
diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js
index 71654b29..35861475 100644
--- a/ext/fg/js/document.js
+++ b/ext/fg/js/document.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global TextSourceElement, TextSourceRange, DOM*/
const REGEX_TRANSPARENT_COLOR = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/;
@@ -49,7 +50,9 @@ function docImposterCreate(element, isTextarea) {
const imposter = document.createElement('div');
const imposterStyle = imposter.style;
- imposter.innerText = element.value;
+ let value = element.value;
+ if (value.endsWith('\n')) { value += '\n'; }
+ imposter.textContent = value;
for (let i = 0, ii = elementStyle.length; i < ii; ++i) {
const property = elementStyle[i];
@@ -191,8 +194,7 @@ function docSentenceExtract(source, extent) {
if (terminators.includes(c)) {
endPos = i + 1;
break;
- }
- else if (c in quotesBwd) {
+ } else if (c in quotesBwd) {
endPos = i;
break;
}
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 8d61d8f6..8f21a9c5 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global popupNestedInitialize, apiForward, apiGetMessageToken, Display*/
class DisplayFloat extends Display {
constructor() {
@@ -28,11 +29,33 @@ class DisplayFloat extends Display {
};
this._orphaned = false;
+ this._prepareInvoked = false;
+ this._messageToken = null;
+ this._messageTokenPromise = null;
yomichan.on('orphaned', () => this.onOrphaned());
window.addEventListener('message', (e) => this.onMessage(e), false);
}
+ async prepare(options, popupInfo, url, childrenSupported, scale, uniqueId) {
+ if (this._prepareInvoked) { return; }
+ this._prepareInvoked = true;
+
+ await super.prepare(options);
+
+ const {id, depth, parentFrameId} = popupInfo;
+ this.optionsContext.depth = depth;
+ this.optionsContext.url = url;
+
+ if (childrenSupported) {
+ popupNestedInitialize(id, depth, parentFrameId, url);
+ }
+
+ this.setContentScale(scale);
+
+ apiForward('popupPrepareCompleted', {uniqueId});
+ }
+
onError(error) {
if (this._orphaned) {
this.setContent('orphaned');
@@ -54,11 +77,23 @@ class DisplayFloat extends Display {
}
onMessage(e) {
- const {action, params} = e.data;
- const handler = DisplayFloat._messageHandlers.get(action);
- if (typeof handler !== 'function') { return; }
-
- handler(this, params);
+ const data = e.data;
+ if (typeof data !== 'object' || data === null) { return; } // Invalid data
+
+ const token = data.token;
+ if (typeof token !== 'string') { return; } // Invalid data
+
+ if (this._messageToken === null) {
+ // Async
+ this.getMessageToken()
+ .then(
+ () => { this.handleAction(token, data); },
+ () => {}
+ );
+ } else {
+ // Sync
+ this.handleAction(token, data);
+ }
}
onKeyDown(e) {
@@ -73,6 +108,30 @@ class DisplayFloat extends Display {
return super.onKeyDown(e);
}
+ async getMessageToken() {
+ // this._messageTokenPromise is used to ensure that only one call to apiGetMessageToken is made.
+ if (this._messageTokenPromise === null) {
+ this._messageTokenPromise = apiGetMessageToken();
+ }
+ const messageToken = await this._messageTokenPromise;
+ if (this._messageToken === null) {
+ this._messageToken = messageToken;
+ }
+ this._messageTokenPromise = null;
+ }
+
+ handleAction(token, {action, params}) {
+ if (token !== this._messageToken) {
+ // Invalid token
+ return;
+ }
+
+ const handler = DisplayFloat._messageHandlers.get(action);
+ if (typeof handler !== 'function') { return; }
+
+ handler(this, params);
+ }
+
getOptionsContext() {
return this.optionsContext;
}
@@ -92,20 +151,6 @@ class DisplayFloat extends Display {
setContentScale(scale) {
document.body.style.fontSize = `${scale}em`;
}
-
- async initialize(options, popupInfo, url, childrenSupported, scale) {
- await super.initialize(options);
-
- const {id, depth, parentFrameId} = popupInfo;
- this.optionsContext.depth = depth;
- this.optionsContext.url = url;
-
- if (childrenSupported) {
- popupNestedInitialize(id, depth, parentFrameId, url);
- }
-
- this.setContentScale(scale);
- }
}
DisplayFloat._onKeyDownHandlers = new Map([
@@ -122,7 +167,7 @@ DisplayFloat._messageHandlers = new Map([
['setContent', (self, {type, details}) => self.setContent(type, details)],
['clearAutoPlayTimer', (self) => self.clearAutoPlayTimer()],
['setCustomCss', (self, {css}) => self.setCustomCss(css)],
- ['initialize', (self, {options, popupInfo, url, childrenSupported, scale}) => self.initialize(options, popupInfo, url, childrenSupported, scale)],
+ ['prepare', (self, {options, popupInfo, url, childrenSupported, scale, uniqueId}) => self.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)],
['setContentScale', (self, {scale}) => self.setContentScale(scale)]
]);
diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js
index 93c2e593..8dc6aaf3 100644
--- a/ext/fg/js/frontend-api-sender.js
+++ b/ext/fg/js/frontend-api-sender.js
@@ -19,7 +19,7 @@
class FrontendApiSender {
constructor() {
- this.senderId = FrontendApiSender.generateId(16);
+ this.senderId = yomichan.generateId(16);
this.ackTimeout = 3000; // 3 seconds
this.responseTimeout = 10000; // 10 seconds
this.callbacks = new Map();
@@ -123,12 +123,4 @@ class FrontendApiSender {
info.timer = null;
info.reject(new Error(reason));
}
-
- static generateId(length) {
- let id = '';
- for (let i = 0; i < length; ++i) {
- id += Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
- }
- return id;
- }
}
diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js
index 9c923fea..54b874f2 100644
--- a/ext/fg/js/frontend-initialize.js
+++ b/ext/fg/js/frontend-initialize.js
@@ -16,18 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global PopupProxyHost, PopupProxy, Frontend*/
async function main() {
const data = window.frontendInitializationData || {};
const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data;
- let popupHost = null;
- if (!proxy) {
- popupHost = new PopupProxyHost();
+ let popup;
+ if (proxy) {
+ popup = new PopupProxy(null, depth + 1, id, parentFrameId, url);
+ } else {
+ const popupHost = new PopupProxyHost();
await popupHost.prepare();
+
+ popup = popupHost.getOrCreatePopup();
}
- const popup = proxy ? new PopupProxy(depth + 1, id, parentFrameId, url) : popupHost.createPopup(null, depth);
const frontend = new Frontend(popup, ignoreNodes);
await frontend.prepare();
}
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 2286bf19..67045241 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiGetZoom, apiOptionsGet, apiTermsFind, apiKanjiFind, docSentenceExtract, TextScanner*/
class Frontend extends TextScanner {
constructor(popup, ignoreNodes) {
@@ -55,7 +56,7 @@ class Frontend extends TextScanner {
}
yomichan.on('orphaned', () => this.onOrphaned());
- yomichan.on('optionsUpdate', () => this.updateOptions());
+ yomichan.on('optionsUpdated', () => this.updateOptions());
yomichan.on('zoomChanged', (e) => this.onZoomChanged(e));
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js
index 3f3c945e..3e5f5b80 100644
--- a/ext/fg/js/popup-nested.js
+++ b/ext/fg/js/popup-nested.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiOptionsGet*/
let popupNestedInitialized = false;
diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js
index 427172c6..e55801ff 100644
--- a/ext/fg/js/popup-proxy-host.js
+++ b/ext/fg/js/popup-proxy-host.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiFrameInformationGet, FrontendApiReceiver, Popup*/
class PopupProxyHost {
constructor() {
@@ -33,7 +34,7 @@ class PopupProxyHost {
if (typeof frameId !== 'number') { return; }
this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, new Map([
- ['createNestedPopup', ({parentId}) => this._onApiCreateNestedPopup(parentId)],
+ ['getOrCreatePopup', ({id, parentId}) => this._onApiGetOrCreatePopup(id, parentId)],
['setOptions', ({id, options}) => this._onApiSetOptions(id, options)],
['hide', ({id, changeFocus}) => this._onApiHide(id, changeFocus)],
['isVisible', ({id}) => this._onApiIsVisibleAsync(id)],
@@ -46,14 +47,51 @@ class PopupProxyHost {
]));
}
- createPopup(parentId, depth) {
- return this._createPopupInternal(parentId, depth).popup;
+ getOrCreatePopup(id=null, parentId=null) {
+ // Find by existing id
+ if (id !== null) {
+ const popup = this._popups.get(id);
+ if (typeof popup !== 'undefined') {
+ return popup;
+ }
+ }
+
+ // Find by existing parent id
+ let parent = null;
+ if (parentId !== null) {
+ parent = this._popups.get(parentId);
+ if (typeof parent !== 'undefined') {
+ const popup = parent.child;
+ if (popup !== null) {
+ return popup;
+ }
+ } else {
+ parent = null;
+ }
+ }
+
+ // New unique id
+ if (id === null) {
+ id = this._nextId++;
+ }
+
+ // Create new popup
+ const depth = (parent !== null ? parent.depth + 1 : 0);
+ const popup = new Popup(id, depth, this._frameIdPromise);
+ if (parent !== null) {
+ popup.setParent(parent);
+ }
+ this._popups.set(id, popup);
+ return popup;
}
// Message handlers
- async _onApiCreateNestedPopup(parentId) {
- return this._createPopupInternal(parentId, 0).id;
+ async _onApiGetOrCreatePopup(id, parentId) {
+ const popup = this.getOrCreatePopup(id, parentId);
+ return {
+ id: popup.id
+ };
}
async _onApiSetOptions(id, options) {
@@ -105,25 +143,10 @@ class PopupProxyHost {
// Private functions
- _createPopupInternal(parentId, depth) {
- const parent = (typeof parentId === 'string' && this._popups.has(parentId) ? this._popups.get(parentId) : null);
- const id = `${this._nextId}`;
- if (parent !== null) {
- depth = parent.depth + 1;
- }
- ++this._nextId;
- const popup = new Popup(id, depth, this._frameIdPromise);
- if (parent !== null) {
- popup.setParent(parent);
- }
- this._popups.set(id, popup);
- return {popup, id};
- }
-
_getPopup(id) {
const popup = this._popups.get(id);
if (typeof popup === 'undefined') {
- throw new Error('Invalid popup ID');
+ throw new Error(`Invalid popup ID ${id}`);
}
return popup;
}
diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js
index 4cacee53..093cdd2e 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -16,12 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global FrontendApiSender*/
class PopupProxy {
- constructor(depth, parentId, parentFrameId, url) {
+ constructor(id, depth, parentId, parentFrameId, url) {
this._parentId = parentId;
this._parentFrameId = parentFrameId;
- this._id = null;
+ this._id = id;
this._idPromise = null;
this._depth = depth;
this._url = url;
@@ -69,7 +70,7 @@ class PopupProxy {
if (this._id === null) {
return;
}
- this._invokeHostApi('setVisibleOverride', {id, visible});
+ this._invokeHostApi('setVisibleOverride', {id: this._id, visible});
}
async containsPoint(x, y) {
@@ -112,7 +113,7 @@ class PopupProxy {
}
async _getPopupIdAsync() {
- const id = await this._invokeHostApi('createNestedPopup', {parentId: this._parentId});
+ const {id} = await this._invokeHostApi('getOrCreatePopup', {id: this._id, parentId: this._parentId});
this._id = id;
return id;
}
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index e7dae93e..4927f4bd 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiInjectStylesheet, apiGetMessageToken*/
class Popup {
constructor(id, depth, frameIdPromise) {
@@ -27,32 +28,40 @@ class Popup {
this._child = null;
this._childrenSupported = true;
this._injectPromise = null;
- this._isInjected = false;
- this._isInjectedAndLoaded = false;
this._visible = false;
this._visibleOverride = null;
this._options = null;
- this._stylesheetInjectedViaApi = false;
this._contentScale = 1.0;
this._containerSizeContentScale = null;
+ this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
+ this._messageToken = null;
this._container = document.createElement('iframe');
this._container.className = 'yomichan-float';
this._container.addEventListener('mousedown', (e) => e.stopPropagation());
this._container.addEventListener('scroll', (e) => e.stopPropagation());
- this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));
this._container.style.width = '0px';
this._container.style.height = '0px';
+ this._fullscreenEventListeners = new EventListenerCollection();
+
this._updateVisibility();
}
// Public properties
+ get id() {
+ return this._id;
+ }
+
get parent() {
return this._parent;
}
+ get child() {
+ return this._child;
+ }
+
get depth() {
return this._depth;
}
@@ -117,16 +126,12 @@ class Popup {
}
clearAutoPlayTimer() {
- if (this._isInjectedAndLoaded) {
- this._invokeApi('clearAutoPlayTimer');
- }
+ this._invokeApi('clearAutoPlayTimer');
}
setContentScale(scale) {
this._contentScale = scale;
- if (this._isInjectedAndLoaded) {
- this._invokeApi('setContentScale', {scale});
- }
+ this._invokeApi('setContentScale', {scale});
}
// Popup-only public functions
@@ -146,7 +151,7 @@ class Popup {
}
isVisibleSync() {
- return this._isInjected && (this._visibleOverride !== null ? this._visibleOverride : this._visible);
+ return (this._visibleOverride !== null ? this._visibleOverride : this._visible);
}
updateTheme() {
@@ -154,21 +159,13 @@ class Popup {
this._container.dataset.yomichanSiteColor = this._getSiteColor();
}
- async setCustomOuterCss(css, injectDirectly) {
- // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them.
- if (this._stylesheetInjectedViaApi) { return; }
-
- if (injectDirectly || Popup._isOnExtensionPage()) {
- Popup.injectOuterStylesheet(css);
- } else {
- if (!css) { return; }
- try {
- await apiInjectStylesheet(css);
- this._stylesheetInjectedViaApi = true;
- } catch (e) {
- // NOP
- }
- }
+ async setCustomOuterCss(css, useWebExtensionApi) {
+ return await Popup._injectStylesheet(
+ 'yomichan-popup-outer-user-stylesheet',
+ 'code',
+ css,
+ useWebExtensionApi
+ );
}
setChildrenSupported(value) {
@@ -183,26 +180,6 @@ class Popup {
return this._container.getBoundingClientRect();
}
- static injectOuterStylesheet(css) {
- if (Popup.outerStylesheet === null) {
- if (!css) { return; }
- Popup.outerStylesheet = document.createElement('style');
- Popup.outerStylesheet.id = 'yomichan-popup-outer-stylesheet';
- }
-
- const outerStylesheet = Popup.outerStylesheet;
- if (css) {
- outerStylesheet.textContent = css;
-
- const par = document.head;
- if (par && outerStylesheet.parentNode !== par) {
- par.appendChild(outerStylesheet);
- }
- } else {
- outerStylesheet.textContent = '';
- }
- }
-
// Private functions
_inject() {
@@ -222,11 +199,18 @@ class Popup {
// NOP
}
+ if (this._messageToken === null) {
+ this._messageToken = await apiGetMessageToken();
+ }
+
return new Promise((resolve) => {
const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null);
+ this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));
this._container.addEventListener('load', () => {
- this._isInjectedAndLoaded = true;
- this._invokeApi('initialize', {
+ const uniqueId = yomichan.generateId(32);
+ Popup._listenForDisplayPrepareCompleted(uniqueId, resolve);
+
+ this._invokeApi('prepare', {
options: this._options,
popupInfo: {
id: this._id,
@@ -235,17 +219,60 @@ class Popup {
},
url: this.url,
childrenSupported: this._childrenSupported,
- scale: this._contentScale
+ scale: this._contentScale,
+ uniqueId
});
- resolve();
});
- this._observeFullscreen();
+ this._observeFullscreen(true);
this._onFullscreenChanged();
- this.setCustomOuterCss(this._options.general.customPopupOuterCss, false);
- this._isInjected = true;
+ this._injectStyles();
});
}
+ async _injectStyles() {
+ try {
+ await Popup._injectStylesheet('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true);
+ } catch (e) {
+ // NOP
+ }
+
+ try {
+ await this.setCustomOuterCss(this._options.general.customPopupOuterCss, true);
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ _observeFullscreen(observe) {
+ if (!observe) {
+ this._fullscreenEventListeners.removeAllEventListeners();
+ return;
+ }
+
+ if (this._fullscreenEventListeners.size > 0) {
+ // Already observing
+ return;
+ }
+
+ const fullscreenEvents = [
+ 'fullscreenchange',
+ 'MSFullscreenChange',
+ 'mozfullscreenchange',
+ 'webkitfullscreenchange'
+ ];
+ const onFullscreenChanged = () => this._onFullscreenChanged();
+ for (const eventName of fullscreenEvents) {
+ this._fullscreenEventListeners.addEventListener(document, eventName, onFullscreenChanged, false);
+ }
+ }
+
+ _onFullscreenChanged() {
+ const parent = (Popup._getFullscreenElement() || document.body || null);
+ if (parent !== null && this._container.parentNode !== parent) {
+ parent.appendChild(this._container);
+ }
+ }
+
async _show(elementRect, writingMode) {
await this._inject();
@@ -327,38 +354,38 @@ class Popup {
}
_invokeApi(action, params={}) {
- if (!this._isInjectedAndLoaded) {
- throw new Error('Frame not loaded');
- }
- this._container.contentWindow.postMessage({action, params}, '*');
- }
+ const token = this._messageToken;
+ const contentWindow = this._container.contentWindow;
+ if (token === null || contentWindow === null) { return; }
- _observeFullscreen() {
- const fullscreenEvents = [
- 'fullscreenchange',
- 'MSFullscreenChange',
- 'mozfullscreenchange',
- 'webkitfullscreenchange'
- ];
- for (const eventName of fullscreenEvents) {
- document.addEventListener(eventName, () => this._onFullscreenChanged(), false);
- }
+ contentWindow.postMessage({action, params, token}, this._targetOrigin);
}
- _getFullscreenElement() {
+ static _getFullscreenElement() {
return (
document.fullscreenElement ||
document.msFullscreenElement ||
document.mozFullScreenElement ||
- document.webkitFullscreenElement
+ document.webkitFullscreenElement ||
+ null
);
}
- _onFullscreenChanged() {
- const parent = (this._getFullscreenElement() || document.body || null);
- if (parent !== null && this._container.parentNode !== parent) {
- parent.appendChild(this._container);
- }
+ static _listenForDisplayPrepareCompleted(uniqueId, resolve) {
+ const runtimeMessageCallback = ({action, params}, sender, callback) => {
+ if (
+ action === 'popupPrepareCompleted' &&
+ typeof params === 'object' &&
+ params !== null &&
+ params.uniqueId === uniqueId
+ ) {
+ chrome.runtime.onMessage.removeListener(runtimeMessageCallback);
+ callback();
+ resolve();
+ return false;
+ }
+ };
+ chrome.runtime.onMessage.addListener(runtimeMessageCallback);
}
static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) {
@@ -492,15 +519,6 @@ class Popup {
];
}
- static _isOnExtensionPage() {
- try {
- const url = chrome.runtime.getURL('/');
- return window.location.href.substring(0, url.length) === url;
- } catch (e) {
- // NOP
- }
- }
-
static _getViewport(useVisualViewport) {
const visualViewport = window.visualViewport;
if (visualViewport !== null && typeof visualViewport === 'object') {
@@ -533,6 +551,80 @@ class Popup {
bottom: window.innerHeight
};
}
+
+ static _isOnExtensionPage() {
+ try {
+ const url = chrome.runtime.getURL('/');
+ return window.location.href.substring(0, url.length) === url;
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ static async _injectStylesheet(id, type, value, useWebExtensionApi) {
+ const injectedStylesheets = Popup._injectedStylesheets;
+
+ if (Popup._isOnExtensionPage()) {
+ // Permissions error will occur if trying to use the WebExtension API to inject
+ // into an extension page.
+ useWebExtensionApi = false;
+ }
+
+ let styleNode = injectedStylesheets.get(id);
+ if (typeof styleNode !== 'undefined') {
+ if (styleNode === null) {
+ // Previously injected via WebExtension API
+ throw new Error(`Stylesheet with id ${id} has already been injected using the WebExtension API`);
+ }
+ } else {
+ styleNode = null;
+ }
+
+ if (useWebExtensionApi) {
+ // Inject via WebExtension API
+ if (styleNode !== null && styleNode.parentNode !== null) {
+ styleNode.parentNode.removeChild(styleNode);
+ }
+
+ await apiInjectStylesheet(type, value);
+
+ injectedStylesheets.set(id, null);
+ return null;
+ }
+
+ // Create node in document
+ const parentNode = document.head;
+ if (parentNode === null) {
+ throw new Error('No parent node');
+ }
+
+ // Create or reuse node
+ const isFile = (type === 'file');
+ const tagName = isFile ? 'link' : 'style';
+ if (styleNode === null || styleNode.nodeName.toLowerCase() !== tagName) {
+ if (styleNode !== null && styleNode.parentNode !== null) {
+ styleNode.parentNode.removeChild(styleNode);
+ }
+ styleNode = document.createElement(tagName);
+ styleNode.id = id;
+ }
+
+ // Update node style
+ if (isFile) {
+ styleNode.rel = value;
+ } else {
+ styleNode.textContent = value;
+ }
+
+ // Update parent
+ if (styleNode.parentNode !== parentNode) {
+ parentNode.appendChild(styleNode);
+ }
+
+ // Add to map
+ injectedStylesheets.set(id, styleNode);
+ return styleNode;
+ }
}
-Popup.outerStylesheet = null;
+Popup._injectedStylesheets = new Map();
diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js
index 11d3ff0e..6dc482bd 100644
--- a/ext/fg/js/source.js
+++ b/ext/fg/js/source.js
@@ -82,7 +82,11 @@ class TextSourceRange {
}
equals(other) {
- if (other === null) {
+ if (!(
+ typeof other === 'object' &&
+ other !== null &&
+ other instanceof TextSourceRange
+ )) {
return false;
}
if (this.imposterSourceElement !== null) {
@@ -362,7 +366,7 @@ class TextSourceElement {
setEndOffset(length) {
switch (this.element.nodeName.toUpperCase()) {
case 'BUTTON':
- this.content = this.element.innerHTML;
+ this.content = this.element.textContent;
break;
case 'IMG':
this.content = this.element.getAttribute('alt');
@@ -409,6 +413,12 @@ class TextSourceElement {
}
equals(other) {
- return other && other.element === this.element && other.content === this.content;
+ return (
+ typeof other === 'object' &&
+ other !== null &&
+ other instanceof TextSourceElement &&
+ other.element === this.element &&
+ other.content === this.content
+ );
}
}
diff --git a/ext/manifest.json b/ext/manifest.json
index 31729992..fd9b6fec 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Yomichan (testing)",
- "version": "20.1.26.0",
+ "version": "20.2.24.0",
"description": "Japanese dictionary with Anki integration (testing)",
"icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"},
@@ -30,7 +30,7 @@
"fg/js/frontend.js",
"fg/js/frontend-initialize.js"
],
- "css": ["fg/css/client.css"],
+ "match_about_blank": true,
"all_frames": true
}],
"minimum_chrome_version": "57.0.0.0",
diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css
index 088fc741..c9cd9f90 100644
--- a/ext/mixed/css/display-dark.css
+++ b/ext/mixed/css/display-dark.css
@@ -38,6 +38,7 @@ body { background-color: #1e1e1e; color: #d4d4d4; }
.tag[data-category=dictionary] { background-color: #9057ad; }
.tag[data-category=frequency] { background-color: #489148; }
.tag[data-category=partOfSpeech] { background-color: #565656; }
+.tag[data-category=search] { background-color: #69696e; }
.term-reasons { color: #888888; }
diff --git a/ext/mixed/css/display-default.css b/ext/mixed/css/display-default.css
index 69141c9d..6eee43c4 100644
--- a/ext/mixed/css/display-default.css
+++ b/ext/mixed/css/display-default.css
@@ -38,6 +38,7 @@ body { background-color: #ffffff; color: #333333; }
.tag[data-category=dictionary] { background-color: #aa66cc; }
.tag[data-category=frequency] { background-color: #5cb85c; }
.tag[data-category=partOfSpeech] { background-color: #565656; }
+.tag[data-category=search] { background-color: #8a8a91; }
.term-reasons { color: #777777; }
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css
index add2583e..6a5383bc 100644
--- a/ext/mixed/css/display.css
+++ b/ext/mixed/css/display.css
@@ -127,15 +127,19 @@ html:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation
user-select: none;
}
-#query-parser {
+#query-parser-content {
margin-top: 0.5em;
font-size: 2em;
}
-#query-parser[data-term-spacing=true] .query-parser-term {
+#query-parser-content[data-term-spacing=true] .query-parser-term {
margin-right: 0.2em;
}
+html:root[data-yomichan-page=search][data-search-mode=popup] .search-input {
+ display: none;
+}
+
/*
* Entries
@@ -208,19 +212,27 @@ button.action-button {
}
.tag {
- display: inline;
+ display: inline-block;
padding: 0.2em 0.6em 0.3em;
font-size: 75%;
font-weight: 700;
- line-height: 1;
+ line-height: 1.25;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25em;
}
-.tag-list>.tag+.tag {
- margin-left: 0.375em;
+.tag-inner {
+ display: block;
+}
+
+.tag-list>.tag:not(:last-child) {
+ margin-right: 0.375em;
+}
+
+html:root:not([data-enable-search-tags=true]) .tag[data-category=search] {
+ display: none;
}
.entry-header2,
@@ -237,7 +249,7 @@ button.action-button {
border-top-style: solid;
}
-.entry[data-type=term][data-expression-multi=true]:not([data-expression-count="1"]) .actions>.action-play-audio {
+.entry[data-type=term][data-expression-multi=true] .actions>.action-play-audio {
display: none;
}
@@ -245,8 +257,9 @@ button.action-button {
display: inline-block;
}
-.term-reasons>.term-reason+.term-reason:before {
+.term-reasons>.term-reason+.term-reason-separator+.term-reason:before {
content: " \00AB "; /* The two spaces is not a typo */
+ white-space: pre-wrap;
display: inline;
}
@@ -284,13 +297,13 @@ button.action-button {
content: "\3001";
}
-.term-expression-list>.term-expression:last-of-type:not(:first-of-type):after {
+.term-expression-list[data-multi=true]>.term-expression:last-of-type:after {
font-size: 2em;
content: "\3000";
visibility: hidden;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details {
+.term-expression-list[data-multi=true] .term-expression-details {
display: inline-block;
position: relative;
width: 0;
@@ -298,21 +311,21 @@ button.action-button {
visibility: hidden;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression:hover .term-expression-details {
+.term-expression-list[data-multi=true] .term-expression:hover .term-expression-details {
visibility: visible;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details>.action-play-audio {
+.term-expression-list[data-multi=true] .term-expression-details>.action-play-audio {
position: absolute;
left: 0;
bottom: 0.5em;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details>.action-play-audio {
+.term-expression-list[data-multi=true] .term-expression-details>.action-play-audio {
display: block;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details>.tags {
+.term-expression-list[data-multi=true] .term-expression-details>.tags {
display: block;
position: absolute;
left: 0;
@@ -320,7 +333,7 @@ button.action-button {
white-space: nowrap;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details>.frequencies {
+.term-expression-list[data-multi=true] .term-expression-details>.frequencies {
display: block;
position: absolute;
left: 0;
@@ -385,7 +398,7 @@ button.action-button {
:root[data-compact-glossaries=true] .term-definition-tag-list,
:root[data-compact-glossaries=true] .term-definition-only-list:not([data-count="0"]) {
- display: inline-block;
+ display: inline;
}
:root[data-compact-glossaries=true] .term-glossary-list {
@@ -399,9 +412,24 @@ button.action-button {
}
:root[data-compact-glossaries=true] .term-glossary-list>li:not(:first-child):before {
+ white-space: pre-wrap;
content: " | ";
+ display: inline;
}
+.term-glossary-separator,
+.term-reason-separator {
+ display: inline;
+ font-size: 0;
+ opacity: 0;
+ white-space: pre-wrap;
+}
+
+.term-special-tags>.frequencies {
+ display: inline;
+}
+
+
/*
* Kanji
*/
diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html
index 62f3c69c..7ae51a62 100644
--- a/ext/mixed/display-templates.html
+++ b/ext/mixed/display-templates.html
@@ -15,7 +15,7 @@
</div>
<div class="term-reasons"></div>
</div>
- <div class="frequencies"></div>
+ <div class="term-special-tags"><div class="frequencies tag-list"></div></div>
</div>
<div class="term-definition-container"><ol class="term-definition-list"></ol></div>
<pre class="debug-info"></pre>
@@ -31,8 +31,8 @@
<ul class="term-glossary-list"></ul>
</li></template>
<template id="term-definition-only-template"><span class="term-definition-only"></span></template>
-<template id="term-glossary-item-template"><li class="term-glossary-item"><span class="term-glossary"></span></li></template>
-<template id="term-reason-template"><span class="term-reason"></span></template>
+<template id="term-glossary-item-template"><li class="term-glossary-item"><span class="term-glossary-separator"> </span><span class="term-glossary"></span></li></template>
+<template id="term-reason-template"><span class="term-reason"></span><span class="term-reason-separator"> </span></template>
<template id="kanji-entry-template"><div class="entry" data-type="kanji">
<div class="entry-header1">
@@ -75,7 +75,8 @@
<template id="kanji-glossary-item-template"><li class="kanji-glossary-item"><span class="kanji-glossary"></span></li></template>
<template id="kanji-reading-template"><dd class="kanji-reading"></dd></template>
-<template id="tag-template"><span class="tag"></span></template>
-<template id="tag-frequency-template"><span class="tag" data-category="frequency"><span class="term-frequency-dictionary-name"></span><span class="term-frequency-separator"></span><span class="term-frequency-value"></span></template>
+<template id="tag-template"><span class="tag"><span class="tag-inner"></span></span></template>
+<template id="tag-frequency-template"><span class="tag" data-category="frequency"><span class="tag-inner"><span class="term-frequency-dictionary-name"></span><span class="term-frequency-separator"></span><span class="term-frequency-value"></span></span></template>
+<template id="tag-search-template"><span class="tag" data-category="search"></span></template>
</body></html>
diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js
index 5ec93b01..7ea68d59 100644
--- a/ext/mixed/js/api.js
+++ b/ext/mixed/js/api.js
@@ -58,15 +58,15 @@ function apiDefinitionAdd(definition, mode, context, optionsContext) {
}
function apiDefinitionsAddable(definitions, modes, optionsContext) {
- return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext}).catch(() => null);
+ return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext});
}
function apiNoteView(noteId) {
return _apiInvoke('noteView', {noteId});
}
-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) {
@@ -89,8 +89,8 @@ function apiFrameInformationGet() {
return _apiInvoke('frameInformationGet');
}
-function apiInjectStylesheet(css) {
- return _apiInvoke('injectStylesheet', {css});
+function apiInjectStylesheet(type, value) {
+ return _apiInvoke('injectStylesheet', {type, value});
}
function apiGetEnvironmentInfo() {
@@ -105,10 +105,18 @@ function apiGetDisplayTemplatesHtml() {
return _apiInvoke('getDisplayTemplatesHtml');
}
+function apiGetQueryParserTemplatesHtml() {
+ return _apiInvoke('getQueryParserTemplatesHtml');
+}
+
function apiGetZoom() {
return _apiInvoke('getZoom');
}
+function apiGetMessageToken() {
+ return _apiInvoke('getMessageToken');
+}
+
function _apiInvoke(action, params={}) {
const data = {action, params};
return new Promise((resolve, reject) => {
diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js
index b0c5fa82..b5a025be 100644
--- a/ext/mixed/js/audio.js
+++ b/ext/mixed/js/audio.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiAudioGetUrl*/
class TextToSpeechAudio {
constructor(text, voice) {
@@ -53,7 +54,6 @@ class TextToSpeechAudio {
speechSynthesis.cancel();
speechSynthesis.speak(this._utterance);
-
} catch (e) {
// NOP
}
@@ -71,21 +71,16 @@ class TextToSpeechAudio {
const m = /^tts:[^#?]*\?([^#]*)/.exec(ttsUri);
if (m === null) { return null; }
- const searchParameters = {};
- for (const group of m[1].split('&')) {
- const sep = group.indexOf('=');
- if (sep < 0) { continue; }
- searchParameters[decodeURIComponent(group.substring(0, sep))] = decodeURIComponent(group.substring(sep + 1));
- }
-
- if (!searchParameters.text) { return null; }
+ const searchParameters = new URLSearchParams(m[1]);
+ const text = searchParameters.get('text');
+ let voice = searchParameters.get('voice');
+ if (text === null || voice === null) { return null; }
- const voice = audioGetTextToSpeechVoice(searchParameters.voice);
+ voice = audioGetTextToSpeechVoice(voice);
if (voice === null) { return null; }
- return new TextToSpeechAudio(searchParameters.text, voice);
+ return new TextToSpeechAudio(text, voice);
}
-
}
function audioGetFromUrl(url, willDownload) {
@@ -113,8 +108,11 @@ function audioGetFromUrl(url, willDownload) {
async function audioGetFromSources(expression, sources, optionsContext, willDownload, cache=null) {
const key = `${expression.expression}:${expression.reading}`;
- if (cache !== null && hasOwn(cache, expression)) {
- return cache[key];
+ if (cache !== null) {
+ const cacheValue = cache.get(expression);
+ if (typeof cacheValue !== 'undefined') {
+ return cacheValue;
+ }
}
for (let i = 0, ii = sources.length; i < ii; ++i) {
@@ -132,7 +130,7 @@ async function audioGetFromSources(expression, sources, optionsContext, willDown
}
const result = {audio, url, source};
if (cache !== null) {
- cache[key] = result;
+ cache.set(key, result);
}
return result;
} catch (e) {
diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js
index 0142d594..83813796 100644
--- a/ext/mixed/js/core.js
+++ b/ext/mixed/js/core.js
@@ -113,11 +113,7 @@ function toIterable(value) {
if (value !== null && typeof value === 'object') {
const length = value.length;
if (typeof length === 'number' && Number.isFinite(length)) {
- const array = [];
- for (let i = 0; i < length; ++i) {
- array.push(value[i]);
- }
- return array;
+ return Array.from(value);
}
}
@@ -128,6 +124,14 @@ function stringReverse(string) {
return string.split('').reverse().join('').replace(/([\uDC00-\uDFFF])([\uD800-\uDBFF])/g, '$2$1');
}
+function parseUrl(url) {
+ const parsedUrl = new URL(url);
+ const baseUrl = `${parsedUrl.origin}${parsedUrl.pathname}`;
+ const queryParams = Array.from(parsedUrl.searchParams.entries())
+ .reduce((a, [k, v]) => Object.assign({}, a, {[k]: v}), {});
+ return {baseUrl, queryParams};
+}
+
/*
* Async utilities
@@ -156,9 +160,9 @@ function promiseTimeout(delay, resolveValue) {
const resolve = (value) => complete(promiseResolve, value);
const reject = (value) => complete(promiseReject, value);
- const promise = new Promise((resolve, reject) => {
- promiseResolve = resolve;
- promiseReject = reject;
+ const promise = new Promise((resolve2, reject2) => {
+ promiseResolve = resolve2;
+ promiseReject = reject2;
});
timer = window.setTimeout(() => {
timer = null;
@@ -232,6 +236,29 @@ class EventDispatcher {
}
}
+class EventListenerCollection {
+ constructor() {
+ this._eventListeners = [];
+ }
+
+ get size() {
+ return this._eventListeners.length;
+ }
+
+ addEventListener(node, type, listener, options) {
+ node.addEventListener(type, listener, options);
+ this._eventListeners.push([node, type, listener, options]);
+ }
+
+ removeAllEventListeners() {
+ if (this._eventListeners.length === 0) { return; }
+ for (const [node, type, listener, options] of this._eventListeners) {
+ node.removeEventListener(type, listener, options);
+ }
+ this._eventListeners = [];
+ }
+}
+
/*
* Default message handlers
@@ -244,7 +271,7 @@ const yomichan = (() => {
this._messageHandlers = new Map([
['getUrl', this._onMessageGetUrl.bind(this)],
- ['optionsUpdate', this._onMessageOptionsUpdate.bind(this)],
+ ['optionsUpdated', this._onMessageOptionsUpdated.bind(this)],
['zoomChanged', this._onMessageZoomChanged.bind(this)]
]);
@@ -253,6 +280,16 @@ const yomichan = (() => {
// Public
+ generateId(length) {
+ const array = new Uint8Array(length);
+ window.crypto.getRandomValues(array);
+ let id = '';
+ for (const value of array) {
+ id += value.toString(16).padStart(2, '0');
+ }
+ return id;
+ }
+
triggerOrphaned(error) {
this.trigger('orphaned', {error});
}
@@ -272,8 +309,8 @@ const yomichan = (() => {
return {url: window.location.href};
}
- _onMessageOptionsUpdate({source}) {
- this.trigger('optionsUpdate', {source});
+ _onMessageOptionsUpdated({source}) {
+ this.trigger('optionsUpdated', {source});
}
_onMessageZoomChanged({oldZoomFactor, newZoomFactor}) {
diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js
index e1710488..d7e77cc0 100644
--- a/ext/mixed/js/display-generator.js
+++ b/ext/mixed/js/display-generator.js
@@ -16,46 +16,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+/*global apiGetDisplayTemplatesHtml, TemplateHandler*/
class DisplayGenerator {
constructor() {
- this._isInitialized = false;
- this._initializationPromise = null;
-
- this._termEntryTemplate = null;
- this._termExpressionTemplate = null;
- this._termDefinitionItemTemplate = null;
- this._termDefinitionOnlyTemplate = null;
- this._termGlossaryItemTemplate = null;
- this._termReasonTemplate = null;
-
- this._kanjiEntryTemplate = null;
- this._kanjiInfoTableTemplate = null;
- this._kanjiInfoTableItemTemplate = null;
- this._kanjiInfoTableEmptyTemplate = null;
- this._kanjiGlossaryItemTemplate = null;
- this._kanjiReadingTemplate = null;
-
- this._tagTemplate = null;
- this._tagFrequencyTemplate = null;
+ this._templateHandler = null;
}
- isInitialized() {
- return this._isInitialized;
- }
-
- initialize() {
- if (this._isInitialized) {
- return Promise.resolve();
- }
- if (this._initializationPromise === null) {
- this._initializationPromise = this._initializeInternal();
- }
- return this._initializationPromise;
+ async prepare() {
+ const html = await apiGetDisplayTemplatesHtml();
+ this._templateHandler = new TemplateHandler(html);
}
createTermEntry(details) {
- const node = DisplayGenerator._instantiateTemplate(this._termEntryTemplate);
+ const node = this._templateHandler.instantiate('term-entry');
const expressionsContainer = node.querySelector('.term-expression-list');
const reasonsContainer = node.querySelector('.term-reasons');
@@ -71,7 +45,11 @@ class DisplayGenerator {
node.dataset.expressionCount = `${expressionMulti ? details.expressions.length : 1}`;
node.dataset.definitionCount = `${definitionMulti ? details.definitions.length : 1}`;
- DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), details.expressions, [details]);
+ const termTags = details.termTags;
+ let expressions = details.expressions;
+ expressions = Array.isArray(expressions) ? expressions.map((e) => [e, termTags]) : null;
+
+ DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), expressions, [[details, termTags]]);
DisplayGenerator._appendMultiple(reasonsContainer, this.createTermReason.bind(this), details.reasons);
DisplayGenerator._appendMultiple(frequenciesContainer, this.createFrequencyTag.bind(this), details.frequencies);
DisplayGenerator._appendMultiple(definitionsContainer, this.createTermDefinitionItem.bind(this), details.definitions, [details]);
@@ -83,8 +61,8 @@ class DisplayGenerator {
return node;
}
- createTermExpression(details) {
- const node = DisplayGenerator._instantiateTemplate(this._termExpressionTemplate);
+ createTermExpression([details, termTags]) {
+ const node = this._templateHandler.instantiate('term-expression');
const expressionContainer = node.querySelector('.term-expression-text');
const tagContainer = node.querySelector('.tags');
@@ -103,21 +81,30 @@ class DisplayGenerator {
DisplayGenerator._appendFurigana(expressionContainer, furiganaSegments, this._appendKanjiLinks.bind(this));
}
- DisplayGenerator._appendMultiple(tagContainer, this.createTag.bind(this), details.termTags);
+ if (!Array.isArray(termTags)) {
+ // Fallback
+ termTags = details.termTags;
+ }
+ const searchQueries = [details.expression, details.reading]
+ .filter((x) => !!x)
+ .map((x) => ({query: x}));
+ DisplayGenerator._appendMultiple(tagContainer, this.createTag.bind(this), termTags);
+ DisplayGenerator._appendMultiple(tagContainer, this.createSearchTag.bind(this), searchQueries);
DisplayGenerator._appendMultiple(frequencyContainer, this.createFrequencyTag.bind(this), details.frequencies);
return node;
}
createTermReason(reason) {
- const node = DisplayGenerator._instantiateTemplate(this._termReasonTemplate);
+ const fragment = this._templateHandler.instantiateFragment('term-reason');
+ const node = fragment.querySelector('.term-reason');
node.textContent = reason;
node.dataset.reason = reason;
- return node;
+ return fragment;
}
createTermDefinitionItem(details) {
- const node = DisplayGenerator._instantiateTemplate(this._termDefinitionItemTemplate);
+ const node = this._templateHandler.instantiate('term-definition-item');
const tagListContainer = node.querySelector('.term-definition-tag-list');
const onlyListContainer = node.querySelector('.term-definition-only-list');
@@ -133,7 +120,7 @@ class DisplayGenerator {
}
createTermGlossaryItem(glossary) {
- const node = DisplayGenerator._instantiateTemplate(this._termGlossaryItemTemplate);
+ const node = this._templateHandler.instantiate('term-glossary-item');
const container = node.querySelector('.term-glossary');
if (container !== null) {
DisplayGenerator._appendMultilineText(container, glossary);
@@ -142,7 +129,7 @@ class DisplayGenerator {
}
createTermOnly(only) {
- const node = DisplayGenerator._instantiateTemplate(this._termDefinitionOnlyTemplate);
+ const node = this._templateHandler.instantiate('term-definition-only');
node.dataset.only = only;
node.textContent = only;
return node;
@@ -157,7 +144,7 @@ class DisplayGenerator {
}
createKanjiEntry(details) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiEntryTemplate);
+ const node = this._templateHandler.instantiate('kanji-entry');
const glyphContainer = node.querySelector('.kanji-glyph');
const frequenciesContainer = node.querySelector('.frequencies');
@@ -202,7 +189,7 @@ class DisplayGenerator {
}
createKanjiGlossaryItem(glossary) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiGlossaryItemTemplate);
+ const node = this._templateHandler.instantiate('kanji-glossary-item');
const container = node.querySelector('.kanji-glossary');
if (container !== null) {
DisplayGenerator._appendMultilineText(container, glossary);
@@ -211,13 +198,13 @@ class DisplayGenerator {
}
createKanjiReading(reading) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiReadingTemplate);
+ const node = this._templateHandler.instantiate('kanji-reading');
node.textContent = reading;
return node;
}
createKanjiInfoTable(details) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiInfoTableTemplate);
+ const node = this._templateHandler.instantiate('kanji-info-table');
const container = node.querySelector('.kanji-info-table-body');
@@ -233,7 +220,7 @@ class DisplayGenerator {
}
createKanjiInfoTableItem(details) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiInfoTableItemTemplate);
+ const node = this._templateHandler.instantiate('kanji-info-table-item');
const nameNode = node.querySelector('.kanji-info-table-item-header');
const valueNode = node.querySelector('.kanji-info-table-item-value');
if (nameNode !== null) {
@@ -246,21 +233,33 @@ class DisplayGenerator {
}
createKanjiInfoTableItemEmpty() {
- return DisplayGenerator._instantiateTemplate(this._kanjiInfoTableEmptyTemplate);
+ return this._templateHandler.instantiate('kanji-info-table-empty');
}
createTag(details) {
- const node = DisplayGenerator._instantiateTemplate(this._tagTemplate);
+ const node = this._templateHandler.instantiate('tag');
+
+ const inner = node.querySelector('.tag-inner');
node.title = details.notes;
- node.textContent = details.name;
+ inner.textContent = details.name;
node.dataset.category = details.category;
return node;
}
+ createSearchTag(details) {
+ const node = this._templateHandler.instantiate('tag-search');
+
+ node.textContent = details.query;
+
+ node.dataset.query = details.query;
+
+ return node;
+ }
+
createFrequencyTag(details) {
- const node = DisplayGenerator._instantiateTemplate(this._tagFrequencyTemplate);
+ const node = this._templateHandler.instantiate('tag-frequency');
let n = node.querySelector('.term-frequency-dictionary-name');
if (n !== null) {
@@ -278,31 +277,6 @@ class DisplayGenerator {
return node;
}
- async _initializeInternal() {
- const html = await apiGetDisplayTemplatesHtml();
- const doc = new DOMParser().parseFromString(html, 'text/html');
- this._setTemplates(doc);
- }
-
- _setTemplates(doc) {
- this._termEntryTemplate = doc.querySelector('#term-entry-template');
- this._termExpressionTemplate = doc.querySelector('#term-expression-template');
- this._termDefinitionItemTemplate = doc.querySelector('#term-definition-item-template');
- this._termDefinitionOnlyTemplate = doc.querySelector('#term-definition-only-template');
- this._termGlossaryItemTemplate = doc.querySelector('#term-glossary-item-template');
- this._termReasonTemplate = doc.querySelector('#term-reason-template');
-
- this._kanjiEntryTemplate = doc.querySelector('#kanji-entry-template');
- this._kanjiInfoTableTemplate = doc.querySelector('#kanji-info-table-template');
- this._kanjiInfoTableItemTemplate = doc.querySelector('#kanji-info-table-item-template');
- this._kanjiInfoTableEmptyTemplate = doc.querySelector('#kanji-info-table-empty-template');
- this._kanjiGlossaryItemTemplate = doc.querySelector('#kanji-glossary-item-template');
- this._kanjiReadingTemplate = doc.querySelector('#kanji-reading-template');
-
- this._tagTemplate = doc.querySelector('#tag-template');
- this._tagFrequencyTemplate = doc.querySelector('#tag-frequency-template');
- }
-
_appendKanjiLinks(container, text) {
let part = '';
for (const c of text) {
@@ -372,8 +346,4 @@ class DisplayGenerator {
container.appendChild(document.createTextNode(parts[i]));
}
}
-
- static _instantiateTemplate(template) {
- return document.importNode(template.content.firstChild, true);
- }
}
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index c4be02f2..5d3076ee 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -16,6 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global docRangeFromPoint, docSentenceExtract
+apiKanjiFind, apiTermsFind, apiNoteView, apiOptionsGet, apiDefinitionsAddable, apiDefinitionAdd
+apiScreenshotGet, apiForward
+audioPrepareTextToSpeech, audioGetFromSources
+DisplayGenerator, WindowScroll, DisplayContext, DOM*/
class Display {
constructor(spinner, container) {
@@ -27,11 +32,11 @@ class Display {
this.index = 0;
this.audioPlaying = null;
this.audioFallback = null;
- this.audioCache = {};
+ this.audioCache = new Map();
this.styleNode = null;
- this.eventListeners = [];
- this.persistentEventListeners = [];
+ this.eventListeners = new EventListenerCollection();
+ this.persistentEventListeners = new EventListenerCollection();
this.interactive = false;
this.eventListenersActive = false;
this.clickScanPrevent = false;
@@ -43,6 +48,13 @@ class Display {
this.setInteractive(true);
}
+ async prepare(options=null) {
+ const displayGeneratorPromise = this.displayGenerator.prepare();
+ const updateOptionsPromise = this.updateOptions(options);
+ await Promise.all([displayGeneratorPromise, updateOptionsPromise]);
+ yomichan.on('optionsUpdated', () => this.updateOptions(null));
+ }
+
onError(_error) {
throw new Error('Override me');
}
@@ -174,15 +186,24 @@ class Display {
e.preventDefault();
const link = e.currentTarget;
const entry = link.closest('.entry');
- const definitionIndex = this.entryIndexFind(entry);
+ const index = this.entryIndexFind(entry);
+ if (index < 0 || index >= this.definitions.length) { return; }
+
const expressionIndex = Display.indexOf(entry.querySelectorAll('.term-expression .action-play-audio'), link);
- this.audioPlay(this.definitions[definitionIndex], expressionIndex, definitionIndex);
+ this.audioPlay(
+ this.definitions[index],
+ // expressionIndex is used in audioPlay to detect result output mode
+ Math.max(expressionIndex, this.options.general.resultOutputMode === 'merge' ? 0 : -1),
+ index
+ );
}
onNoteAdd(e) {
e.preventDefault();
const link = e.currentTarget;
const index = this.entryIndexFind(link);
+ if (index < 0 || index >= this.definitions.length) { return; }
+
this.noteAdd(this.definitions[index], link.dataset.mode);
}
@@ -216,13 +237,16 @@ class Display {
}
onHistoryWheel(e) {
+ if (e.altKey) { return; }
const delta = -e.deltaX || e.deltaY;
if (delta > 0) {
this.sourceTermView();
e.preventDefault();
+ e.stopPropagation();
} else if (delta < 0) {
this.nextTermView();
e.preventDefault();
+ e.stopPropagation();
}
}
@@ -230,15 +254,6 @@ class Display {
throw new Error('Override me');
}
- isInitialized() {
- return this.options !== null;
- }
-
- async initialize(options=null) {
- await this.updateOptions(options);
- yomichan.on('optionsUpdate', () => this.updateOptions(null));
- }
-
async updateOptions(options) {
this.options = options ? options : await apiOptionsGet(this.getOptionsContext());
this.updateDocumentOptions(this.options);
@@ -252,6 +267,7 @@ class Display {
data.ankiEnabled = `${options.anki.enable}`;
data.audioEnabled = `${options.audio.enable}`;
data.compactGlossaries = `${options.general.compactGlossaries}`;
+ data.enableSearchTags = `${options.scanning.enableSearchTags}`;
data.debug = `${options.general.debugInfo}`;
}
@@ -285,13 +301,24 @@ class Display {
this.interactive = interactive;
if (interactive) {
- Display.addEventListener(this.persistentEventListeners, document, 'keydown', this.onKeyDown.bind(this), false);
- Display.addEventListener(this.persistentEventListeners, document, 'wheel', this.onWheel.bind(this), {passive: false});
- Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-previous'), 'click', this.onSourceTermView.bind(this));
- Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-next'), 'click', this.onNextTermView.bind(this));
- Display.addEventListener(this.persistentEventListeners, document.querySelector('.navigation-header'), 'wheel', this.onHistoryWheel.bind(this), {passive: false});
+ const actionPrevious = document.querySelector('.action-previous');
+ const actionNext = document.querySelector('.action-next');
+ // const navigationHeader = document.querySelector('.navigation-header');
+
+ this.persistentEventListeners.addEventListener(document, 'keydown', this.onKeyDown.bind(this), false);
+ this.persistentEventListeners.addEventListener(document, 'wheel', this.onWheel.bind(this), {passive: false});
+ if (actionPrevious !== null) {
+ this.persistentEventListeners.addEventListener(actionPrevious, 'click', this.onSourceTermView.bind(this));
+ }
+ if (actionNext !== null) {
+ this.persistentEventListeners.addEventListener(actionNext, 'click', this.onNextTermView.bind(this));
+ }
+ // temporarily disabled
+ // if (navigationHeader !== null) {
+ // this.persistentEventListeners.addEventListener(navigationHeader, 'wheel', this.onHistoryWheel.bind(this), {passive: false});
+ // }
} else {
- Display.clearEventListeners(this.persistentEventListeners);
+ this.persistentEventListeners.removeAllEventListeners();
}
this.setEventListenersActive(this.eventListenersActive);
}
@@ -302,23 +329,23 @@ class Display {
this.eventListenersActive = active;
if (active) {
- this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
- this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
- this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
- this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
+ this.addMultipleEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
+ this.addMultipleEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
+ this.addMultipleEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
+ this.addMultipleEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
if (this.options.scanning.enablePopupSearch) {
- this.addEventListeners('.glossary-item', 'mouseup', this.onGlossaryMouseUp.bind(this));
- this.addEventListeners('.glossary-item', 'mousedown', this.onGlossaryMouseDown.bind(this));
- this.addEventListeners('.glossary-item', 'mousemove', this.onGlossaryMouseMove.bind(this));
+ this.addMultipleEventListeners('.term-glossary-item, .tag', 'mouseup', this.onGlossaryMouseUp.bind(this));
+ this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousedown', this.onGlossaryMouseDown.bind(this));
+ this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousemove', this.onGlossaryMouseMove.bind(this));
}
} else {
- Display.clearEventListeners(this.eventListeners);
+ this.eventListeners.removeAllEventListeners();
}
}
- addEventListeners(selector, type, listener, options) {
+ addMultipleEventListeners(selector, type, listener, options) {
for (const node of this.container.querySelectorAll(selector)) {
- Display.addEventListener(this.eventListeners, node, type, listener, options);
+ this.eventListeners.addEventListener(node, type, listener, options);
}
}
@@ -348,7 +375,6 @@ class Display {
async setContentTerms(definitions, context, token) {
if (!context) { throw new Error('Context expected'); }
- if (!this.isInitialized()) { return; }
this.setEventListenersActive(false);
@@ -356,11 +382,6 @@ class Display {
window.focus();
}
- if (!this.displayGenerator.isInitialized()) {
- await this.displayGenerator.initialize();
- if (this.setContentToken !== token) { return; }
- }
-
this.definitions = definitions;
if (context.disableHistory) {
delete context.disableHistory;
@@ -404,7 +425,7 @@ class Display {
this.setEventListenersActive(true);
- const states = await apiDefinitionsAddable(definitions, ['term-kanji', 'term-kana'], this.getOptionsContext());
+ const states = await this.getDefinitionsAddable(definitions, ['term-kanji', 'term-kana']);
if (this.setContentToken !== token) { return; }
this.updateAdderButtons(states);
@@ -412,7 +433,6 @@ class Display {
async setContentKanji(definitions, context, token) {
if (!context) { throw new Error('Context expected'); }
- if (!this.isInitialized()) { return; }
this.setEventListenersActive(false);
@@ -420,11 +440,6 @@ class Display {
window.focus();
}
- if (!this.displayGenerator.isInitialized()) {
- await this.displayGenerator.initialize();
- if (this.setContentToken !== token) { return; }
- }
-
this.definitions = definitions;
if (context.disableHistory) {
delete context.disableHistory;
@@ -446,7 +461,7 @@ class Display {
for (let i = 0, ii = definitions.length; i < ii; ++i) {
if (i > 0) {
- await promiseTimeout(0);
+ await promiseTimeout(1);
if (this.setContentToken !== token) { return; }
}
@@ -459,7 +474,7 @@ class Display {
this.setEventListenersActive(true);
- const states = await apiDefinitionsAddable(definitions, ['kanji'], this.getOptionsContext());
+ const states = await this.getDefinitionsAddable(definitions, ['kanji']);
if (this.setContentToken !== token) { return; }
this.updateAdderButtons(states);
@@ -498,6 +513,8 @@ class Display {
}
autoPlayAudio() {
+ if (this.definitions.length === 0) { return; }
+
this.audioPlay(this.definitions[0], this.firstExpressionIndex, 0);
}
@@ -597,9 +614,12 @@ class Display {
}
noteTryAdd(mode) {
- const button = this.adderButtonFind(this.index, mode);
+ const index = this.index;
+ if (index < 0 || index >= this.definitions.length) { return; }
+
+ const button = this.adderButtonFind(index, mode);
if (button !== null && !button.classList.contains('disabled')) {
- this.noteAdd(this.definitions[this.index], mode);
+ this.noteAdd(this.definitions[index], mode);
}
}
@@ -698,7 +718,7 @@ class Display {
async getScreenshot() {
try {
await this.setPopupVisibleOverride(false);
- await Display.delay(1); // Wait for popup to be hidden.
+ await promiseTimeout(1); // Wait for popup to be hidden.
const {format, quality} = this.options.anki.screenshot;
const dataUrl = await apiScreenshotGet({format, quality});
@@ -767,8 +787,12 @@ class Display {
return entry !== null ? entry.querySelector('.action-play-audio>img') : null;
}
- static delay(time) {
- return new Promise((resolve) => setTimeout(resolve, time));
+ async getDefinitionsAddable(definitions, modes) {
+ try {
+ return await apiDefinitionsAddable(definitions, modes, this.getOptionsContext());
+ } catch (e) {
+ return [];
+ }
}
static indexOf(nodeList, node) {
@@ -780,19 +804,6 @@ class Display {
return -1;
}
- static addEventListener(eventListeners, object, type, listener, options) {
- if (object === null) { return; }
- object.addEventListener(type, listener, options);
- eventListeners.push([object, type, listener, options]);
- }
-
- static clearEventListeners(eventListeners) {
- for (const [object, type, listener, options] of eventListeners) {
- object.removeEventListener(type, listener, options);
- }
- eventListeners.length = 0;
- }
-
static getElementTop(element) {
const elementRect = element.getBoundingClientRect();
const documentRect = document.documentElement.getBoundingClientRect();
@@ -901,9 +912,12 @@ Display._onKeyDownHandlers = new Map([
['P', (self, e) => {
if (e.altKey) {
- const entry = self.getEntry(self.index);
+ const index = self.index;
+ if (index < 0 || index >= self.definitions.length) { return; }
+
+ const entry = self.getEntry(index);
if (entry !== null && entry.dataset.type === 'term') {
- self.audioPlay(self.definitions[self.index], self.firstExpressionIndex, self.index);
+ self.audioPlay(self.definitions[index], self.firstExpressionIndex, index);
}
return true;
}
diff --git a/ext/mixed/js/template-handler.js b/ext/mixed/js/template-handler.js
new file mode 100644
index 00000000..a5a62937
--- /dev/null
+++ b/ext/mixed/js/template-handler.js
@@ -0,0 +1,47 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+
+class TemplateHandler {
+ constructor(html) {
+ this._templates = new Map();
+
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ for (const template of doc.querySelectorAll('template')) {
+ this._setTemplate(template);
+ }
+ }
+
+ _setTemplate(template) {
+ const idMatch = template.id.match(/^([a-z-]+)-template$/);
+ if (!idMatch) {
+ throw new Error(`Invalid template ID: ${template.id}`);
+ }
+ this._templates.set(idMatch[1], template);
+ }
+
+ instantiate(name) {
+ const template = this._templates.get(name);
+ return document.importNode(template.content.firstChild, true);
+ }
+
+ instantiateFragment(name) {
+ const template = this._templates.get(name);
+ return document.importNode(template.content, true);
+ }
+}
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index 88f1e27a..ff0eac8b 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global docRangeFromPoint, TextSourceRange, DOM*/
class TextScanner {
constructor(node, ignoreNodes, ignoreElements, ignorePoints) {
@@ -30,7 +31,7 @@ class TextScanner {
this.options = null;
this.enabled = false;
- this.eventListeners = [];
+ this.eventListeners = new EventListenerCollection();
this.primaryTouchIdentifier = null;
this.preventNextContextMenu = false;
@@ -140,24 +141,24 @@ class TextScanner {
const textSourceCurrentPrevious = this.textSourceCurrent !== null ? this.textSourceCurrent.clone() : null;
this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchStart')
- .then(() => {
- if (
- this.textSourceCurrent === null ||
- this.textSourceCurrent.equals(textSourceCurrentPrevious)
- ) {
- return;
- }
+ .then(() => {
+ if (
+ this.textSourceCurrent === null ||
+ this.textSourceCurrent.equals(textSourceCurrentPrevious)
+ ) {
+ return;
+ }
- this.preventScroll = true;
- this.preventNextContextMenu = true;
- this.preventNextMouseDown = true;
- });
+ this.preventScroll = true;
+ this.preventNextContextMenu = true;
+ this.preventNextMouseDown = true;
+ });
}
onTouchEnd(e) {
if (
this.primaryTouchIdentifier === null ||
- TextScanner.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0
+ TextScanner.getTouch(e.changedTouches, this.primaryTouchIdentifier) === null
) {
return;
}
@@ -180,13 +181,11 @@ class TextScanner {
return;
}
- const touches = e.changedTouches;
- const index = TextScanner.getIndexOfTouch(touches, this.primaryTouchIdentifier);
- if (index < 0) {
+ const primaryTouch = TextScanner.getTouch(e.changedTouches, this.primaryTouchIdentifier);
+ if (primaryTouch === null) {
return;
}
- const primaryTouch = touches[index];
this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchMove');
e.preventDefault(); // Disable scroll
@@ -228,7 +227,7 @@ class TextScanner {
}
} else {
if (this.enabled) {
- this.clearEventListeners();
+ this.eventListeners.removeAllEventListeners();
this.enabled = false;
}
this.onSearchClear(false);
@@ -236,13 +235,13 @@ class TextScanner {
}
hookEvents() {
- let eventListeners = this.getMouseEventListeners();
+ let eventListenerInfos = this.getMouseEventListeners();
if (this.options.scanning.touchInputEnabled) {
- eventListeners = eventListeners.concat(this.getTouchEventListeners());
+ eventListenerInfos = eventListenerInfos.concat(this.getTouchEventListeners());
}
- for (const [node, type, listener, options] of eventListeners) {
- this.addEventListener(node, type, listener, options);
+ for (const [node, type, listener, options] of eventListenerInfos) {
+ this.eventListeners.addEventListener(node, type, listener, options);
}
}
@@ -267,18 +266,6 @@ class TextScanner {
];
}
- 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 = [];
- }
-
setOptions(options) {
this.options = options;
this.setEnabled(this.options.general.enable);
@@ -367,13 +354,12 @@ class TextScanner {
}
}
- static getIndexOfTouch(touchList, identifier) {
- for (const i in touchList) {
- const t = touchList[i];
- if (t.identifier === identifier) {
- return i;
+ static getTouch(touchList, identifier) {
+ for (const touch of touchList) {
+ if (touch.identifier === identifier) {
+ return touch;
}
}
- return -1;
+ return null;
}
}