summaryrefslogtreecommitdiff
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
parent0c5b9b1fa1599cbf769d96cdebc226310f9dd8bc (diff)
parent706c3edcffb0078d71fd5b58775f16cf5fc1205b (diff)
Merge branch 'master' into testing
-rw-r--r--.eslintrc.json101
-rw-r--r--.github/workflows/ci.yml27
-rw-r--r--.gitignore1
-rw-r--r--README.md10
-rwxr-xr-xbuild_tmpl.sh2
-rwxr-xr-xbuild_tmpl_auto.sh16
-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
-rw-r--r--package-lock.json1119
-rw-r--r--package.json34
-rw-r--r--test/data/dictionaries/invalid-dictionary1/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary2/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary2/kanji_bank_1.json3
-rw-r--r--test/data/dictionaries/invalid-dictionary3/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary3/kanji_meta_bank_1.json1
-rw-r--r--test/data/dictionaries/invalid-dictionary4/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary4/tag_bank_1.json3
-rw-r--r--test/data/dictionaries/invalid-dictionary5/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary5/term_bank_1.json3
-rw-r--r--test/data/dictionaries/invalid-dictionary6/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary6/term_meta_bank_1.json1
-rw-r--r--test/data/dictionaries/valid-dictionary1/index.json6
-rw-r--r--test/data/dictionaries/valid-dictionary1/kanji_bank_1.json42
-rw-r--r--test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json4
-rw-r--r--test/data/dictionaries/valid-dictionary1/tag_bank_1.json7
-rw-r--r--test/data/dictionaries/valid-dictionary1/tag_bank_2.json9
-rw-r--r--test/data/dictionaries/valid-dictionary1/term_bank_1.json34
-rw-r--r--test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json5
-rw-r--r--test/dictionary-validate.js113
-rw-r--r--test/schema-validate.js52
-rw-r--r--test/test-database.js935
-rw-r--r--test/test-dictionary.js59
-rw-r--r--test/test-schema.js251
-rw-r--r--test/yomichan-test.js70
-rw-r--r--tmpl/query-parser.html27
103 files changed, 5415 insertions, 1477 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
index 0e3b939a..fcc6995b 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,8 +1,16 @@
{
+ "root": true,
"extends": "eslint:recommended",
"parserOptions": {
- "ecmaVersion": 8
+ "ecmaVersion": 8,
+ "sourceType": "script"
},
+ "env": {
+ "browser": true,
+ "es2017": true,
+ "webextensions": true
+ },
+ "plugins": ["no-unsanitized"],
"ignorePatterns": [
"/ext/mixed/lib/",
"/ext/bg/js/templates.js"
@@ -13,16 +21,101 @@
"curly": ["error", "all"],
"dot-notation": "error",
"eqeqeq": "error",
+ "func-names": ["error", "always"],
"no-case-declarations": "error",
"no-const-assign": "error",
"no-constant-condition": "off",
- "no-undef": "off",
+ "no-global-assign": "error",
+ "no-param-reassign": "off",
+ "no-prototype-builtins": "error",
+ "no-shadow": ["error", {"builtinGlobals": false}],
+ "no-undef": "error",
+ "no-unneeded-ternary": "error",
"no-unused-vars": ["error", {"vars": "local", "args": "after-used", "argsIgnorePattern": "^_", "caughtErrors": "none"}],
+ "no-unused-expressions": "error",
"no-var": "error",
"prefer-const": ["error", {"destructuring": "all"}],
"quote-props": ["error", "consistent"],
"quotes": ["error", "single", "avoid-escape"],
"require-atomic-updates": "off",
- "semi": "error"
- }
+ "semi": "error",
+
+ // Whitespace rules
+ "brace-style": ["error", "1tbs", {"allowSingleLine": true}],
+ "indent": ["error", 4, {"SwitchCase": 1, "MemberExpression": 1, "flatTernaryExpressions": true, "ignoredNodes": ["ConditionalExpression"]}],
+ "object-curly-newline": "error",
+ "padded-blocks": ["error", "never"],
+
+ "array-bracket-spacing": ["error", "never"],
+ "arrow-spacing": ["error", {"before": true, "after": true}],
+ "block-spacing": ["error", "always"],
+ "comma-spacing": ["error", { "before": false, "after": true }],
+ "computed-property-spacing": ["error", "never"],
+ "func-call-spacing": ["error", "never"],
+ "generator-star-spacing": ["error", "before"],
+ "key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}],
+ "keyword-spacing": ["error", {"before": true, "after": true}],
+ "no-trailing-spaces": "error",
+ "no-whitespace-before-property": "error",
+ "object-curly-spacing": ["error", "never"],
+ "rest-spread-spacing": ["error", "never"],
+ "semi-spacing": ["error", {"before": false, "after": true}],
+ "space-in-parens": ["error", "never"],
+ "space-unary-ops": "error",
+ "spaced-comment": ["error", "always", {"markers": ["global"]}],
+ "switch-colon-spacing": ["error", {"after": true, "before": false}],
+ "template-curly-spacing": ["error", "never"],
+ "template-tag-spacing": ["error", "never"],
+
+ // Extensions
+ "no-unsanitized/method": "error",
+ "no-unsanitized/property": "error"
+ },
+ "overrides": [
+ {
+ "files": ["*.js"],
+ "excludedFiles": ["ext/mixed/js/core.js"],
+ "globals": {
+ "yomichan": "readonly",
+ "errorToJson": "readonly",
+ "jsonToError": "readonly",
+ "logError": "readonly",
+ "isObject": "readonly",
+ "hasOwn": "readonly",
+ "toIterable": "readonly",
+ "stringReverse": "readonly",
+ "promiseTimeout": "readonly",
+ "stringReplaceAsync": "readonly",
+ "parseUrl": "readonly",
+ "EventDispatcher": "readonly",
+ "EventListenerCollection": "readonly",
+ "EXTENSION_IS_BROWSER_EDGE": "readonly"
+ }
+ },
+ {
+ "files": ["ext/mixed/js/core.js"],
+ "globals": {
+ "chrome": "writable"
+ }
+ },
+ {
+ "files": ["ext/bg/js/settings/*.js"],
+ "env": {
+ "jquery": true
+ }
+ },
+ {
+ "files": ["test/**/*.js"],
+ "parserOptions": {
+ "ecmaVersion": 8,
+ "sourceType": "module"
+ },
+ "env": {
+ "browser": false,
+ "es2017": true,
+ "node": true,
+ "webextensions": false
+ }
+ }
+ ]
}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..c65d254b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,27 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup node
+ uses: actions/setup-node@v1
+ with:
+ node-version: '12.x'
+ - name: Install dependencies
+ run: npm ci
+ - name: Build
+ run: npm run build --if-present
+ - name: Lint
+ run: npm run test-lint
+ env:
+ CI: true
+ - name: Tests
+ run: npm run test-code
+ env:
+ CI: true
diff --git a/.gitignore b/.gitignore
index c4c4ffc6..6dd55d4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
*.zip
+node_modules
diff --git a/README.md b/README.md
index 7b25b852..631f5a8b 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,6 @@ Yomichan provides advanced features not available in other browser-based diction
* [Flashcard Creation](https://foosoft.net/projects/yomichan/#flashcard-creation)
* [Keyboard Shortcuts](https://foosoft.net/projects/yomichan/#keyboard-shortcuts)
* [Development](https://foosoft.net/projects/yomichan/#development)
- * [Templates](https://foosoft.net/projects/yomichan/#templates)
* [Dependencies](https://foosoft.net/projects/yomichan/#dependencies)
* [Frequently Asked Questions](https://foosoft.net/projects/yomichan/#frequently-asked-questions)
* [Screenshots](https://foosoft.net/projects/yomichan/#screenshots)
@@ -241,15 +240,6 @@ following basic guidelines when creating pull requests:
* Large pull requests without a clear scope will not be merged.
* Incomplete or non-standalone features will not be merged.
-### Templates ###
-
-Yomichan uses [Handlebars](https://handlebarsjs.com/) templates for user interface generation. The source templates are
-found in the `tmpl` directory and the compiled version is stored in the `ext/bg/js/templates.js` file. If you modify the
-source templates, you will need to also recompile them. If you are developing on Linux or Mac OS X, you can use the
-included `build_tmpl.sh` and `build_tmpl_auto.sh` shell scripts to do this for you
-([inotify-tools](https://github.com/rvoicilas/inotify-tools/wiki) required). Otherwise, simply execute `handlebars
-tmpl/*.html -f ext/bg/js/templates.js` from the project's base directory to compile all the templates.
-
### Dependencies ###
Yomichan uses several third-party libraries to function. Below are links to homepages, snapshots, and licenses of the exact
diff --git a/build_tmpl.sh b/build_tmpl.sh
deleted file mode 100755
index e91f8de8..00000000
--- a/build_tmpl.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-handlebars tmpl/*.html -f ext/bg/js/templates.js
diff --git a/build_tmpl_auto.sh b/build_tmpl_auto.sh
deleted file mode 100755
index 98065cb7..00000000
--- a/build_tmpl_auto.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/bash
-DIRECTORY_TO_OBSERVE="tmpl"
-BUILD_SCRIPT="build_tmpl.sh"
-
-function block_for_change {
- inotifywait -e modify,move,create,delete $DIRECTORY_TO_OBSERVE
-}
-
-function build {
- bash $BUILD_SCRIPT
-}
-
-build
-while block_for_change; do
- build
-done
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;
}
}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..505c71db
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1119 @@
+{
+ "name": "yomichan",
+ "version": "0.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
+ "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.8.3"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
+ "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.0.0",
+ "esutils": "^2.0.2",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "acorn": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
+ "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz",
+ "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==",
+ "dev": true
+ },
+ "ajv": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
+ "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-escapes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz",
+ "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.8.1"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "astral-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "base64-arraybuffer-es6": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.5.0.tgz",
+ "integrity": "sha512-UCIPaDJrNNj5jG2ZL+nzJ7czvZV/ZYX6LaIRgfVU1k1edJOQg7dkbiSKzwHkNp6aHEHER/PhlFBrMYnlvJJQEw==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "cli-width": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+ "dev": true
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "core-js": {
+ "version": "2.6.11",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
+ "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
+ }
+ },
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+ "dev": true
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "domexception": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
+ "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==",
+ "dev": true,
+ "requires": {
+ "webidl-conversions": "^4.0.2"
+ }
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "eslint": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
+ "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "ajv": "^6.10.0",
+ "chalk": "^2.1.0",
+ "cross-spawn": "^6.0.5",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "eslint-scope": "^5.0.0",
+ "eslint-utils": "^1.4.3",
+ "eslint-visitor-keys": "^1.1.0",
+ "espree": "^6.1.2",
+ "esquery": "^1.0.1",
+ "esutils": "^2.0.2",
+ "file-entry-cache": "^5.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.0.0",
+ "globals": "^12.1.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "inquirer": "^7.0.0",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.3.0",
+ "lodash": "^4.17.14",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.1",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.8.3",
+ "progress": "^2.0.0",
+ "regexpp": "^2.0.1",
+ "semver": "^6.1.2",
+ "strip-ansi": "^5.2.0",
+ "strip-json-comments": "^3.0.1",
+ "table": "^5.2.3",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ }
+ },
+ "eslint-plugin-no-unsanitized": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-3.0.2.tgz",
+ "integrity": "sha512-JnwpoH8Sv4QOjrTDutENBHzSnyYtspdjtglYtqUtAHe6f6LLKqykJle+UwFPg23GGwt5hI3amS9CRDezW8GAww==",
+ "dev": true
+ },
+ "eslint-scope": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
+ "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
+ "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^1.1.0"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+ "dev": true
+ },
+ "espree": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz",
+ "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.1.0",
+ "acorn-jsx": "^5.1.0",
+ "eslint-visitor-keys": "^1.1.0"
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "esquery": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+ "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^4.0.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^4.1.0"
+ }
+ },
+ "estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "requires": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ }
+ },
+ "fake-indexeddb": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-3.0.0.tgz",
+ "integrity": "sha512-VrnV9dJWlVWvd8hp9MMR+JS4RLC4ZmToSkuCg91ZwpYE5mSODb3n5VEaV62Hf3AusnbrPfwQhukU+rGZm5W8PQ==",
+ "dev": true,
+ "requires": {
+ "realistic-structured-clone": "^2.0.1",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "fast-deep-equal": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+ "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
+ "dev": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "figures": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz",
+ "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "file-entry-cache": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
+ "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^2.0.1"
+ }
+ },
+ "flat-cache": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
+ "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+ "dev": true,
+ "requires": {
+ "flatted": "^2.0.0",
+ "rimraf": "2.6.3",
+ "write": "1.0.3"
+ }
+ },
+ "flatted": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz",
+ "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
+ "dev": true
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
+ "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "globals": {
+ "version": "12.3.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz",
+ "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.8.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
+ "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "inquirer": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
+ "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^2.4.2",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.15",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.2.0",
+ "rxjs": "^6.5.3",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^5.1.0",
+ "through": "^2.3.6"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-promise": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+ "dev": true
+ },
+ "lodash.sortby": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
+ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
+ "dev": true
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
+ "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "dev": true,
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+ "dev": true
+ },
+ "progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ },
+ "realistic-structured-clone": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/realistic-structured-clone/-/realistic-structured-clone-2.0.2.tgz",
+ "integrity": "sha512-5IEvyfuMJ4tjQOuKKTFNvd+H9GSbE87IcendSBannE28PTrbolgaVg5DdEApRKhtze794iXqVUFKV60GLCNKEg==",
+ "dev": true,
+ "requires": {
+ "core-js": "^2.5.3",
+ "domexception": "^1.0.1",
+ "typeson": "^5.8.2",
+ "typeson-registry": "^1.0.0-alpha.20"
+ }
+ },
+ "regexpp": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+ "dev": true
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
+ },
+ "restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "run-async": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+ "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+ "dev": true,
+ "requires": {
+ "is-promise": "^2.1.0"
+ }
+ },
+ "rxjs": {
+ "version": "6.5.4",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
+ "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ },
+ "setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+ "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "astral-regex": "^1.0.0",
+ "is-fullwidth-code-point": "^2.0.0"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ }
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+ "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ }
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ }
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
+ "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "table": {
+ "version": "5.4.6",
+ "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
+ "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.10.2",
+ "lodash": "^4.17.14",
+ "slice-ansi": "^2.1.0",
+ "string-width": "^3.0.0"
+ },
+ "dependencies": {
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ }
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ },
+ "tr46": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
+ "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "tslib": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
+ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
+ "dev": true
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ },
+ "type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true
+ },
+ "typeson": {
+ "version": "5.18.2",
+ "resolved": "https://registry.npmjs.org/typeson/-/typeson-5.18.2.tgz",
+ "integrity": "sha512-Vetd+OGX05P4qHyHiSLdHZ5Z5GuQDrHHwSdjkqho9NSCYVSLSfRMjklD/unpHH8tXBR9Z/R05rwJSuMpMFrdsw==",
+ "dev": true
+ },
+ "typeson-registry": {
+ "version": "1.0.0-alpha.34",
+ "resolved": "https://registry.npmjs.org/typeson-registry/-/typeson-registry-1.0.0-alpha.34.tgz",
+ "integrity": "sha512-2U0R5eFGJPaqha8HBAICJv6rW2x/cAVHizURHbcAo61Mpd47s+MDn67Ktxoyl9jWgsqCAibZsrldG8v/2ZuCaw==",
+ "dev": true,
+ "requires": {
+ "base64-arraybuffer-es6": "0.5.0",
+ "typeson": "5.18.2",
+ "whatwg-url": "7.1.0"
+ }
+ },
+ "uri-js": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "v8-compile-cache": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
+ "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
+ "dev": true
+ },
+ "webidl-conversions": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
+ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
+ "dev": true
+ },
+ "whatwg-url": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
+ "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
+ "dev": true,
+ "requires": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^1.0.1",
+ "webidl-conversions": "^4.0.2"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "write": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
+ "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^0.5.1"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..17fdfa82
--- /dev/null
+++ b/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "yomichan",
+ "version": "0.0.0",
+ "description": "Japanese pop-up dictionary extension for Chrome and Firefox.",
+ "directories": {
+ "test": "test"
+ },
+ "scripts": {
+ "test": "npm run test-lint && npm run test-code",
+ "test-lint": "eslint .",
+ "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/FooSoft/yomichan.git"
+ },
+ "author": "FooSoft",
+ "license": "GPL-3.0-or-later",
+ "licenses": [
+ {
+ "type": "GPL-3.0-or-later",
+ "url": "https://www.gnu.org/licenses/gpl-3.0.html"
+ }
+ ],
+ "bugs": {
+ "url": "https://github.com/FooSoft/yomichan/issues"
+ },
+ "homepage": "https://foosoft.net/projects/yomichan/",
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-plugin-no-unsanitized": "^3.0.2",
+ "fake-indexeddb": "^3.0.0"
+ }
+}
diff --git a/test/data/dictionaries/invalid-dictionary1/index.json b/test/data/dictionaries/invalid-dictionary1/index.json
new file mode 100644
index 00000000..1be3b360
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary1/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 1",
+ "format": 0,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid format number"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary2/index.json b/test/data/dictionaries/invalid-dictionary2/index.json
new file mode 100644
index 00000000..ba2cc669
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary2/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 2",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Empty entry in kanji bank"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary2/kanji_bank_1.json b/test/data/dictionaries/invalid-dictionary2/kanji_bank_1.json
new file mode 100644
index 00000000..5825bcac
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary2/kanji_bank_1.json
@@ -0,0 +1,3 @@
+[
+ []
+] \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary3/index.json b/test/data/dictionaries/invalid-dictionary3/index.json
new file mode 100644
index 00000000..f23fa3f0
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary3/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 3",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid type entry in kanji meta bank"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary3/kanji_meta_bank_1.json b/test/data/dictionaries/invalid-dictionary3/kanji_meta_bank_1.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary3/kanji_meta_bank_1.json
@@ -0,0 +1 @@
+{} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary4/index.json b/test/data/dictionaries/invalid-dictionary4/index.json
new file mode 100644
index 00000000..542791d7
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary4/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 4",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid value as part of a tag bank entry"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary4/tag_bank_1.json b/test/data/dictionaries/invalid-dictionary4/tag_bank_1.json
new file mode 100644
index 00000000..4f19b476
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary4/tag_bank_1.json
@@ -0,0 +1,3 @@
+[
+ [{"invalid": true}, "category1", 0, "tag1 notes", 0]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary5/index.json b/test/data/dictionaries/invalid-dictionary5/index.json
new file mode 100644
index 00000000..e0d0f00e
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary5/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 5",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid type as part of a term bank entry"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary5/term_bank_1.json b/test/data/dictionaries/invalid-dictionary5/term_bank_1.json
new file mode 100644
index 00000000..7288a996
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary5/term_bank_1.json
@@ -0,0 +1,3 @@
+[
+ ["打", "だ", "tag1 tag2", "", 2, false, 1, "tag3 tag4 tag5"]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary6/index.json b/test/data/dictionaries/invalid-dictionary6/index.json
new file mode 100644
index 00000000..b91acca3
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary6/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 6",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid root type for term meta bank"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary6/term_meta_bank_1.json b/test/data/dictionaries/invalid-dictionary6/term_meta_bank_1.json
new file mode 100644
index 00000000..02e4a84d
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary6/term_meta_bank_1.json
@@ -0,0 +1 @@
+false \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/index.json b/test/data/dictionaries/valid-dictionary1/index.json
new file mode 100644
index 00000000..3034bf38
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/index.json
@@ -0,0 +1,6 @@
+{
+ "title": "Test Dictionary",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true
+} \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/kanji_bank_1.json b/test/data/dictionaries/valid-dictionary1/kanji_bank_1.json
new file mode 100644
index 00000000..264f94c1
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/kanji_bank_1.json
@@ -0,0 +1,42 @@
+[
+ [
+ "打",
+ "ダ ダアス",
+ "う.つ う.ち- ぶ.つ",
+ "ktag1 ktag2",
+ [
+ "meaning1",
+ "meaning2",
+ "meaning3",
+ "meaning4",
+ "meaning5"
+ ],
+ {
+ "kstat1": "1",
+ "kstat2": "2",
+ "kstat3": "3",
+ "kstat4": "4",
+ "kstat5": "5"
+ }
+ ],
+ [
+ "込",
+ "",
+ "-こ.む こ.む こ.み -こ.み こ.める",
+ "ktag1 ktag2",
+ [
+ "meaning1",
+ "meaning2",
+ "meaning3",
+ "meaning4",
+ "meaning5"
+ ],
+ {
+ "kstat1": "1",
+ "kstat2": "2",
+ "kstat3": "3",
+ "kstat4": "4",
+ "kstat5": "5"
+ }
+ ]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json
new file mode 100644
index 00000000..73e75b8a
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json
@@ -0,0 +1,4 @@
+[
+ ["打", "freq", 1],
+ ["込", "freq", 2]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/tag_bank_1.json b/test/data/dictionaries/valid-dictionary1/tag_bank_1.json
new file mode 100644
index 00000000..109ad395
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/tag_bank_1.json
@@ -0,0 +1,7 @@
+[
+ ["tag1", "category1", 0, "tag1 notes", 0],
+ ["tag2", "category2", 0, "tag2 notes", 0],
+ ["tag3", "category3", 0, "tag3 notes", 0],
+ ["tag4", "category4", 0, "tag4 notes", 0],
+ ["tag5", "category5", 0, "tag5 notes", 0]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/tag_bank_2.json b/test/data/dictionaries/valid-dictionary1/tag_bank_2.json
new file mode 100644
index 00000000..5e7936b3
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/tag_bank_2.json
@@ -0,0 +1,9 @@
+[
+ ["ktag1", "kcategory1", 0, "ktag1 notes", 0],
+ ["ktag2", "kcategory2", 0, "ktag2 notes", 0],
+ ["kstat1", "kcategory3", 0, "kstat1 notes", 0],
+ ["kstat2", "kcategory4", 0, "kstat2 notes", 0],
+ ["kstat3", "kcategory5", 0, "kstat3 notes", 0],
+ ["kstat4", "kcategory6", 0, "kstat4 notes", 0],
+ ["kstat5", "kcategory7", 0, "kstat5 notes", 0]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/term_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_bank_1.json
new file mode 100644
index 00000000..755d9f6a
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/term_bank_1.json
@@ -0,0 +1,34 @@
+[
+ ["打", "だ", "tag1 tag2", "", 2, ["definition1a (打, だ)", "definition1b (打, だ)"], 1, "tag3 tag4 tag5"],
+ ["打", "ダース", "tag1 tag2", "", 1, ["definition1a (打, ダース)", "definition1b (打, ダース)"], 2, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 3, ["definition1a (打つ, うつ)", "definition1b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 4, ["definition2a (打つ, うつ)", "definition2b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 5, ["definition3a (打つ, うつ)", "definition3b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 6, ["definition4a (打つ, うつ)", "definition4b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 7, ["definition5a (打つ, うつ)", "definition5b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 8, ["definition6a (打つ, うつ)", "definition6b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 9, ["definition7a (打つ, うつ)", "definition7b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 10, ["definition8a (打つ, うつ)", "definition8b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 11, ["definition9a (打つ, うつ)", "definition9b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 12, ["definition10a (打つ, うつ)", "definition10b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 13, ["definition11a (打つ, うつ)", "definition11b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 14, ["definition12a (打つ, うつ)", "definition12b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 15, ["definition13a (打つ, うつ)", "definition13b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 16, ["definition14a (打つ, うつ)", "definition14b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 17, ["definition15a (打つ, うつ)", "definition15b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "ぶつ", "tag1 tag2", "v5", 18, ["definition1a (打つ, ぶつ)", "definition1b (打つ, ぶつ)"], 4, "tag3 tag4 tag5"],
+ ["打つ", "ぶつ", "tag1 tag2", "v5", 19, ["definition2a (打つ, ぶつ)", "definition2b (打つ, ぶつ)"], 4, "tag3 tag4 tag5"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 20, ["definition1a (打ち込む, うちこむ)", "definition1b (打ち込む, うちこむ)"], 5, "tag3 tag4 tag5"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 21, ["definition2a (打ち込む, うちこむ)", "definition2b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 22, ["definition3a (打ち込む, うちこむ)", "definition3b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 23, ["definition4a (打ち込む, うちこむ)", "definition4b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 24, ["definition5a (打ち込む, うちこむ)", "definition5b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 25, ["definition6a (打ち込む, うちこむ)", "definition6b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 26, ["definition7a (打ち込む, うちこむ)", "definition7b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 27, ["definition8a (打ち込む, うちこむ)", "definition8b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 28, ["definition9a (打ち込む, うちこむ)", "definition9b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 29, ["definition1a (打ち込む, ぶちこむ)", "definition1b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"],
+ ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 30, ["definition2a (打ち込む, ぶちこむ)", "definition2b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"],
+ ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 31, ["definition3a (打ち込む, ぶちこむ)", "definition3b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"],
+ ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 32, ["definition4a (打ち込む, ぶちこむ)", "definition4b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json
new file mode 100644
index 00000000..78096502
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json
@@ -0,0 +1,5 @@
+[
+ ["打", "freq", 1],
+ ["打つ", "freq", 2],
+ ["打ち込む", "freq", 3]
+] \ No newline at end of file
diff --git a/test/dictionary-validate.js b/test/dictionary-validate.js
new file mode 100644
index 00000000..14eee2ed
--- /dev/null
+++ b/test/dictionary-validate.js
@@ -0,0 +1,113 @@
+/*
+ * 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/>.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const yomichanTest = require('./yomichan-test');
+
+const JSZip = yomichanTest.JSZip;
+const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+
+
+function readSchema(relativeFileName) {
+ const fileName = path.join(__dirname, relativeFileName);
+ const source = fs.readFileSync(fileName, {encoding: 'utf8'});
+ return JSON.parse(source);
+}
+
+
+async function validateDictionaryBanks(zip, fileNameFormat, schema) {
+ let index = 1;
+ while (true) {
+ const fileName = fileNameFormat.replace(/\?/, index);
+
+ const file = zip.files[fileName];
+ if (!file) { break; }
+
+ const data = JSON.parse(await file.async('string'));
+ JsonSchema.validate(data, schema);
+
+ ++index;
+ }
+}
+
+async function validateDictionary(archive, schemas) {
+ const indexFile = archive.files['index.json'];
+ if (!indexFile) {
+ throw new Error('No dictionary index found in archive');
+ }
+
+ const index = JSON.parse(await indexFile.async('string'));
+ const version = index.format || index.version;
+
+ JsonSchema.validate(index, schemas.index);
+
+ await validateDictionaryBanks(archive, 'term_bank_?.json', version === 1 ? schemas.termBankV1 : schemas.termBankV3);
+ await validateDictionaryBanks(archive, 'term_meta_bank_?.json', schemas.termMetaBankV3);
+ await validateDictionaryBanks(archive, 'kanji_bank_?.json', version === 1 ? schemas.kanjiBankV1 : schemas.kanjiBankV3);
+ await validateDictionaryBanks(archive, 'kanji_meta_bank_?.json', schemas.kanjiMetaBankV3);
+ await validateDictionaryBanks(archive, 'tag_bank_?.json', schemas.tagBankV3);
+}
+
+function getSchemas() {
+ return {
+ index: readSchema('../ext/bg/data/dictionary-index-schema.json'),
+ kanjiBankV1: readSchema('../ext/bg/data/dictionary-kanji-bank-v1-schema.json'),
+ kanjiBankV3: readSchema('../ext/bg/data/dictionary-kanji-bank-v3-schema.json'),
+ kanjiMetaBankV3: readSchema('../ext/bg/data/dictionary-kanji-meta-bank-v3-schema.json'),
+ tagBankV3: readSchema('../ext/bg/data/dictionary-tag-bank-v3-schema.json'),
+ termBankV1: readSchema('../ext/bg/data/dictionary-term-bank-v1-schema.json'),
+ termBankV3: readSchema('../ext/bg/data/dictionary-term-bank-v3-schema.json'),
+ termMetaBankV3: readSchema('../ext/bg/data/dictionary-term-meta-bank-v3-schema.json')
+ };
+}
+
+
+async function main() {
+ const dictionaryFileNames = process.argv.slice(2);
+ if (dictionaryFileNames.length === 0) {
+ console.log([
+ 'Usage:',
+ ' node dictionary-validate <dictionary-file-names>...'
+ ].join('\n'));
+ return;
+ }
+
+ const schemas = getSchemas();
+
+ for (const dictionaryFileName of dictionaryFileNames) {
+ try {
+ console.log(`Validating ${dictionaryFileName}...`);
+ const source = fs.readFileSync(dictionaryFileName);
+ const archive = await JSZip.loadAsync(source);
+ await validateDictionary(archive, schemas);
+ console.log('No issues found');
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+}
+
+
+if (require.main === module) { main(); }
+
+
+module.exports = {
+ getSchemas,
+ validateDictionary
+};
diff --git a/test/schema-validate.js b/test/schema-validate.js
new file mode 100644
index 00000000..a4f2d94c
--- /dev/null
+++ b/test/schema-validate.js
@@ -0,0 +1,52 @@
+/*
+ * 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/>.
+ */
+
+const fs = require('fs');
+const yomichanTest = require('./yomichan-test');
+
+const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+
+
+function main() {
+ const args = process.argv.slice(2);
+ if (args.length < 2) {
+ console.log([
+ 'Usage:',
+ ' node schema-validate <schema-file-name> <data-file-names>...'
+ ].join('\n'));
+ return;
+ }
+
+ const schemaSource = fs.readFileSync(args[0], {encoding: 'utf8'});
+ const schema = JSON.parse(schemaSource);
+
+ for (const dataFileName of args.slice(1)) {
+ try {
+ console.log(`Validating ${dataFileName}...`);
+ const dataSource = fs.readFileSync(dataFileName, {encoding: 'utf8'});
+ const data = JSON.parse(dataSource);
+ JsonSchema.validate(data, schema);
+ console.log('No issues found');
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/test-database.js b/test/test-database.js
new file mode 100644
index 00000000..c2317881
--- /dev/null
+++ b/test/test-database.js
@@ -0,0 +1,935 @@
+/*
+ * 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/>.
+ */
+
+const fs = require('fs');
+const url = require('url');
+const path = require('path');
+const assert = require('assert');
+const yomichanTest = require('./yomichan-test');
+require('fake-indexeddb/auto');
+
+const chrome = {
+ runtime: {
+ onMessage: {
+ addListener() { /* NOP */ }
+ },
+ getURL(path2) {
+ return url.pathToFileURL(path.join(__dirname, '..', 'ext', path2.replace(/^\//, '')));
+ }
+ }
+};
+
+class XMLHttpRequest {
+ constructor() {
+ this._eventCallbacks = new Map();
+ this._url = '';
+ this._responseText = null;
+ }
+
+ overrideMimeType() {
+ // NOP
+ }
+
+ addEventListener(eventName, callback) {
+ let callbacks = this._eventCallbacks.get(eventName);
+ if (typeof callbacks === 'undefined') {
+ callbacks = [];
+ this._eventCallbacks.set(eventName, callbacks);
+ }
+ callbacks.push(callback);
+ }
+
+ open(action, url2) {
+ this._url = url2;
+ }
+
+ send() {
+ const filePath = url.fileURLToPath(this._url);
+ Promise.resolve()
+ .then(() => {
+ let source;
+ try {
+ source = fs.readFileSync(filePath, {encoding: 'utf8'});
+ } catch (e) {
+ this._trigger('error');
+ return;
+ }
+ this._responseText = source;
+ this._trigger('load');
+ });
+ }
+
+ get responseText() {
+ return this._responseText;
+ }
+
+ _trigger(eventName, ...args) {
+ const callbacks = this._eventCallbacks.get(eventName);
+ if (typeof callbacks === 'undefined') { return; }
+
+ for (let i = 0, ii = callbacks.length; i < ii; ++i) {
+ callbacks[i](...args);
+ }
+ }
+}
+
+const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+const {dictFieldSplit, dictTagSanitize} = yomichanTest.requireScript('ext/bg/js/dictionary.js', ['dictFieldSplit', 'dictTagSanitize']);
+const {stringReverse, hasOwn} = yomichanTest.requireScript('ext/mixed/js/core.js', ['stringReverse', 'hasOwn'], {chrome});
+const {requestJson} = yomichanTest.requireScript('ext/bg/js/request.js', ['requestJson'], {XMLHttpRequest});
+
+const databaseGlobals = {
+ chrome,
+ JsonSchema,
+ requestJson,
+ stringReverse,
+ hasOwn,
+ dictFieldSplit,
+ dictTagSanitize,
+ indexedDB: global.indexedDB,
+ JSZip: yomichanTest.JSZip
+};
+databaseGlobals.window = databaseGlobals;
+const {Database} = yomichanTest.requireScript('ext/bg/js/database.js', ['Database'], databaseGlobals);
+
+
+function countTermsWithExpression(terms, expression) {
+ return terms.reduce((i, v) => (i + (v.expression === expression ? 1 : 0)), 0);
+}
+
+function countTermsWithReading(terms, reading) {
+ return terms.reduce((i, v) => (i + (v.reading === reading ? 1 : 0)), 0);
+}
+
+function countMetasWithMode(metas, mode) {
+ return metas.reduce((i, v) => (i + (v.mode === mode ? 1 : 0)), 0);
+}
+
+function countKanjiWithCharacter(kanji, character) {
+ return kanji.reduce((i, v) => (i + (v.character === character ? 1 : 0)), 0);
+}
+
+
+function clearDatabase(timeout) {
+ return new Promise((resolve, reject) => {
+ let timer = setTimeout(() => {
+ timer = null;
+ reject(new Error(`clearDatabase failed to resolve after ${timeout}ms`));
+ }, timeout);
+
+ (async () => {
+ const indexedDB = global.indexedDB;
+ for (const {name} of await indexedDB.databases()) {
+ await new Promise((resolve2, reject2) => {
+ const request = indexedDB.deleteDatabase(name);
+ request.onerror = (e) => reject2(e);
+ request.onsuccess = () => resolve2();
+ });
+ }
+ if (timer !== null) {
+ clearTimeout(timer);
+ }
+ resolve();
+ })();
+ });
+}
+
+
+async function testDatabase1() {
+ // Load dictionary data
+ const testDictionary = yomichanTest.createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'string'});
+ const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
+
+ const title = testDictionaryIndex.title;
+ const titles = new Map([
+ [title, {priority: 0, allowSecondarySearches: false}]
+ ]);
+
+ // Setup iteration data
+ const iterations = [
+ {
+ cleanup: async () => {
+ // Test purge
+ await database.purge();
+ await testDatabaseEmpty1(database);
+ }
+ },
+ {
+ cleanup: async () => {
+ // Test deleteDictionary
+ let progressEvent = false;
+ await database.deleteDictionary(
+ title,
+ () => {
+ progressEvent = true;
+ },
+ {rate: 1000}
+ );
+ assert.ok(progressEvent);
+
+ await testDatabaseEmpty1(database);
+ }
+ },
+ {
+ cleanup: async () => {}
+ }
+ ];
+
+ // Setup database
+ const database = new Database();
+ await database.prepare();
+
+ for (const {cleanup} of iterations) {
+ const expectedSummary = {
+ title,
+ revision: 'test',
+ sequenced: true,
+ version: 3,
+ prefixWildcardsSupported: true
+ };
+
+ // Import data
+ let progressEvent = false;
+ const {result, errors} = await database.importDictionary(
+ testDictionarySource,
+ () => {
+ progressEvent = true;
+ },
+ {prefixWildcardsSupported: true}
+ );
+ assert.deepStrictEqual(errors, []);
+ assert.deepStrictEqual(result, expectedSummary);
+ assert.ok(progressEvent);
+
+ // Get info summary
+ const info = await database.getDictionaryInfo();
+ assert.deepStrictEqual(info, [expectedSummary]);
+
+ // Get counts
+ const counts = await database.getDictionaryCounts(
+ info.map((v) => v.title),
+ true
+ );
+ assert.deepStrictEqual(counts, {
+ counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}],
+ total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}
+ });
+
+ // Test find* functions
+ await testFindTermsBulkTest1(database, titles);
+ await testTindTermsExactBulk1(database, titles);
+ await testFindTermsBySequenceBulk1(database, title);
+ await testFindTermMetaBulk1(database, titles);
+ await testFindKanjiBulk1(database, titles);
+ await testFindKanjiMetaBulk1(database, titles);
+ await testFindTagForTitle1(database, title);
+
+ // Cleanup
+ await cleanup();
+ }
+
+ await database.close();
+}
+
+async function testDatabaseEmpty1(database) {
+ const info = await database.getDictionaryInfo();
+ assert.deepStrictEqual(info, []);
+
+ const counts = await database.getDictionaryCounts([], true);
+ assert.deepStrictEqual(counts, {
+ counts: [],
+ total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0}
+ });
+}
+
+async function testFindTermsBulkTest1(database, titles) {
+ const data = [
+ {
+ inputs: [
+ {
+ wildcard: null,
+ termList: ['打', '打つ', '打ち込む']
+ },
+ {
+ wildcard: null,
+ termList: ['だ', 'ダース', 'うつ', 'ぶつ', 'うちこむ', 'ぶちこむ']
+ },
+ {
+ wildcard: 'suffix',
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 32,
+ expressions: [
+ ['打', 2],
+ ['打つ', 17],
+ ['打ち込む', 13]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 15],
+ ['ぶつ', 2],
+ ['うちこむ', 9],
+ ['ぶちこむ', 4]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ wildcard: null,
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ wildcard: 'prefix',
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 13,
+ expressions: [
+ ['打ち込む', 13]
+ ],
+ readings: [
+ ['うちこむ', 9],
+ ['ぶちこむ', 4]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ wildcard: null,
+ termList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList, wildcard} of inputs) {
+ const results = await database.findTermsBulk(termList, titles, wildcard);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [expression, count] of expectedResults.expressions) {
+ assert.strictEqual(countTermsWithExpression(results, expression), count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ assert.strictEqual(countTermsWithReading(results, reading), count);
+ }
+ }
+ }
+}
+
+async function testTindTermsExactBulk1(database, titles) {
+ const data = [
+ {
+ inputs: [
+ {
+ termList: ['打', '打つ', '打ち込む'],
+ readingList: ['だ', 'うつ', 'うちこむ']
+ }
+ ],
+ expectedResults: {
+ total: 25,
+ expressions: [
+ ['打', 1],
+ ['打つ', 15],
+ ['打ち込む', 9]
+ ],
+ readings: [
+ ['だ', 1],
+ ['うつ', 15],
+ ['うちこむ', 9]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打', '打つ', '打ち込む'],
+ readingList: ['だ?', 'うつ?', 'うちこむ?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打つ', '打つ'],
+ readingList: ['うつ', 'ぶつ']
+ }
+ ],
+ expectedResults: {
+ total: 17,
+ expressions: [
+ ['打つ', 17]
+ ],
+ readings: [
+ ['うつ', 15],
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打つ'],
+ readingList: ['うちこむ']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [],
+ readingList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList, readingList} of inputs) {
+ const results = await database.findTermsExactBulk(termList, readingList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [expression, count] of expectedResults.expressions) {
+ assert.strictEqual(countTermsWithExpression(results, expression), count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ assert.strictEqual(countTermsWithReading(results, reading), count);
+ }
+ }
+ }
+}
+
+async function testFindTermsBySequenceBulk1(database, mainDictionary) {
+ const data = [
+ {
+ inputs: [
+ {
+ sequenceList: [1, 2, 3, 4, 5, 6]
+ }
+ ],
+ expectedResults: {
+ total: 32,
+ expressions: [
+ ['打', 2],
+ ['打つ', 17],
+ ['打ち込む', 13]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 15],
+ ['ぶつ', 2],
+ ['うちこむ', 9],
+ ['ぶちこむ', 4]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [1]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ expressions: [
+ ['打', 1]
+ ],
+ readings: [
+ ['だ', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [2]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ expressions: [
+ ['打', 1]
+ ],
+ readings: [
+ ['ダース', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [3]
+ }
+ ],
+ expectedResults: {
+ total: 15,
+ expressions: [
+ ['打つ', 15]
+ ],
+ readings: [
+ ['うつ', 15]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [4]
+ }
+ ],
+ expectedResults: {
+ total: 2,
+ expressions: [
+ ['打つ', 2]
+ ],
+ readings: [
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [5]
+ }
+ ],
+ expectedResults: {
+ total: 9,
+ expressions: [
+ ['打ち込む', 9]
+ ],
+ readings: [
+ ['うちこむ', 9]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [6]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ expressions: [
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['ぶちこむ', 4]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [-1]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {sequenceList} of inputs) {
+ const results = await database.findTermsBySequenceBulk(sequenceList, mainDictionary);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [expression, count] of expectedResults.expressions) {
+ assert.strictEqual(countTermsWithExpression(results, expression), count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ assert.strictEqual(countTermsWithReading(results, reading), count);
+ }
+ }
+ }
+}
+
+async function testFindTermMetaBulk1(database, titles) {
+ const data = [
+ {
+ inputs: [
+ {
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打つ']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打ち込む']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList} of inputs) {
+ const results = await database.findTermMetaBulk(termList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ assert.strictEqual(countMetasWithMode(results, mode), count);
+ }
+ }
+ }
+}
+
+async function testFindKanjiBulk1(database, titles) {
+ const data = [
+ {
+ inputs: [
+ {
+ kanjiList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ kanji: [
+ ['打', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['込']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ kanji: [
+ ['込', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ kanji: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {kanjiList} of inputs) {
+ const results = await database.findKanjiBulk(kanjiList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [kanji, count] of expectedResults.kanji) {
+ assert.strictEqual(countKanjiWithCharacter(results, kanji), count);
+ }
+ }
+ }
+}
+
+async function testFindKanjiMetaBulk1(database, titles) {
+ const data = [
+ {
+ inputs: [
+ {
+ kanjiList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['込']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {kanjiList} of inputs) {
+ const results = await database.findKanjiMetaBulk(kanjiList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ assert.strictEqual(countMetasWithMode(results, mode), count);
+ }
+ }
+ }
+}
+
+async function testFindTagForTitle1(database, title) {
+ const data = [
+ {
+ inputs: [
+ {
+ name: 'tag1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'category1', dictionary: title, name: 'tag1', notes: 'tag1 notes', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'ktag1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'kcategory1', dictionary: title, name: 'ktag1', notes: 'ktag1 notes', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'kstat1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'kcategory3', dictionary: title, name: 'kstat1', notes: 'kstat1 notes', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'invalid'
+ }
+ ],
+ expectedResults: {
+ value: null
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {name} of inputs) {
+ const result = await database.findTagForTitle(name, title);
+ assert.deepStrictEqual(result, expectedResults.value);
+ }
+ }
+}
+
+
+async function testDatabase2() {
+ // Load dictionary data
+ const testDictionary = yomichanTest.createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'string'});
+ const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
+
+ const title = testDictionaryIndex.title;
+ const titles = new Map([
+ [title, {priority: 0, allowSecondarySearches: false}]
+ ]);
+
+ // Setup database
+ const database = new Database();
+
+ // Error: not prepared
+ await assert.rejects(async () => await database.purge());
+ await assert.rejects(async () => await database.deleteDictionary(title, () => {}, {}));
+ await assert.rejects(async () => await database.findTermsBulk(['?'], titles, null));
+ await assert.rejects(async () => await database.findTermsExactBulk(['?'], ['?'], titles));
+ await assert.rejects(async () => await database.findTermsBySequenceBulk([1], title));
+ await assert.rejects(async () => await database.findTermMetaBulk(['?'], titles));
+ await assert.rejects(async () => await database.findTermMetaBulk(['?'], titles));
+ await assert.rejects(async () => await database.findKanjiBulk(['?'], titles));
+ await assert.rejects(async () => await database.findKanjiMetaBulk(['?'], titles));
+ await assert.rejects(async () => await database.findTagForTitle('tag', title));
+ await assert.rejects(async () => await database.getDictionaryInfo());
+ await assert.rejects(async () => await database.getDictionaryCounts(titles, true));
+ await assert.rejects(async () => await database.importDictionary(testDictionarySource, () => {}, {}));
+
+ await database.prepare();
+
+ // Error: already prepared
+ await assert.rejects(async () => await database.prepare());
+
+ await database.importDictionary(testDictionarySource, () => {}, {});
+
+ // Error: dictionary already imported
+ await assert.rejects(async () => await database.importDictionary(testDictionarySource, () => {}, {}));
+
+ await database.close();
+}
+
+
+async function testDatabase3() {
+ const invalidDictionaries = [
+ 'invalid-dictionary1',
+ 'invalid-dictionary2',
+ 'invalid-dictionary3',
+ 'invalid-dictionary4',
+ 'invalid-dictionary5',
+ 'invalid-dictionary6'
+ ];
+
+ // Setup database
+ const database = new Database();
+ await database.prepare();
+
+ for (const invalidDictionary of invalidDictionaries) {
+ const testDictionary = yomichanTest.createTestDictionaryArchive(invalidDictionary);
+ const testDictionarySource = await testDictionary.generateAsync({type: 'string'});
+
+ let error = null;
+ try {
+ await database.importDictionary(testDictionarySource, () => {}, {});
+ } catch (e) {
+ error = e;
+ }
+
+ if (error === null) {
+ assert.ok(false, `Expected an error while importing ${invalidDictionary}`);
+ } else {
+ const prefix = 'Dictionary has invalid data';
+ const message = error.message;
+ assert.ok(typeof message, 'string');
+ assert.ok(message.startsWith(prefix), `Expected error message to start with '${prefix}': ${message}`);
+ }
+ }
+
+ await database.close();
+}
+
+
+async function main() {
+ const clearTimeout = 5000;
+ try {
+ await testDatabase1();
+ await clearDatabase(clearTimeout);
+
+ await testDatabase2();
+ await clearDatabase(clearTimeout);
+
+ await testDatabase3();
+ await clearDatabase(clearTimeout);
+ } catch (e) {
+ console.log(e);
+ process.exit(-1);
+ throw e;
+ }
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/test-dictionary.js b/test/test-dictionary.js
new file mode 100644
index 00000000..74f9e62b
--- /dev/null
+++ b/test/test-dictionary.js
@@ -0,0 +1,59 @@
+/*
+ * 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/>.
+ */
+
+const yomichanTest = require('./yomichan-test');
+const dictionaryValidate = require('./dictionary-validate');
+
+
+async function main() {
+ const dictionaries = [
+ {name: 'valid-dictionary1', valid: true},
+ {name: 'invalid-dictionary1', valid: false},
+ {name: 'invalid-dictionary2', valid: false},
+ {name: 'invalid-dictionary3', valid: false},
+ {name: 'invalid-dictionary4', valid: false},
+ {name: 'invalid-dictionary5', valid: false},
+ {name: 'invalid-dictionary6', valid: false}
+ ];
+
+ const schemas = dictionaryValidate.getSchemas();
+
+ for (const {name, valid} of dictionaries) {
+ const archive = yomichanTest.createTestDictionaryArchive(name);
+
+ let error = null;
+ try {
+ await dictionaryValidate.validateDictionary(archive, schemas);
+ } catch (e) {
+ error = e;
+ }
+
+ if (valid) {
+ if (error !== null) {
+ throw error;
+ }
+ } else {
+ if (error === null) {
+ throw new Error(`Expected dictionary ${name} to be invalid`);
+ }
+ }
+ }
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/test-schema.js b/test/test-schema.js
new file mode 100644
index 00000000..f4612f86
--- /dev/null
+++ b/test/test-schema.js
@@ -0,0 +1,251 @@
+/*
+ * 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/>.
+ */
+
+const assert = require('assert');
+const yomichanTest = require('./yomichan-test');
+
+const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+
+
+function testValidate1() {
+ const schema = {
+ allOf: [
+ {
+ type: 'number'
+ },
+ {
+ anyOf: [
+ {minimum: 10, maximum: 100},
+ {minimum: -100, maximum: -10}
+ ]
+ },
+ {
+ oneOf: [
+ {multipleOf: 3},
+ {multipleOf: 5}
+ ]
+ },
+ {
+ not: [
+ {multipleOf: 20}
+ ]
+ }
+ ]
+ };
+
+ const schemaValidate = (value) => {
+ try {
+ JsonSchema.validate(value, schema);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ };
+
+ const jsValidate = (value) => {
+ return (
+ typeof value === 'number' &&
+ (
+ (value >= 10 && value <= 100) ||
+ (value >= -100 && value <= -10)
+ ) &&
+ (
+ (
+ (value % 3) === 0 ||
+ (value % 5) === 0
+ ) &&
+ (value % 15) !== 0
+ ) &&
+ (value % 20) !== 0
+ );
+ };
+
+ for (let i = -111; i <= 111; i++) {
+ const actual = schemaValidate(i, schema);
+ const expected = jsValidate(i);
+ assert.strictEqual(actual, expected);
+ }
+}
+
+
+function testGetValidValueOrDefault1() {
+ // Test value defaulting on objects with additionalProperties=false
+ const schema = {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: false
+ };
+
+ const testData = [
+ [
+ void 0,
+ {test: 'default'}
+ ],
+ [
+ null,
+ {test: 'default'}
+ ],
+ [
+ 0,
+ {test: 'default'}
+ ],
+ [
+ '',
+ {test: 'default'}
+ ],
+ [
+ [],
+ {test: 'default'}
+ ],
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value'}
+ ]
+ ];
+
+ for (const [value, expected] of testData) {
+ const actual = JsonSchema.getValidValueOrDefault(schema, value);
+ assert.deepStrictEqual(actual, expected);
+ }
+}
+
+function testGetValidValueOrDefault2() {
+ // Test value defaulting on objects with additionalProperties=true
+ const schema = {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: true
+ };
+
+ const testData = [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default', test2: 'value2'}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value', test2: 'value2'}
+ ]
+ ];
+
+ for (const [value, expected] of testData) {
+ const actual = JsonSchema.getValidValueOrDefault(schema, value);
+ assert.deepStrictEqual(actual, expected);
+ }
+}
+
+function testGetValidValueOrDefault3() {
+ // Test value defaulting on objects with additionalProperties={schema}
+ const schema = {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: {
+ type: 'number',
+ default: 10
+ }
+ };
+
+ const testData = [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default', test2: 10}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value', test2: 10}
+ ],
+ [
+ {test2: 2},
+ {test: 'default', test2: 2}
+ ],
+ [
+ {test: 'value', test2: 2},
+ {test: 'value', test2: 2}
+ ],
+ [
+ {test: 'value', test2: 2, test3: null},
+ {test: 'value', test2: 2, test3: 10}
+ ],
+ [
+ {test: 'value', test2: 2, test3: void 0},
+ {test: 'value', test2: 2, test3: 10}
+ ]
+ ];
+
+ for (const [value, expected] of testData) {
+ const actual = JsonSchema.getValidValueOrDefault(schema, value);
+ assert.deepStrictEqual(actual, expected);
+ }
+}
+
+
+function main() {
+ testValidate1();
+ testGetValidValueOrDefault1();
+ testGetValidValueOrDefault2();
+ testGetValidValueOrDefault3();
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/yomichan-test.js b/test/yomichan-test.js
new file mode 100644
index 00000000..78bfb9c6
--- /dev/null
+++ b/test/yomichan-test.js
@@ -0,0 +1,70 @@
+/*
+ * 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/>.
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+
+let JSZip = null;
+
+function requireScript(fileName, exportNames, variables) {
+ const absoluteFileName = path.join(__dirname, '..', fileName);
+ const source = fs.readFileSync(absoluteFileName, {encoding: 'utf8'});
+ const exportNamesString = Array.isArray(exportNames) ? exportNames.join(',') : '';
+ const variablesArgumentName = '__variables__';
+ let variableString = '';
+ if (typeof variables === 'object' && variables !== null) {
+ variableString = Object.keys(variables).join(',');
+ variableString = `const {${variableString}} = ${variablesArgumentName};`;
+ }
+ return Function(variablesArgumentName, `'use strict';${variableString}${source}\n;return {${exportNamesString}};`)(variables);
+}
+
+function getJSZip() {
+ if (JSZip === null) {
+ process.noDeprecation = true; // Suppress a warning about JSZip
+ JSZip = require(path.join(__dirname, '../ext/mixed/lib/jszip.min.js'));
+ process.noDeprecation = false;
+ }
+ return JSZip;
+}
+
+function createTestDictionaryArchive(dictionary, dictionaryName) {
+ const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary);
+ const fileNames = fs.readdirSync(dictionaryDirectory);
+
+ const archive = new (getJSZip())();
+
+ for (const fileName of fileNames) {
+ const source = fs.readFileSync(path.join(dictionaryDirectory, fileName), {encoding: 'utf8'});
+ const json = JSON.parse(source);
+ if (fileName === 'index.json' && typeof dictionaryName === 'string') {
+ json.title = dictionaryName;
+ }
+ archive.file(fileName, JSON.stringify(json, null, 0));
+ }
+
+ return archive;
+}
+
+
+module.exports = {
+ requireScript,
+ createTestDictionaryArchive,
+ get JSZip() { return getJSZip(); }
+};
diff --git a/tmpl/query-parser.html b/tmpl/query-parser.html
deleted file mode 100644
index db98b5ff..00000000
--- a/tmpl/query-parser.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{{~#*inline "term"~}}
-{{~#if preview~}}
-<span class="query-parser-term-preview">
-{{~else~}}
-<span class="query-parser-term">
-{{~/if~}}
-{{~#each this~}}
-{{> part }}
-{{~/each~}}
-</span>
-{{~/inline~}}
-
-{{~#*inline "part"~}}
-{{~#if raw~}}
-{{~#each text~}}
-<span class="query-parser-char">{{this}}</span>
-{{~/each~}}
-{{~else~}}
-<ruby>{{~#each text~}}
-<span class="query-parser-char">{{this}}</span>
-{{~/each~}}<rt>{{reading}}</rt></ruby>
-{{~/if~}}
-{{~/inline~}}
-
-{{~#each terms~}}
-{{> term preview=../preview }}
-{{~/each~}}