aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2020-03-14 13:14:03 -0700
committerAlex Yatskov <alex@foosoft.net>2020-03-14 13:14:03 -0700
commitb77e2afe3a8ef9e96a53dd8ca97d8b913941244b (patch)
tree818a2f25169845a72b3424b7400b5b07f0f7cacf
parentd32f4def0eeed1599857bc04c973337a2a13dd8b (diff)
parent98afe7adae80c6bc9de0c4b996e6f6cb0a5df49d (diff)
Merge branch 'master' into testing
-rw-r--r--.eslintrc.json6
-rw-r--r--ext/bg/background.html6
-rw-r--r--ext/bg/data/default-anki-field-templates.handlebars161
-rw-r--r--ext/bg/js/anki-note-builder.js100
-rw-r--r--ext/bg/js/anki.js4
-rw-r--r--ext/bg/js/api.js55
-rw-r--r--ext/bg/js/audio-uri-builder.js (renamed from ext/bg/js/audio.js)154
-rw-r--r--ext/bg/js/backend.js325
-rw-r--r--ext/bg/js/clipboard-monitor.js50
-rw-r--r--ext/bg/js/context.js10
-rw-r--r--ext/bg/js/database.js7
-rw-r--r--ext/bg/js/deinflector.js4
-rw-r--r--ext/bg/js/dictionary.js105
-rw-r--r--ext/bg/js/handlebars.js8
-rw-r--r--ext/bg/js/japanese.js53
-rw-r--r--ext/bg/js/options.js181
-rw-r--r--ext/bg/js/search-frontend.js6
-rw-r--r--ext/bg/js/search-query-parser-generator.js5
-rw-r--r--ext/bg/js/search-query-parser.js18
-rw-r--r--ext/bg/js/search.js182
-rw-r--r--ext/bg/js/settings/anki-templates.js38
-rw-r--r--ext/bg/js/settings/anki.js19
-rw-r--r--ext/bg/js/settings/audio-ui.js6
-rw-r--r--ext/bg/js/settings/audio.js39
-rw-r--r--ext/bg/js/settings/backup.js24
-rw-r--r--ext/bg/js/settings/conditions-ui.js34
-rw-r--r--ext/bg/js/settings/dictionaries.js48
-rw-r--r--ext/bg/js/settings/main.js32
-rw-r--r--ext/bg/js/settings/popup-preview-frame.js39
-rw-r--r--ext/bg/js/settings/profiles.js30
-rw-r--r--ext/bg/js/settings/storage.js6
-rw-r--r--ext/bg/js/translator.js52
-rw-r--r--ext/bg/js/util.js6
-rw-r--r--ext/bg/search.html2
-rw-r--r--ext/bg/settings.html3
-rw-r--r--ext/fg/float.html2
-rw-r--r--ext/fg/js/document.js6
-rw-r--r--ext/fg/js/float.js64
-rw-r--r--ext/fg/js/frontend-api-sender.js3
-rw-r--r--ext/fg/js/frontend-initialize.js10
-rw-r--r--ext/fg/js/frontend.js41
-rw-r--r--ext/fg/js/popup-nested.js4
-rw-r--r--ext/fg/js/popup-proxy-host.js67
-rw-r--r--ext/fg/js/popup-proxy.js4
-rw-r--r--ext/fg/js/popup.js7
-rw-r--r--ext/manifest.json2
-rw-r--r--ext/mixed/css/display.css29
-rw-r--r--ext/mixed/js/api.js8
-rw-r--r--ext/mixed/js/audio-system.js185
-rw-r--r--ext/mixed/js/audio.js178
-rw-r--r--ext/mixed/js/core.js28
-rw-r--r--ext/mixed/js/display-generator.js7
-rw-r--r--ext/mixed/js/display.js284
-rw-r--r--ext/mixed/js/scroll.js2
-rw-r--r--ext/mixed/js/text-scanner.js6
-rw-r--r--package-lock.json672
-rw-r--r--package.json7
-rw-r--r--test/data/html/test-document1.html264
-rw-r--r--test/data/html/test-stylesheet.css32
-rw-r--r--test/dictionary-validate.js8
-rw-r--r--test/lint/global-declarations.js105
-rw-r--r--test/schema-validate.js6
-rw-r--r--test/test-database.js46
-rw-r--r--test/test-document.js240
-rw-r--r--test/test-schema.js12
-rw-r--r--test/yomichan-test.js34
-rw-r--r--test/yomichan-vm.js174
67 files changed, 3029 insertions, 1326 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
index fcc6995b..db8ff1fa 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -22,6 +22,7 @@
"dot-notation": "error",
"eqeqeq": "error",
"func-names": ["error", "always"],
+ "guard-for-in": "error",
"no-case-declarations": "error",
"no-const-assign": "error",
"no-constant-condition": "off",
@@ -62,7 +63,7 @@
"semi-spacing": ["error", {"before": false, "after": true}],
"space-in-parens": ["error", "never"],
"space-unary-ops": "error",
- "spaced-comment": ["error", "always", {"markers": ["global"]}],
+ "spaced-comment": ["error", "always"],
"switch-colon-spacing": ["error", {"after": true, "before": false}],
"template-curly-spacing": ["error", "never"],
"template-tag-spacing": ["error", "never"],
@@ -73,7 +74,7 @@
},
"overrides": [
{
- "files": ["*.js"],
+ "files": ["ext/**/*.js"],
"excludedFiles": ["ext/mixed/js/core.js"],
"globals": {
"yomichan": "readonly",
@@ -85,7 +86,6 @@
"toIterable": "readonly",
"stringReverse": "readonly",
"promiseTimeout": "readonly",
- "stringReplaceAsync": "readonly",
"parseUrl": "readonly",
"EventDispatcher": "readonly",
"EventListenerCollection": "readonly",
diff --git a/ext/bg/background.html b/ext/bg/background.html
index 7fd1c477..44abe8fd 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -22,9 +22,9 @@
<script src="/mixed/js/dom.js"></script>
<script src="/bg/js/anki.js"></script>
- <script src="/bg/js/api.js"></script>
+ <script src="/bg/js/anki-note-builder.js"></script>
<script src="/bg/js/mecab.js"></script>
- <script src="/bg/js/audio.js"></script>
+ <script src="/bg/js/audio-uri-builder.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>
@@ -39,7 +39,7 @@
<script src="/bg/js/request.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/audio-system.js"></script>
<script src="/bg/js/backend.js"></script>
</body>
diff --git a/ext/bg/data/default-anki-field-templates.handlebars b/ext/bg/data/default-anki-field-templates.handlebars
new file mode 100644
index 00000000..0442f7c5
--- /dev/null
+++ b/ext/bg/data/default-anki-field-templates.handlebars
@@ -0,0 +1,161 @@
+{{#*inline "glossary-single"}}
+ {{~#unless brief~}}
+ {{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}
+ {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
+ {{~/unless~}}
+ {{~#if glossary.[1]~}}
+ {{~#if compactGlossaries~}}
+ {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}}
+ {{~else~}}
+ <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul>
+ {{~/if~}}
+ {{~else~}}
+ {{~#multiLine}}{{glossary.[0]}}{{/multiLine~}}
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "audio"}}{{/inline}}
+
+{{#*inline "character"}}
+ {{~definition.character~}}
+{{/inline}}
+
+{{#*inline "dictionary"}}
+ {{~definition.dictionary~}}
+{{/inline}}
+
+{{#*inline "expression"}}
+ {{~#if merge~}}
+ {{~#if modeTermKana~}}
+ {{~#each definition.reading~}}
+ {{{.}}}
+ {{~#unless @last}}、{{/unless~}}
+ {{~else~}}
+ {{~#each definition.expression~}}
+ {{{.}}}
+ {{~#unless @last}}、{{/unless~}}
+ {{~/each~}}
+ {{~/each~}}
+ {{~else~}}
+ {{~#each definition.expression~}}
+ {{{.}}}
+ {{~#unless @last}}、{{/unless~}}
+ {{~/each~}}
+ {{~/if~}}
+ {{~else~}}
+ {{~#if modeTermKana~}}
+ {{~#if definition.reading~}}
+ {{definition.reading}}
+ {{~else~}}
+ {{definition.expression}}
+ {{~/if~}}
+ {{~else~}}
+ {{definition.expression}}
+ {{~/if~}}
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "furigana"}}
+ {{~#if merge~}}
+ {{~#each definition.expressions~}}
+ <span class="expression-{{termFrequency}}">{{~#furigana}}{{{.}}}{{/furigana~}}</span>
+ {{~#unless @last}}、{{/unless~}}
+ {{~/each~}}
+ {{~else~}}
+ {{#furigana}}{{{definition}}}{{/furigana}}
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "furigana-plain"}}
+ {{~#if merge~}}
+ {{~#each definition.expressions~}}
+ <span class="expression-{{termFrequency}}">{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}</span>
+ {{~#unless @last}}、{{/unless~}}
+ {{~/each~}}
+ {{~else~}}
+ {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}}
+ {{~/if~}}
+{{/inline}}
+
+{{#*inline "glossary"}}
+ <div style="text-align: left;">
+ {{~#if modeKanji~}}
+ {{~#if definition.glossary.[1]~}}
+ <ol>{{#each definition.glossary}}<li>{{.}}</li>{{/each}}</ol>
+ {{~else~}}
+ {{definition.glossary.[0]}}
+ {{~/if~}}
+ {{~else~}}
+ {{~#if group~}}
+ {{~#if definition.definitions.[1]~}}
+ <ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries}}</li>{{/each}}</ol>
+ {{~else~}}
+ {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries~}}
+ {{~/if~}}
+ {{~else if merge~}}
+ {{~#if definition.definitions.[1]~}}
+ <ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries}}</li>{{/each}}</ol>
+ {{~else~}}
+ {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries~}}
+ {{~/if~}}
+ {{~else~}}
+ {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries~}}
+ {{~/if~}}
+ {{~/if~}}
+ </div>
+{{/inline}}
+
+{{#*inline "glossary-brief"}}
+ {{~> glossary brief=true ~}}
+{{/inline}}
+
+{{#*inline "kunyomi"}}
+ {{~#each definition.kunyomi}}{{.}}{{#unless @last}}, {{/unless}}{{/each~}}
+{{/inline}}
+
+{{#*inline "onyomi"}}
+ {{~#each definition.onyomi}}{{.}}{{#unless @last}}, {{/unless}}{{/each~}}
+{{/inline}}
+
+{{#*inline "reading"}}
+ {{~#unless modeTermKana~}}
+ {{~#if merge~}}
+ {{~#each definition.reading~}}
+ {{{.}}}
+ {{~#unless @last}}、{{/unless~}}
+ {{~/each~}}
+ {{~else~}}
+ {{~definition.reading~}}
+ {{~/if~}}
+ {{~/unless~}}
+{{/inline}}
+
+{{#*inline "sentence"}}
+ {{~#if definition.cloze}}{{definition.cloze.sentence}}{{/if~}}
+{{/inline}}
+
+{{#*inline "cloze-prefix"}}
+ {{~#if definition.cloze}}{{definition.cloze.prefix}}{{/if~}}
+{{/inline}}
+
+{{#*inline "cloze-body"}}
+ {{~#if definition.cloze}}{{definition.cloze.body}}{{/if~}}
+{{/inline}}
+
+{{#*inline "cloze-suffix"}}
+ {{~#if definition.cloze}}{{definition.cloze.suffix}}{{/if~}}
+{{/inline}}
+
+{{#*inline "tags"}}
+ {{~#each definition.definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each~}}
+{{/inline}}
+
+{{#*inline "url"}}
+ <a href="{{definition.url}}">{{definition.url}}</a>
+{{/inline}}
+
+{{#*inline "screenshot"}}
+ <img src="{{definition.screenshotFileName}}" />
+{{/inline}}
+
+{{~> (lookup . "marker") ~}} \ No newline at end of file
diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js
new file mode 100644
index 00000000..d0ff8205
--- /dev/null
+++ b/ext/bg/js/anki-note-builder.js
@@ -0,0 +1,100 @@
+/*
+ * 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/>.
+ */
+
+class AnkiNoteBuilder {
+ constructor({renderTemplate}) {
+ this._renderTemplate = renderTemplate;
+ }
+
+ async createNote(definition, mode, options, templates) {
+ const isKanji = (mode === 'kanji');
+ const tags = options.anki.tags;
+ const modeOptions = isKanji ? options.anki.kanji : options.anki.terms;
+ const modeOptionsFieldEntries = Object.entries(modeOptions.fields);
+
+ const note = {
+ fields: {},
+ tags,
+ deckName: modeOptions.deck,
+ modelName: modeOptions.model
+ };
+
+ for (const [fieldName, fieldValue] of modeOptionsFieldEntries) {
+ note.fields[fieldName] = await this.formatField(fieldValue, definition, mode, options, templates, null);
+ }
+
+ if (!isKanji && definition.audio) {
+ const audioFields = [];
+
+ for (const [fieldName, fieldValue] of modeOptionsFieldEntries) {
+ if (fieldValue.includes('{audio}')) {
+ audioFields.push(fieldName);
+ }
+ }
+
+ if (audioFields.length > 0) {
+ note.audio = {
+ url: definition.audio.url,
+ filename: definition.audio.filename,
+ skipHash: '7e2c2f954ef6051373ba916f000168dc', // hash of audio data that should be skipped
+ fields: audioFields
+ };
+ }
+ }
+
+ return note;
+ }
+
+ async formatField(field, definition, mode, options, templates, errors=null) {
+ const data = {
+ marker: null,
+ definition,
+ group: options.general.resultOutputMode === 'group',
+ merge: options.general.resultOutputMode === 'merge',
+ modeTermKanji: mode === 'term-kanji',
+ modeTermKana: mode === 'term-kana',
+ modeKanji: mode === 'kanji',
+ compactGlossaries: options.general.compactGlossaries
+ };
+ const pattern = /\{([\w-]+)\}/g;
+ return await AnkiNoteBuilder.stringReplaceAsync(field, pattern, async (g0, marker) => {
+ data.marker = marker;
+ try {
+ return await this._renderTemplate(templates, data);
+ } catch (e) {
+ if (errors) { errors.push(e); }
+ return `{${marker}-render-error}`;
+ }
+ });
+ }
+
+ static stringReplaceAsync(str, regex, replacer) {
+ let match;
+ let index = 0;
+ const parts = [];
+ while ((match = regex.exec(str)) !== null) {
+ parts.push(str.substring(index, match.index), replacer(...match, match.index, str));
+ index = regex.lastIndex;
+ }
+ if (parts.length === 0) {
+ return Promise.resolve(str);
+ }
+ parts.push(str.substring(index));
+ return Promise.all(parts).then((v) => v.join(''));
+ }
+}
diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js
index 39c6ad51..a70388bd 100644
--- a/ext/bg/js/anki.js
+++ b/ext/bg/js/anki.js
@@ -16,7 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global requestJson*/
+/* global
+ * requestJson
+ */
/*
* AnkiConnect
diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js
deleted file mode 100644
index 0c244ffa..00000000
--- a/ext/bg/js/api.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2019-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/>.
- */
-
-
-function apiTemplateRender(template, data) {
- return _apiInvoke('templateRender', {data, template});
-}
-
-function apiAudioGetUrl(definition, source, optionsContext) {
- return _apiInvoke('audioGetUrl', {definition, source, optionsContext});
-}
-
-function apiClipboardGet() {
- return _apiInvoke('clipboardGet');
-}
-
-function _apiInvoke(action, params={}) {
- const data = {action, params};
- return new Promise((resolve, reject) => {
- try {
- const callback = (response) => {
- if (response !== null && typeof response === 'object') {
- if (typeof response.error !== 'undefined') {
- reject(jsonToError(response.error));
- } else {
- resolve(response.result);
- }
- } else {
- const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`;
- reject(new Error(`${message} (${JSON.stringify(data)})`));
- }
- };
- const backend = window.yomichanBackend;
- backend.onMessage({action, params}, null, callback);
- } catch (e) {
- reject(e);
- yomichan.triggerOrphaned(e);
- }
- });
-}
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio-uri-builder.js
index d300570b..499c3441 100644
--- a/ext/bg/js/audio.js
+++ b/ext/bg/js/audio-uri-builder.js
@@ -16,10 +16,53 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global jpIsStringEntirelyKana, audioGetFromSources*/
+/* global
+ * jpIsStringEntirelyKana
+ */
+
+class AudioUriBuilder {
+ constructor() {
+ this._getUrlHandlers = new Map([
+ ['jpod101', this._getUriJpod101.bind(this)],
+ ['jpod101-alternate', this._getUriJpod101Alternate.bind(this)],
+ ['jisho', this._getUriJisho.bind(this)],
+ ['text-to-speech', this._getUriTextToSpeech.bind(this)],
+ ['text-to-speech-reading', this._getUriTextToSpeechReading.bind(this)],
+ ['custom', this._getUriCustom.bind(this)]
+ ]);
+ }
+
+ normalizeUrl(url, baseUrl, basePath) {
+ if (url) {
+ if (url[0] === '/') {
+ if (url.length >= 2 && url[1] === '/') {
+ // Begins with "//"
+ url = baseUrl.substring(0, baseUrl.indexOf(':') + 1) + url;
+ } else {
+ // Begins with "/"
+ url = baseUrl + url;
+ }
+ } else if (!/^[a-z][a-z0-9\-+.]*:/i.test(url)) {
+ // No URI scheme => relative path
+ url = baseUrl + basePath + url;
+ }
+ }
+ return url;
+ }
-const audioUrlBuilders = new Map([
- ['jpod101', async (definition) => {
+ async getUri(definition, source, options) {
+ const handler = this._getUrlHandlers.get(source);
+ if (typeof handler === 'function') {
+ try {
+ return await handler(definition, options);
+ } catch (e) {
+ // NOP
+ }
+ }
+ return null;
+ }
+
+ async _getUriJpod101(definition) {
let kana = definition.reading;
let kanji = definition.expression;
@@ -37,8 +80,9 @@ const audioUrlBuilders = new Map([
}
return `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`;
- }],
- ['jpod101-alternate', async (definition) => {
+ }
+
+ async _getUriJpod101Alternate(definition) {
const response = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post');
@@ -54,7 +98,7 @@ const audioUrlBuilders = new Map([
const url = row.querySelector('audio>source[src]').getAttribute('src');
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/');
+ return this.normalizeUrl(url, 'https://www.japanesepod101.com', '/learningcenter/reference/');
}
} catch (e) {
// NOP
@@ -62,8 +106,9 @@ const audioUrlBuilders = new Map([
}
throw new Error('Failed to find audio URL');
- }],
- ['jisho', async (definition) => {
+ }
+
+ async _getUriJisho(definition) {
const response = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `https://jisho.org/search/${definition.expression}`);
@@ -78,7 +123,7 @@ const audioUrlBuilders = new Map([
if (audio !== null) {
const url = audio.getElementsByTagName('source').item(0).getAttribute('src');
if (url) {
- return audioUrlNormalize(url, 'https://jisho.org', '/search/');
+ return this.normalizeUrl(url, 'https://jisho.org', '/search/');
}
}
} catch (e) {
@@ -86,101 +131,28 @@ const audioUrlBuilders = new Map([
}
throw new Error('Failed to find audio URL');
- }],
- ['text-to-speech', async (definition, options) => {
+ }
+
+ async _getUriTextToSpeech(definition, options) {
const voiceURI = options.audio.textToSpeechVoice;
if (!voiceURI) {
throw new Error('No voice');
}
return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`;
- }],
- ['text-to-speech-reading', async (definition, options) => {
+ }
+
+ async _getUriTextToSpeechReading(definition, options) {
const voiceURI = options.audio.textToSpeechVoice;
if (!voiceURI) {
throw new Error('No voice');
}
return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`;
- }],
- ['custom', async (definition, options) => {
- const customSourceUrl = options.audio.customSourceUrl;
- return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0));
- }]
-]);
-
-async function audioGetUrl(definition, mode, options, download) {
- const handler = audioUrlBuilders.get(mode);
- if (typeof handler === 'function') {
- try {
- return await handler(definition, options, download);
- } catch (e) {
- // NOP
- }
- }
- return null;
-}
-
-function audioUrlNormalize(url, baseUrl, basePath) {
- if (url) {
- if (url[0] === '/') {
- if (url.length >= 2 && url[1] === '/') {
- // Begins with "//"
- url = baseUrl.substring(0, baseUrl.indexOf(':') + 1) + url;
- } else {
- // Begins with "/"
- url = baseUrl + url;
- }
- } else if (!/^[a-z][a-z0-9\-+.]*:/i.test(url)) {
- // No URI scheme => relative path
- url = baseUrl + basePath + url;
- }
- }
- return url;
-}
-
-function audioBuildFilename(definition) {
- if (definition.reading || definition.expression) {
- let filename = 'yomichan';
- if (definition.reading) {
- filename += `_${definition.reading}`;
- }
- if (definition.expression) {
- filename += `_${definition.expression}`;
- }
-
- return filename += '.mp3';
- }
- return null;
-}
-
-async function audioInject(definition, fields, sources, optionsContext) {
- let usesAudio = false;
- for (const name in fields) {
- if (fields[name].includes('{audio}')) {
- usesAudio = true;
- break;
- }
- }
-
- if (!usesAudio) {
- return true;
}
- try {
- const expressions = definition.expressions;
- const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition;
-
- const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, true);
- if (url !== null) {
- const filename = audioBuildFilename(audioSourceDefinition);
- if (filename !== null) {
- definition.audio = {url, filename};
- }
- }
-
- return true;
- } catch (e) {
- return false;
+ async _getUriCustom(definition, options) {
+ const customSourceUrl = options.audio.customSourceUrl;
+ return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0));
}
}
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index e3bf7bda..978c5a4a 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -16,30 +16,51 @@
* 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*/
+/* global
+ * AnkiConnect
+ * AnkiNoteBuilder
+ * AnkiNull
+ * AudioSystem
+ * AudioUriBuilder
+ * BackendApiForwarder
+ * ClipboardMonitor
+ * JsonSchema
+ * Mecab
+ * Translator
+ * conditionsTestValue
+ * dictConfigured
+ * dictEnabledSet
+ * dictTermsSort
+ * handlebarsRenderDynamic
+ * jpConvertReading
+ * jpDistributeFuriganaInflected
+ * jpKatakanaToHiragana
+ * optionsLoad
+ * optionsSave
+ * profileConditionsDescriptor
+ * requestJson
+ * requestText
+ * utilIsolate
+ */
class Backend {
constructor() {
this.translator = new Translator();
this.anki = new AnkiNull();
this.mecab = new Mecab();
- this.clipboardMonitor = new ClipboardMonitor();
+ this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)});
+ this.ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: this._renderTemplate.bind(this)});
this.options = null;
this.optionsSchema = null;
+ this.defaultAnkiFieldTemplates = null;
+ this.audioSystem = new AudioSystem({getAudioUri: this._getAudioUri.bind(this)});
+ this.audioUriBuilder = new AudioUriBuilder();
this.optionsContext = {
depth: 0,
url: window.location.href
};
- this.isPreparedResolve = null;
- this.isPreparedPromise = new Promise((resolve) => (this.isPreparedResolve = resolve));
+ this.isPrepared = false;
this.clipboardPasteTarget = document.querySelector('#clipboard-paste-target');
@@ -48,12 +69,50 @@ class Backend {
this.apiForwarder = new BackendApiForwarder();
this.messageToken = yomichan.generateId(16);
+
+ this._messageHandlers = new Map([
+ ['yomichanCoreReady', this._onApiYomichanCoreReady.bind(this)],
+ ['optionsSchemaGet', this._onApiOptionsSchemaGet.bind(this)],
+ ['optionsGet', this._onApiOptionsGet.bind(this)],
+ ['optionsGetFull', this._onApiOptionsGetFull.bind(this)],
+ ['optionsSet', this._onApiOptionsSet.bind(this)],
+ ['optionsSave', this._onApiOptionsSave.bind(this)],
+ ['kanjiFind', this._onApiKanjiFind.bind(this)],
+ ['termsFind', this._onApiTermsFind.bind(this)],
+ ['textParse', this._onApiTextParse.bind(this)],
+ ['textParseMecab', this._onApiTextParseMecab.bind(this)],
+ ['definitionAdd', this._onApiDefinitionAdd.bind(this)],
+ ['definitionsAddable', this._onApiDefinitionsAddable.bind(this)],
+ ['noteView', this._onApiNoteView.bind(this)],
+ ['templateRender', this._onApiTemplateRender.bind(this)],
+ ['commandExec', this._onApiCommandExec.bind(this)],
+ ['audioGetUri', this._onApiAudioGetUri.bind(this)],
+ ['screenshotGet', this._onApiScreenshotGet.bind(this)],
+ ['forward', this._onApiForward.bind(this)],
+ ['frameInformationGet', this._onApiFrameInformationGet.bind(this)],
+ ['injectStylesheet', this._onApiInjectStylesheet.bind(this)],
+ ['getEnvironmentInfo', this._onApiGetEnvironmentInfo.bind(this)],
+ ['clipboardGet', this._onApiClipboardGet.bind(this)],
+ ['getDisplayTemplatesHtml', this._onApiGetDisplayTemplatesHtml.bind(this)],
+ ['getQueryParserTemplatesHtml', this._onApiGetQueryParserTemplatesHtml.bind(this)],
+ ['getZoom', this._onApiGetZoom.bind(this)],
+ ['getMessageToken', this._onApiGetMessageToken.bind(this)],
+ ['getDefaultAnkiFieldTemplates', this._onApiGetDefaultAnkiFieldTemplates.bind(this)]
+ ]);
+
+ this._commandHandlers = new Map([
+ ['search', this._onCommandSearch.bind(this)],
+ ['help', this._onCommandHelp.bind(this)],
+ ['options', this._onCommandOptions.bind(this)],
+ ['toggle', this._onCommandToggle.bind(this)]
+ ]);
}
async prepare() {
await this.translator.prepare();
this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET');
+ this.defaultAnkiFieldTemplates = await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET');
this.options = await optionsLoad();
try {
this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, this.options);
@@ -65,42 +124,47 @@ class Backend {
this.onOptionsUpdated('background');
if (isObject(chrome.commands) && isObject(chrome.commands.onCommand)) {
- chrome.commands.onCommand.addListener((command) => this._runCommand(command));
+ chrome.commands.onCommand.addListener(this._runCommand.bind(this));
}
if (isObject(chrome.tabs) && isObject(chrome.tabs.onZoomChange)) {
- chrome.tabs.onZoomChange.addListener((info) => this._onZoomChange(info));
+ chrome.tabs.onZoomChange.addListener(this._onZoomChange.bind(this));
}
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
- const options = this.getOptionsSync(this.optionsContext);
+ this.isPrepared = true;
+
+ const options = this.getOptions(this.optionsContext);
if (options.general.showGuide) {
chrome.tabs.create({url: chrome.runtime.getURL('/bg/guide.html')});
}
- this.isPreparedResolve();
- this.isPreparedResolve = null;
- this.isPreparedPromise = null;
+ this.clipboardMonitor.on('change', this._onClipboardText.bind(this));
- this.clipboardMonitor.onClipboardText = (text) => this._onClipboardText(text);
+ this._sendMessageAllTabs('backendPrepared');
+ const callback = () => this.checkLastError(chrome.runtime.lastError);
+ chrome.runtime.sendMessage({action: 'backendPrepared'}, callback);
}
- onOptionsUpdated(source) {
- this.applyOptions();
-
+ _sendMessageAllTabs(action, params={}) {
const callback = () => this.checkLastError(chrome.runtime.lastError);
chrome.tabs.query({}, (tabs) => {
for (const tab of tabs) {
- chrome.tabs.sendMessage(tab.id, {action: 'optionsUpdated', params: {source}}, callback);
+ chrome.tabs.sendMessage(tab.id, {action, params}, callback);
}
});
}
+ onOptionsUpdated(source) {
+ this.applyOptions();
+ this._sendMessageAllTabs('optionsUpdated', {source});
+ }
+
onMessage({action, params}, sender, callback) {
- const handler = Backend._messageHandlers.get(action);
+ const handler = this._messageHandlers.get(action);
if (typeof handler !== 'function') { return false; }
try {
- const promise = handler(this, params, sender);
+ const promise = handler(params, sender);
promise.then(
(result) => callback({result}),
(error) => callback({error: errorToJson(error)})
@@ -112,7 +176,7 @@ class Backend {
}
}
- _onClipboardText(text) {
+ _onClipboardText({text}) {
this._onCommandSearch({mode: 'popup', query: text});
}
@@ -122,7 +186,7 @@ class Backend {
}
applyOptions() {
- const options = this.getOptionsSync(this.optionsContext);
+ const options = this.getOptions(this.optionsContext);
if (!options.general.enable) {
this.setExtensionBadgeBackgroundColor('#555555');
this.setExtensionBadgeText('off');
@@ -148,24 +212,15 @@ class Backend {
}
}
- async getOptionsSchema() {
- if (this.isPreparedPromise !== null) {
- await this.isPreparedPromise;
- }
+ getOptionsSchema() {
return this.optionsSchema;
}
- async getFullOptions() {
- if (this.isPreparedPromise !== null) {
- await this.isPreparedPromise;
- }
+ getFullOptions() {
return this.options;
}
- async setFullOptions(options) {
- if (this.isPreparedPromise !== null) {
- await this.isPreparedPromise;
- }
+ setFullOptions(options) {
try {
this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, utilIsolate(options));
} catch (e) {
@@ -174,18 +229,11 @@ class Backend {
}
}
- async getOptions(optionsContext) {
- if (this.isPreparedPromise !== null) {
- await this.isPreparedPromise;
- }
- return this.getOptionsSync(optionsContext);
- }
-
- getOptionsSync(optionsContext) {
- return this.getProfileSync(optionsContext).options;
+ getOptions(optionsContext) {
+ return this.getProfile(optionsContext).options;
}
- getProfileSync(optionsContext) {
+ getProfile(optionsContext) {
const profiles = this.options.profiles;
if (typeof optionsContext.index === 'number') {
return profiles[optionsContext.index];
@@ -243,29 +291,43 @@ class Backend {
}
_runCommand(command, params) {
- const handler = Backend._commandHandlers.get(command);
+ const handler = this._commandHandlers.get(command);
if (typeof handler !== 'function') { return false; }
- handler(this, params);
+ handler(params);
return true;
}
// Message handlers
- _onApiOptionsSchemaGet() {
+ _onApiYomichanCoreReady(_params, sender) {
+ // tab ID isn't set in background (e.g. browser_action)
+ if (typeof sender.tab === 'undefined') {
+ const callback = () => this.checkLastError(chrome.runtime.lastError);
+ chrome.runtime.sendMessage({action: 'backendPrepared'}, callback);
+ return Promise.resolve();
+ }
+
+ const tabId = sender.tab.id;
+ return new Promise((resolve) => {
+ chrome.tabs.sendMessage(tabId, {action: 'backendPrepared'}, resolve);
+ });
+ }
+
+ async _onApiOptionsSchemaGet() {
return this.getOptionsSchema();
}
- _onApiOptionsGet({optionsContext}) {
+ async _onApiOptionsGet({optionsContext}) {
return this.getOptions(optionsContext);
}
- _onApiOptionsGetFull() {
+ async _onApiOptionsGetFull() {
return this.getFullOptions();
}
async _onApiOptionsSet({changedOptions, optionsContext, source}) {
- const options = await this.getOptions(optionsContext);
+ const options = this.getOptions(optionsContext);
function getValuePaths(obj) {
const valuePaths = [];
@@ -305,20 +367,20 @@ class Backend {
}
async _onApiOptionsSave({source}) {
- const options = await this.getFullOptions();
+ const options = this.getFullOptions();
await optionsSave(options);
this.onOptionsUpdated(source);
}
async _onApiKanjiFind({text, optionsContext}) {
- const options = await this.getOptions(optionsContext);
+ const options = this.getOptions(optionsContext);
const definitions = await this.translator.findKanji(text, options);
definitions.splice(options.general.maxResults);
return definitions;
}
async _onApiTermsFind({text, details, optionsContext}) {
- const options = await this.getOptions(optionsContext);
+ const options = this.getOptions(optionsContext);
const mode = options.general.resultOutputMode;
const [definitions, length] = await this.translator.findTerms(mode, text, details, options);
definitions.splice(options.general.maxResults);
@@ -326,7 +388,7 @@ class Backend {
}
async _onApiTextParse({text, optionsContext}) {
- const options = await this.getOptions(optionsContext);
+ const options = this.getOptions(optionsContext);
const results = [];
while (text.length > 0) {
const term = [];
@@ -356,12 +418,12 @@ class Backend {
}
async _onApiTextParseMecab({text, optionsContext}) {
- const options = await this.getOptions(optionsContext);
- const results = {};
+ const options = this.getOptions(optionsContext);
+ const results = [];
const rawResults = await this.mecab.parseText(text);
- for (const mecabName in rawResults) {
+ for (const [mecabName, parsedLines] of Object.entries(rawResults)) {
const result = [];
- for (const parsedLine of rawResults[mecabName]) {
+ for (const parsedLine of parsedLines) {
for (const {expression, reading, source} of parsedLine) {
const term = [];
if (expression !== null && reading !== null) {
@@ -381,17 +443,17 @@ class Backend {
}
result.push([{text: '\n'}]);
}
- results[mecabName] = result;
+ results.push([mecabName, result]);
}
return results;
}
async _onApiDefinitionAdd({definition, mode, context, optionsContext}) {
- const options = await this.getOptions(optionsContext);
- const templates = Backend._getTemplates(options);
+ const options = this.getOptions(optionsContext);
+ const templates = this.defaultAnkiFieldTemplates;
if (mode !== 'kanji') {
- await audioInject(
+ await this._audioInject(
definition,
options.anki.terms.fields,
options.audio.sources,
@@ -407,20 +469,20 @@ class Backend {
);
}
- const note = await dictNoteFormat(definition, mode, options, templates);
+ const note = await this.ankiNoteBuilder.createNote(definition, mode, options, templates);
return this.anki.addNote(note);
}
async _onApiDefinitionsAddable({definitions, modes, optionsContext}) {
- const options = await this.getOptions(optionsContext);
- const templates = Backend._getTemplates(options);
+ const options = this.getOptions(optionsContext);
+ const templates = this.defaultAnkiFieldTemplates;
const states = [];
try {
const notes = [];
for (const definition of definitions) {
for (const mode of modes) {
- const note = await dictNoteFormat(definition, mode, options, templates);
+ const note = await this.ankiNoteBuilder.createNote(definition, mode, options, templates);
notes.push(note);
}
}
@@ -459,20 +521,20 @@ class Backend {
}
async _onApiNoteView({noteId}) {
- return this.anki.guiBrowse(`nid:${noteId}`);
+ return await this.anki.guiBrowse(`nid:${noteId}`);
}
async _onApiTemplateRender({template, data}) {
- return handlebarsRenderDynamic(template, data);
+ return this._renderTemplate(template, data);
}
async _onApiCommandExec({command, params}) {
return this._runCommand(command, params);
}
- async _onApiAudioGetUrl({definition, source, optionsContext}) {
- const options = await this.getOptions(optionsContext);
- return await audioGetUrl(definition, source, options);
+ async _onApiAudioGetUri({definition, source, optionsContext}) {
+ const options = this.getOptions(optionsContext);
+ return await this.audioUriBuilder.getUri(definition, source, options);
}
_onApiScreenshotGet({options}, sender) {
@@ -621,12 +683,16 @@ class Backend {
return this.messageToken;
}
+ async _onApiGetDefaultAnkiFieldTemplates() {
+ return this.defaultAnkiFieldTemplates;
+ }
+
// Command handlers
async _onCommandSearch(params) {
const {mode='existingOrNewTab', query} = params || {};
- const options = await this.getOptions(this.optionsContext);
+ const options = this.getOptions(this.optionsContext);
const {popupWidth, popupHeight} = options.general;
const baseUrl = chrome.runtime.getURL('/bg/search.html');
@@ -647,7 +713,7 @@ class Backend {
await Backend._focusTab(tab);
if (queryParams.query) {
await new Promise((resolve) => chrome.tabs.sendMessage(
- tab.id, {action: 'searchQueryUpdate', params: {query: queryParams.query}}, resolve
+ tab.id, {action: 'searchQueryUpdate', params: {text: queryParams.query}}, resolve
));
}
return true;
@@ -693,9 +759,10 @@ class Backend {
}
_onCommandOptions(params) {
- if (!(params && params.newTab)) {
+ const {mode='existingOrNewTab'} = params || {};
+ if (mode === 'existingOrNewTab') {
chrome.runtime.openOptionsPage();
- } else {
+ } else if (mode === 'newTab') {
const manifest = chrome.runtime.getManifest();
const url = chrome.runtime.getURL(manifest.options_ui.page);
chrome.tabs.create({url});
@@ -709,17 +776,56 @@ class Backend {
};
const source = 'popup';
- const options = await this.getOptions(optionsContext);
+ const options = this.getOptions(optionsContext);
options.general.enable = !options.general.enable;
await this._onApiOptionsSave({source});
}
// Utilities
+ async _getAudioUri(definition, source, details) {
+ let optionsContext = (typeof details === 'object' && details !== null ? details.optionsContext : null);
+ if (!(typeof optionsContext === 'object' && optionsContext !== null)) {
+ optionsContext = this.optionsContext;
+ }
+
+ const options = this.getOptions(optionsContext);
+ return await this.audioUriBuilder.getUri(definition, source, options);
+ }
+
+ async _audioInject(definition, fields, sources, optionsContext) {
+ let usesAudio = false;
+ for (const fieldValue of Object.values(fields)) {
+ if (fieldValue.includes('{audio}')) {
+ usesAudio = true;
+ break;
+ }
+ }
+
+ if (!usesAudio) {
+ return true;
+ }
+
+ try {
+ const expressions = definition.expressions;
+ const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition;
+
+ const {uri} = await this.audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext});
+ const filename = this._createInjectedAudioFileName(audioSourceDefinition);
+ if (filename !== null) {
+ definition.audio = {url: uri, filename};
+ }
+
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
async _injectScreenshot(definition, fields, screenshot) {
let usesScreenshot = false;
- for (const name in fields) {
- if (fields[name].includes('{screenshot}')) {
+ for (const fieldValue of Object.values(fields)) {
+ if (fieldValue.includes('{screenshot}')) {
usesScreenshot = true;
break;
}
@@ -752,6 +858,21 @@ class Backend {
definition.screenshotFileName = filename;
}
+ async _renderTemplate(template, data) {
+ return handlebarsRenderDynamic(template, data);
+ }
+
+ _createInjectedAudioFileName(definition) {
+ const {reading, expression} = definition;
+ if (!reading && !expression) { return null; }
+
+ let filename = 'yomichan';
+ if (reading) { filename += `_${reading}`; }
+ if (expression) { filename += `_${expression}`; }
+ filename += '.mp3';
+ return filename;
+ }
+
static _getTabUrl(tab) {
return new Promise((resolve) => {
chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => {
@@ -860,47 +981,7 @@ class Backend {
return 'chrome';
}
}
-
- static _getTemplates(options) {
- const templates = options.anki.fieldTemplates;
- return typeof templates === 'string' ? templates : profileOptionsGetDefaultFieldTemplates();
- }
}
-Backend._messageHandlers = new Map([
- ['optionsSchemaGet', (self, ...args) => self._onApiOptionsSchemaGet(...args)],
- ['optionsGet', (self, ...args) => self._onApiOptionsGet(...args)],
- ['optionsGetFull', (self, ...args) => self._onApiOptionsGetFull(...args)],
- ['optionsSet', (self, ...args) => self._onApiOptionsSet(...args)],
- ['optionsSave', (self, ...args) => self._onApiOptionsSave(...args)],
- ['kanjiFind', (self, ...args) => self._onApiKanjiFind(...args)],
- ['termsFind', (self, ...args) => self._onApiTermsFind(...args)],
- ['textParse', (self, ...args) => self._onApiTextParse(...args)],
- ['textParseMecab', (self, ...args) => self._onApiTextParseMecab(...args)],
- ['definitionAdd', (self, ...args) => self._onApiDefinitionAdd(...args)],
- ['definitionsAddable', (self, ...args) => self._onApiDefinitionsAddable(...args)],
- ['noteView', (self, ...args) => self._onApiNoteView(...args)],
- ['templateRender', (self, ...args) => self._onApiTemplateRender(...args)],
- ['commandExec', (self, ...args) => self._onApiCommandExec(...args)],
- ['audioGetUrl', (self, ...args) => self._onApiAudioGetUrl(...args)],
- ['screenshotGet', (self, ...args) => self._onApiScreenshotGet(...args)],
- ['forward', (self, ...args) => self._onApiForward(...args)],
- ['frameInformationGet', (self, ...args) => self._onApiFrameInformationGet(...args)],
- ['injectStylesheet', (self, ...args) => self._onApiInjectStylesheet(...args)],
- ['getEnvironmentInfo', (self, ...args) => self._onApiGetEnvironmentInfo(...args)],
- ['clipboardGet', (self, ...args) => self._onApiClipboardGet(...args)],
- ['getDisplayTemplatesHtml', (self, ...args) => self._onApiGetDisplayTemplatesHtml(...args)],
- ['getQueryParserTemplatesHtml', (self, ...args) => self._onApiGetQueryParserTemplatesHtml(...args)],
- ['getZoom', (self, ...args) => self._onApiGetZoom(...args)],
- ['getMessageToken', (self, ...args) => self._onApiGetMessageToken(...args)]
-]);
-
-Backend._commandHandlers = new Map([
- ['search', (self, ...args) => self._onCommandSearch(...args)],
- ['help', (self, ...args) => self._onCommandHelp(...args)],
- ['options', (self, ...args) => self._onCommandOptions(...args)],
- ['toggle', (self, ...args) => self._onCommandToggle(...args)]
-]);
-
window.yomichanBackend = new Backend();
window.yomichanBackend.prepare();
diff --git a/ext/bg/js/clipboard-monitor.js b/ext/bg/js/clipboard-monitor.js
index c2f41385..9a881f57 100644
--- a/ext/bg/js/clipboard-monitor.js
+++ b/ext/bg/js/clipboard-monitor.js
@@ -16,66 +16,66 @@
* 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;
- }
+/* global
+ * jpIsStringPartiallyJapanese
+ */
- onClipboardText(_text) {
- throw new Error('Override me');
+class ClipboardMonitor extends EventDispatcher {
+ constructor({getClipboard}) {
+ super();
+ this._timerId = null;
+ this._timerToken = null;
+ this._interval = 250;
+ this._previousText = null;
+ this._getClipboard = getClipboard;
}
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()
+ // hasn't been started during the await call. The check below the await this._getClipboard()
// call will exit early if the reference has changed.
const token = {};
const intervalCallback = async () => {
- this.timerId = null;
+ this._timerId = null;
let text = null;
try {
- text = await apiClipboardGet();
+ text = await this._getClipboard();
} catch (e) {
// NOP
}
- if (this.timerToken !== token) { return; }
+ if (this._timerToken !== token) { return; }
if (
typeof text === 'string' &&
(text = text.trim()).length > 0 &&
- text !== this.previousText
+ text !== this._previousText
) {
- this.previousText = text;
+ this._previousText = text;
if (jpIsStringPartiallyJapanese(text)) {
- this.onClipboardText(text);
+ this.trigger('change', {text});
}
}
- this.timerId = setTimeout(intervalCallback, this.interval);
+ this._timerId = setTimeout(intervalCallback, this._interval);
};
- this.timerToken = token;
+ this._timerToken = token;
intervalCallback();
}
stop() {
- this.timerToken = null;
- if (this.timerId !== null) {
- clearTimeout(this.timerId);
- this.timerId = null;
+ this._timerToken = null;
+ if (this._timerId !== null) {
+ clearTimeout(this._timerId);
+ this._timerId = null;
}
}
setPreviousText(text) {
- this.previousText = text;
+ this._previousText = text;
}
}
diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js
index bec964fb..c3e74656 100644
--- a/ext/bg/js/context.js
+++ b/ext/bg/js/context.js
@@ -16,7 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiCommandExec, apiGetEnvironmentInfo, apiOptionsGet*/
+/* global
+ * apiCommandExec
+ * apiGetEnvironmentInfo
+ * apiOptionsGet
+ */
function showExtensionInfo() {
const node = document.getElementById('extension-info');
@@ -48,7 +52,9 @@ function setupButtonEvents(selector, command, url) {
}
}
-window.addEventListener('DOMContentLoaded', () => {
+window.addEventListener('DOMContentLoaded', async () => {
+ await yomichan.prepare();
+
showExtensionInfo();
apiGetEnvironmentInfo().then(({browser}) => {
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index 558f3ceb..08a2a39f 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -16,7 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global dictFieldSplit, requestJson, JsonSchema, JSZip*/
+/* global
+ * JSZip
+ * JsonSchema
+ * dictFieldSplit
+ * requestJson
+ */
class Database {
constructor() {
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
index e2ced965..d548d271 100644
--- a/ext/bg/js/deinflector.js
+++ b/ext/bg/js/deinflector.js
@@ -57,9 +57,9 @@ class Deinflector {
static normalizeReasons(reasons) {
const normalizedReasons = [];
- for (const reason in reasons) {
+ for (const [reason, reasonInfo] of Object.entries(reasons)) {
const variants = [];
- for (const {kanaIn, kanaOut, rulesIn, rulesOut} of reasons[reason]) {
+ for (const {kanaIn, kanaOut, rulesIn, rulesOut} of reasonInfo) {
variants.push([
kanaIn,
kanaOut,
diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js
index f5c5b21b..3dd1d0c1 100644
--- a/ext/bg/js/dictionary.js
+++ b/ext/bg/js/dictionary.js
@@ -16,26 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiTemplateRender*/
-
function dictEnabledSet(options) {
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
- });
+ for (const [title, {enabled, priority, allowSecondarySearches}] of Object.entries(options.dictionaries)) {
+ if (!enabled) { continue; }
+ enabledDictionaryMap.set(title, {priority, allowSecondarySearches});
}
return enabledDictionaryMap;
}
function dictConfigured(options) {
- for (const title in options.dictionaries) {
- if (options.dictionaries[title].enabled) {
+ for (const {enabled} of Object.values(options.dictionaries)) {
+ if (enabled) {
return true;
}
}
@@ -339,90 +331,3 @@ function dictTagsSort(tags) {
function dictFieldSplit(field) {
return field.length === 0 ? [] : field.split(' ');
}
-
-async function dictFieldFormat(field, definition, mode, options, templates, exceptions) {
- const data = {
- marker: null,
- definition,
- group: options.general.resultOutputMode === 'group',
- merge: options.general.resultOutputMode === 'merge',
- modeTermKanji: mode === 'term-kanji',
- modeTermKana: mode === 'term-kana',
- modeKanji: mode === 'kanji',
- compactGlossaries: options.general.compactGlossaries
- };
- const markers = dictFieldFormat.markers;
- const pattern = /\{([\w-]+)\}/g;
- return await stringReplaceAsync(field, pattern, async (g0, marker) => {
- if (!markers.has(marker)) {
- return g0;
- }
- data.marker = marker;
- try {
- return await apiTemplateRender(templates, data);
- } catch (e) {
- if (exceptions) { exceptions.push(e); }
- return `{${marker}-render-error}`;
- }
- });
-}
-dictFieldFormat.markers = new Set([
- 'audio',
- 'character',
- 'cloze-body',
- 'cloze-prefix',
- 'cloze-suffix',
- 'dictionary',
- 'expression',
- 'furigana',
- 'furigana-plain',
- 'glossary',
- 'glossary-brief',
- 'kunyomi',
- 'onyomi',
- 'reading',
- 'screenshot',
- 'sentence',
- 'tags',
- 'url'
-]);
-
-async function dictNoteFormat(definition, mode, options, templates) {
- const note = {fields: {}, tags: options.anki.tags};
- let fields = [];
-
- if (mode === 'kanji') {
- fields = options.anki.kanji.fields;
- note.deckName = options.anki.kanji.deck;
- note.modelName = options.anki.kanji.model;
- } else {
- fields = options.anki.terms.fields;
- note.deckName = options.anki.terms.deck;
- note.modelName = options.anki.terms.model;
-
- if (definition.audio) {
- const audio = {
- url: definition.audio.url,
- filename: definition.audio.filename,
- skipHash: '7e2c2f954ef6051373ba916f000168dc',
- fields: []
- };
-
- for (const name in fields) {
- if (fields[name].includes('{audio}')) {
- audio.fields.push(name);
- }
- }
-
- if (audio.fields.length > 0) {
- note.audio = audio;
- }
- }
- }
-
- for (const name in fields) {
- note.fields[name] = await dictFieldFormat(fields[name], definition, mode, options, templates);
- }
-
- return note;
-}
diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js
index b1443447..e3ce6bd0 100644
--- a/ext/bg/js/handlebars.js
+++ b/ext/bg/js/handlebars.js
@@ -16,7 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global jpIsCharCodeKanji, jpDistributeFurigana, Handlebars*/
+/* global
+ * Handlebars
+ * jpDistributeFurigana
+ * jpIsCodePointKanji
+ */
function handlebarsEscape(text) {
return Handlebars.Utils.escapeExpression(text);
@@ -62,7 +66,7 @@ function handlebarsFuriganaPlain(options) {
function handlebarsKanjiLinks(options) {
let result = '';
for (const c of options.fn(this)) {
- if (jpIsCharCodeKanji(c.charCodeAt(0))) {
+ if (jpIsCodePointKanji(c.codePointAt(0))) {
result += `<a href="#" class="kanji-link">${c}</a>`;
} else {
result += c;
diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js
index abb32da4..3b37754d 100644
--- a/ext/bg/js/japanese.js
+++ b/ext/bg/js/japanese.js
@@ -16,7 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global wanakana*/
+/* global
+ * wanakana
+ */
const JP_HALFWIDTH_KATAKANA_MAPPING = new Map([
['ヲ', 'ヲヺ-'],
@@ -115,9 +117,9 @@ const JP_JAPANESE_RANGES = [
// Helper functions
-function _jpIsCharCodeInRanges(charCode, ranges) {
+function _jpIsCodePointInRanges(codePoint, ranges) {
for (const [min, max] of ranges) {
- if (charCode >= min && charCode <= max) {
+ if (codePoint >= min && codePoint <= max) {
return true;
}
}
@@ -127,16 +129,16 @@ function _jpIsCharCodeInRanges(charCode, ranges) {
// Character code testing functions
-function jpIsCharCodeKanji(charCode) {
- return _jpIsCharCodeInRanges(charCode, JP_CJK_RANGES);
+function jpIsCodePointKanji(codePoint) {
+ return _jpIsCodePointInRanges(codePoint, JP_CJK_RANGES);
}
-function jpIsCharCodeKana(charCode) {
- return _jpIsCharCodeInRanges(charCode, JP_KANA_RANGES);
+function jpIsCodePointKana(codePoint) {
+ return _jpIsCodePointInRanges(codePoint, JP_KANA_RANGES);
}
-function jpIsCharCodeJapanese(charCode) {
- return _jpIsCharCodeInRanges(charCode, JP_JAPANESE_RANGES);
+function jpIsCodePointJapanese(codePoint) {
+ return _jpIsCodePointInRanges(codePoint, JP_JAPANESE_RANGES);
}
@@ -144,8 +146,8 @@ function jpIsCharCodeJapanese(charCode) {
function jpIsStringEntirelyKana(str) {
if (str.length === 0) { return false; }
- for (let i = 0, ii = str.length; i < ii; ++i) {
- if (!jpIsCharCodeKana(str.charCodeAt(i))) {
+ for (const c of str) {
+ if (!jpIsCodePointKana(c.codePointAt(0))) {
return false;
}
}
@@ -154,8 +156,8 @@ function jpIsStringEntirelyKana(str) {
function jpIsStringPartiallyJapanese(str) {
if (str.length === 0) { return false; }
- for (let i = 0, ii = str.length; i < ii; ++i) {
- if (jpIsCharCodeJapanese(str.charCodeAt(i))) {
+ for (const c of str) {
+ if (jpIsCodePointJapanese(c.codePointAt(0))) {
return true;
}
}
@@ -264,8 +266,8 @@ function jpDistributeFurigana(expression, reading) {
const groups = [];
let modePrev = null;
for (const c of expression) {
- const charCode = c.charCodeAt(0);
- const modeCurr = jpIsCharCodeKanji(charCode) || charCode === JP_ITERATION_MARK_CHAR_CODE ? 'kanji' : 'kana';
+ const codePoint = c.codePointAt(0);
+ const modeCurr = jpIsCodePointKanji(codePoint) || codePoint === JP_ITERATION_MARK_CHAR_CODE ? 'kanji' : 'kana';
if (modeCurr === modePrev) {
groups[groups.length - 1].text += c;
} else {
@@ -311,10 +313,11 @@ function jpDistributeFuriganaInflected(expression, reading, source) {
function jpConvertHalfWidthKanaToFullWidth(text, sourceMapping) {
let result = '';
- const ii = text.length;
const hasSourceMapping = Array.isArray(sourceMapping);
- for (let i = 0; i < ii; ++i) {
+ // This function is safe to use charCodeAt instead of codePointAt, since all
+ // the relevant characters are represented with a single UTF-16 character code.
+ for (let i = 0, ii = text.length; i < ii; ++i) {
const c = text[i];
const mapping = JP_HALFWIDTH_KATAKANA_MAPPING.get(c);
if (typeof mapping !== 'string') {
@@ -355,13 +358,13 @@ function jpConvertHalfWidthKanaToFullWidth(text, sourceMapping) {
function jpConvertNumericTofullWidth(text) {
let result = '';
- for (let i = 0, ii = text.length; i < ii; ++i) {
- let c = text.charCodeAt(i);
+ for (const char of text) {
+ let c = char.codePointAt(0);
if (c >= 0x30 && c <= 0x39) { // ['0', '9']
c += 0xff10 - 0x30; // 0xff10 = '0' full width
- result += String.fromCharCode(c);
+ result += String.fromCodePoint(c);
} else {
- result += text[i];
+ result += char;
}
}
return result;
@@ -377,9 +380,9 @@ function jpConvertAlphabeticToKana(text, sourceMapping) {
sourceMapping.fill(1);
}
- for (let i = 0; i < ii; ++i) {
+ for (const char of text) {
// Note: 0x61 is the character code for 'a'
- let c = text.charCodeAt(i);
+ let c = char.codePointAt(0);
if (c >= 0x41 && c <= 0x5a) { // ['A', 'Z']
c += (0x61 - 0x41);
} else if (c >= 0x61 && c <= 0x7a) { // ['a', 'z']
@@ -395,10 +398,10 @@ function jpConvertAlphabeticToKana(text, sourceMapping) {
result += jpToHiragana(part, sourceMapping, result.length);
part = '';
}
- result += text[i];
+ result += char;
continue;
}
- part += String.fromCharCode(c);
+ part += String.fromCodePoint(c);
}
if (part.length > 0) {
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index f9db99a2..bd0bbe0e 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -16,7 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global utilStringHashCode*/
+/* global
+ * utilStringHashCode
+ */
/*
* Generic options functions
@@ -58,22 +60,17 @@ const profileOptionsVersionUpdates = [
options.scanning.modifier = options.scanning.requireShift ? 'shift' : 'none';
},
(options) => {
- const fieldTemplatesDefault = profileOptionsGetDefaultFieldTemplates();
options.general.resultOutputMode = options.general.groupResults ? 'group' : 'split';
- options.anki.fieldTemplates = (
- (utilStringHashCode(options.anki.fieldTemplates) !== -805327496) ?
- `{{#if merge}}${fieldTemplatesDefault}{{else}}${options.anki.fieldTemplates}{{/if}}` :
- fieldTemplatesDefault
- );
+ options.anki.fieldTemplates = null;
},
(options) => {
if (utilStringHashCode(options.anki.fieldTemplates) === 1285806040) {
- options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates();
+ options.anki.fieldTemplates = null;
}
},
(options) => {
if (utilStringHashCode(options.anki.fieldTemplates) === -250091611) {
- options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates();
+ options.anki.fieldTemplates = null;
}
},
(options) => {
@@ -97,172 +94,6 @@ const profileOptionsVersionUpdates = [
}
];
-function profileOptionsGetDefaultFieldTemplates() {
- return `
-{{#*inline "glossary-single"}}
- {{~#unless brief~}}
- {{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}
- {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}}
- {{~/unless~}}
- {{~#if glossary.[1]~}}
- {{~#if compactGlossaries~}}
- {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}}
- {{~else~}}
- <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul>
- {{~/if~}}
- {{~else~}}
- {{~#multiLine}}{{glossary.[0]}}{{/multiLine~}}
- {{~/if~}}
-{{/inline}}
-
-{{#*inline "audio"}}{{/inline}}
-
-{{#*inline "character"}}
- {{~definition.character~}}
-{{/inline}}
-
-{{#*inline "dictionary"}}
- {{~definition.dictionary~}}
-{{/inline}}
-
-{{#*inline "expression"}}
- {{~#if merge~}}
- {{~#if modeTermKana~}}
- {{~#each definition.reading~}}
- {{{.}}}
- {{~#unless @last}}、{{/unless~}}
- {{~else~}}
- {{~#each definition.expression~}}
- {{{.}}}
- {{~#unless @last}}、{{/unless~}}
- {{~/each~}}
- {{~/each~}}
- {{~else~}}
- {{~#each definition.expression~}}
- {{{.}}}
- {{~#unless @last}}、{{/unless~}}
- {{~/each~}}
- {{~/if~}}
- {{~else~}}
- {{~#if modeTermKana~}}
- {{~#if definition.reading~}}
- {{definition.reading}}
- {{~else~}}
- {{definition.expression}}
- {{~/if~}}
- {{~else~}}
- {{definition.expression}}
- {{~/if~}}
- {{~/if~}}
-{{/inline}}
-
-{{#*inline "furigana"}}
- {{~#if merge~}}
- {{~#each definition.expressions~}}
- <span class="expression-{{termFrequency}}">{{~#furigana}}{{{.}}}{{/furigana~}}</span>
- {{~#unless @last}}、{{/unless~}}
- {{~/each~}}
- {{~else~}}
- {{#furigana}}{{{definition}}}{{/furigana}}
- {{~/if~}}
-{{/inline}}
-
-{{#*inline "furigana-plain"}}
- {{~#if merge~}}
- {{~#each definition.expressions~}}
- <span class="expression-{{termFrequency}}">{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}</span>
- {{~#unless @last}}、{{/unless~}}
- {{~/each~}}
- {{~else~}}
- {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}}
- {{~/if~}}
-{{/inline}}
-
-{{#*inline "glossary"}}
- <div style="text-align: left;">
- {{~#if modeKanji~}}
- {{~#if definition.glossary.[1]~}}
- <ol>{{#each definition.glossary}}<li>{{.}}</li>{{/each}}</ol>
- {{~else~}}
- {{definition.glossary.[0]}}
- {{~/if~}}
- {{~else~}}
- {{~#if group~}}
- {{~#if definition.definitions.[1]~}}
- <ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries}}</li>{{/each}}</ol>
- {{~else~}}
- {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries~}}
- {{~/if~}}
- {{~else if merge~}}
- {{~#if definition.definitions.[1]~}}
- <ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries}}</li>{{/each}}</ol>
- {{~else~}}
- {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries~}}
- {{~/if~}}
- {{~else~}}
- {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries~}}
- {{~/if~}}
- {{~/if~}}
- </div>
-{{/inline}}
-
-{{#*inline "glossary-brief"}}
- {{~> glossary brief=true ~}}
-{{/inline}}
-
-{{#*inline "kunyomi"}}
- {{~#each definition.kunyomi}}{{.}}{{#unless @last}}, {{/unless}}{{/each~}}
-{{/inline}}
-
-{{#*inline "onyomi"}}
- {{~#each definition.onyomi}}{{.}}{{#unless @last}}, {{/unless}}{{/each~}}
-{{/inline}}
-
-{{#*inline "reading"}}
- {{~#unless modeTermKana~}}
- {{~#if merge~}}
- {{~#each definition.reading~}}
- {{{.}}}
- {{~#unless @last}}、{{/unless~}}
- {{~/each~}}
- {{~else~}}
- {{~definition.reading~}}
- {{~/if~}}
- {{~/unless~}}
-{{/inline}}
-
-{{#*inline "sentence"}}
- {{~#if definition.cloze}}{{definition.cloze.sentence}}{{/if~}}
-{{/inline}}
-
-{{#*inline "cloze-prefix"}}
- {{~#if definition.cloze}}{{definition.cloze.prefix}}{{/if~}}
-{{/inline}}
-
-{{#*inline "cloze-body"}}
- {{~#if definition.cloze}}{{definition.cloze.body}}{{/if~}}
-{{/inline}}
-
-{{#*inline "cloze-suffix"}}
- {{~#if definition.cloze}}{{definition.cloze.suffix}}{{/if~}}
-{{/inline}}
-
-{{#*inline "tags"}}
- {{~#each definition.definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each~}}
-{{/inline}}
-
-{{#*inline "url"}}
- <a href="{{definition.url}}">{{definition.url}}</a>
-{{/inline}}
-
-{{#*inline "screenshot"}}
- <img src="{{definition.screenshotFileName}}" />
-{{/inline}}
-
-{{~> (lookup . "marker") ~}}
-`.trim();
-}
-
function profileOptionsCreateDefaults() {
return {
general: {
diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js
index 509c4009..a470e873 100644
--- a/ext/bg/js/search-frontend.js
+++ b/ext/bg/js/search-frontend.js
@@ -16,9 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiOptionsGet*/
+/* global
+ * apiOptionsGet
+ */
async function searchFrontendSetup() {
+ await yomichan.prepare();
+
const optionsContext = {
depth: 0,
url: window.location.href
diff --git a/ext/bg/js/search-query-parser-generator.js b/ext/bg/js/search-query-parser-generator.js
index 1ab23a82..664858a4 100644
--- a/ext/bg/js/search-query-parser-generator.js
+++ b/ext/bg/js/search-query-parser-generator.js
@@ -16,7 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiGetQueryParserTemplatesHtml, TemplateHandler*/
+/* global
+ * TemplateHandler
+ * apiGetQueryParserTemplatesHtml
+ */
class QueryParserGenerator {
constructor() {
diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js
index 0d4aaa50..06316ce2 100644
--- a/ext/bg/js/search-query-parser.js
+++ b/ext/bg/js/search-query-parser.js
@@ -16,7 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiTermsFind, apiOptionsSet, apiTextParse, apiTextParseMecab, TextScanner, QueryParserGenerator*/
+/* global
+ * QueryParserGenerator
+ * TextScanner
+ * apiOptionsSet
+ * apiTermsFind
+ * apiTextParse
+ * apiTextParseMecab
+ * docSentenceExtract
+ */
class QueryParser extends TextScanner {
constructor(search) {
@@ -55,12 +63,14 @@ class QueryParser extends TextScanner {
const {definitions, length} = await apiTermsFind(searchText, {}, this.search.getOptionsContext());
if (definitions.length === 0) { return null; }
+ const sentence = docSentenceExtract(textSource, this.search.options.anki.sentenceExt);
+
textSource.setEndOffset(length);
this.search.setContent('terms', {definitions, context: {
focus: false,
disableHistory: cause === 'mouse',
- sentence: {text: searchText, offset: 0},
+ sentence,
url: window.location.href
}});
@@ -142,11 +152,11 @@ class QueryParser extends TextScanner {
}
if (this.search.options.parsing.enableMecabParser) {
const mecabResults = await apiTextParseMecab(text, this.search.getOptionsContext());
- for (const mecabDictName in mecabResults) {
+ for (const [mecabDictName, mecabDictResults] of mecabResults) {
results.push({
name: `MeCab: ${mecabDictName}`,
id: `mecab-${mecabDictName}`,
- parsedText: mecabResults[mecabDictName]
+ parsedText: mecabDictResults
});
}
}
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index 98e167ad..e2bdff73 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -16,7 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiOptionsSet, apiTermsFind, Display, QueryParser, ClipboardMonitor*/
+/* global
+ * ClipboardMonitor
+ * Display
+ * QueryParser
+ * apiClipboardGet
+ * apiOptionsSet
+ * apiTermsFind
+ */
class DisplaySearch extends Display {
constructor() {
@@ -38,7 +45,26 @@ class DisplaySearch extends Display {
this.introVisible = true;
this.introAnimationTimer = null;
- this.clipboardMonitor = new ClipboardMonitor();
+ this.clipboardMonitor = new ClipboardMonitor({getClipboard: apiClipboardGet});
+
+ this._onKeyDownIgnoreKeys = new Map([
+ ['ANY_MOD', new Set([
+ 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End',
+ 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10',
+ 'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20',
+ 'F21', 'F22', 'F23', 'F24'
+ ])],
+ ['Control', new Set(['C', 'A', 'Z', 'Y', 'X', 'F', 'G'])],
+ ['Meta', new Set(['C', 'A', 'Z', 'Y', 'X', 'F', 'G'])],
+ ['OS', new Set()],
+ ['Alt', new Set()],
+ ['AltGraph', new Set()],
+ ['Shift', new Set()]
+ ]);
+
+ this._runtimeMessageHandlers = new Map([
+ ['searchQueryUpdate', this.onExternalSearchUpdate.bind(this)]
+ ]);
}
static create() {
@@ -49,76 +75,41 @@ class DisplaySearch extends Display {
async prepare() {
try {
- const superPromise = super.prepare();
- const queryParserPromise = this.queryParser.prepare();
- await Promise.all([superPromise, queryParserPromise]);
+ await super.prepare();
+ await this.queryParser.prepare();
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) {
- if (this.options.general.enableWanakana === true) {
- this.wanakanaEnable.checked = true;
- window.wanakana.bind(this.query);
- } else {
- this.wanakanaEnable.checked = false;
- }
- this.wanakanaEnable.addEventListener('change', (e) => {
- const {queryParams: {query: query2=''}} = parseUrl(window.location.href);
- if (e.target.checked) {
- window.wanakana.bind(this.query);
- apiOptionsSet({general: {enableWanakana: true}}, this.getOptionsContext());
- } else {
- window.wanakana.unbind(this.query);
- apiOptionsSet({general: {enableWanakana: false}}, this.getOptionsContext());
- }
- this.setQuery(query2);
- this.onSearchQueryUpdated(this.query.value, false);
- });
- }
+ document.documentElement.dataset.searchMode = mode;
- this.setQuery(query);
- this.onSearchQueryUpdated(this.query.value, false);
+ if (this.options.general.enableWanakana === true) {
+ this.wanakanaEnable.checked = true;
+ window.wanakana.bind(this.query);
+ } else {
+ this.wanakanaEnable.checked = false;
}
- if (this.clipboardMonitorEnable !== null && mode !== 'popup') {
+
+ this.setQuery(query);
+ this.onSearchQueryUpdated(this.query.value, false);
+
+ if (mode !== 'popup') {
if (this.options.general.enableClipboardMonitor === true) {
this.clipboardMonitorEnable.checked = true;
this.clipboardMonitor.start();
} else {
this.clipboardMonitorEnable.checked = false;
}
- this.clipboardMonitorEnable.addEventListener('change', (e) => {
- if (e.target.checked) {
- chrome.permissions.request(
- {permissions: ['clipboardRead']},
- (granted) => {
- if (granted) {
- this.clipboardMonitor.start();
- apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext());
- } else {
- e.target.checked = false;
- }
- }
- );
- } else {
- this.clipboardMonitor.stop();
- apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext());
- }
- });
+ this.clipboardMonitorEnable.addEventListener('change', this.onClipboardMonitorEnableChange.bind(this));
}
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.search.addEventListener('click', this.onSearch.bind(this), false);
+ this.query.addEventListener('input', this.onSearchInput.bind(this), false);
+ this.wanakanaEnable.addEventListener('change', this.onWanakanaEnableChange.bind(this));
+ window.addEventListener('popstate', this.onPopState.bind(this));
+ window.addEventListener('copy', this.onCopy.bind(this));
+ this.clipboardMonitor.on('change', this.onExternalSearchUpdate.bind(this));
this.updateSearchButton();
} catch (e) {
@@ -174,28 +165,30 @@ class DisplaySearch extends Display {
}
onRuntimeMessage({action, params}, sender, callback) {
- const handler = DisplaySearch._runtimeMessageHandlers.get(action);
+ const handler = this._runtimeMessageHandlers.get(action);
if (typeof handler !== 'function') { return false; }
- const result = handler(this, params, sender);
+ const result = handler(params, sender);
callback(result);
return false;
}
onKeyDown(e) {
const key = Display.getKeyFromEvent(e);
- const ignoreKeys = DisplaySearch.onKeyDownIgnoreKeys;
+ const ignoreKeys = this._onKeyDownIgnoreKeys;
- const activeModifierMap = {
- 'Control': e.ctrlKey,
- 'Meta': e.metaKey,
- 'ANY_MOD': true
- };
+ const activeModifierMap = new Map([
+ ['Control', e.ctrlKey],
+ ['Meta', e.metaKey],
+ ['Shift', e.shiftKey],
+ ['Alt', e.altKey],
+ ['ANY_MOD', true]
+ ]);
let preventFocus = false;
- for (const [modifier, keys] of Object.entries(ignoreKeys)) {
- const modifierActive = activeModifierMap[modifier];
- if (key === modifier || (modifierActive && keys.includes(key))) {
+ for (const [modifier, keys] of ignoreKeys.entries()) {
+ const modifierActive = activeModifierMap.get(modifier);
+ if (key === modifier || (modifierActive && keys.has(key))) {
preventFocus = true;
break;
}
@@ -211,7 +204,7 @@ class DisplaySearch extends Display {
this.clipboardMonitor.setPreviousText(document.getSelection().toString().trim());
}
- onExternalSearchUpdate(text) {
+ onExternalSearchUpdate({text}) {
this.setQuery(text);
const url = new URL(window.location.href);
url.searchParams.set('query', text);
@@ -253,6 +246,38 @@ class DisplaySearch extends Display {
}
}
+ onWanakanaEnableChange(e) {
+ const {queryParams: {query=''}} = parseUrl(window.location.href);
+ const enableWanakana = e.target.checked;
+ if (enableWanakana) {
+ window.wanakana.bind(this.query);
+ } else {
+ window.wanakana.unbind(this.query);
+ }
+ this.setQuery(query);
+ this.onSearchQueryUpdated(this.query.value, false);
+ apiOptionsSet({general: {enableWanakana}}, this.getOptionsContext());
+ }
+
+ onClipboardMonitorEnableChange(e) {
+ if (e.target.checked) {
+ chrome.permissions.request(
+ {permissions: ['clipboardRead']},
+ (granted) => {
+ if (granted) {
+ this.clipboardMonitor.start();
+ apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext());
+ } else {
+ e.target.checked = false;
+ }
+ }
+ );
+ } else {
+ this.clipboardMonitor.stop();
+ apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext());
+ }
+ }
+
async updateOptions(options) {
await super.updateOptions(options);
this.queryParser.setOptions(this.options);
@@ -346,23 +371,4 @@ class DisplaySearch extends Display {
}
}
-DisplaySearch.onKeyDownIgnoreKeys = {
- 'ANY_MOD': [
- 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End',
- 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10',
- 'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20',
- 'F21', 'F22', 'F23', 'F24'
- ],
- 'Control': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'],
- 'Meta': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'],
- 'OS': [],
- 'Alt': [],
- 'AltGraph': [],
- '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 2e80e334..c5222d30 100644
--- a/ext/bg/js/settings/anki-templates.js
+++ b/ext/bg/js/settings/anki-templates.js
@@ -16,22 +16,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global getOptionsContext, getOptionsMutable, settingsSaveOptions
-profileOptionsGetDefaultFieldTemplates, ankiGetFieldMarkers, ankiGetFieldMarkersHtml, dictFieldFormat
-apiOptionsGet, apiTermsFind*/
+/* global
+ * AnkiNoteBuilder
+ * ankiGetFieldMarkers
+ * ankiGetFieldMarkersHtml
+ * apiGetDefaultAnkiFieldTemplates
+ * apiOptionsGet
+ * apiTemplateRender
+ * apiTermsFind
+ * getOptionsContext
+ * getOptionsMutable
+ * settingsSaveOptions
+ */
function onAnkiFieldTemplatesReset(e) {
e.preventDefault();
$('#field-template-reset-modal').modal('show');
}
-function onAnkiFieldTemplatesResetConfirm(e) {
+async function onAnkiFieldTemplatesResetConfirm(e) {
e.preventDefault();
$('#field-template-reset-modal').modal('hide');
+ const value = await apiGetDefaultAnkiFieldTemplates();
+
const element = document.querySelector('#field-templates');
- element.value = profileOptionsGetDefaultFieldTemplates();
+ element.value = value;
element.dispatchEvent(new Event('change'));
}
@@ -45,10 +56,10 @@ function ankiTemplatesInitialize() {
node.addEventListener('click', onAnkiTemplateMarkerClicked, false);
}
- $('#field-templates').on('change', (e) => onAnkiFieldTemplatesChanged(e));
- $('#field-template-render').on('click', (e) => onAnkiTemplateRender(e));
- $('#field-templates-reset').on('click', (e) => onAnkiFieldTemplatesReset(e));
- $('#field-templates-reset-confirm').on('click', (e) => onAnkiFieldTemplatesResetConfirm(e));
+ $('#field-templates').on('change', onAnkiFieldTemplatesChanged);
+ $('#field-template-render').on('click', onAnkiTemplateRender);
+ $('#field-templates-reset').on('click', onAnkiFieldTemplatesReset);
+ $('#field-templates-reset-confirm').on('click', onAnkiFieldTemplatesResetConfirm);
ankiTemplatesUpdateValue();
}
@@ -57,7 +68,7 @@ async function ankiTemplatesUpdateValue() {
const optionsContext = getOptionsContext();
const options = await apiOptionsGet(optionsContext);
let templates = options.anki.fieldTemplates;
- if (typeof templates !== 'string') { templates = profileOptionsGetDefaultFieldTemplates(); }
+ if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); }
$('#field-templates').val(templates);
onAnkiTemplatesValidateCompile();
@@ -89,8 +100,9 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i
if (definition !== null) {
const options = await apiOptionsGet(optionsContext);
let templates = options.anki.fieldTemplates;
- if (typeof templates !== 'string') { templates = profileOptionsGetDefaultFieldTemplates(); }
- result = await dictFieldFormat(field, definition, mode, options, templates, exceptions);
+ if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); }
+ const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: apiTemplateRender});
+ result = await ankiNoteBuilder.formatField(field, definition, mode, options, templates, exceptions);
}
} catch (e) {
exceptions.push(e);
@@ -109,7 +121,7 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i
async function onAnkiFieldTemplatesChanged(e) {
// Get value
let templates = e.currentTarget.value;
- if (templates === profileOptionsGetDefaultFieldTemplates()) {
+ if (templates === await apiGetDefaultAnkiFieldTemplates()) {
// Default
templates = null;
}
diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js
index 4263fc51..b706cd1b 100644
--- a/ext/bg/js/settings/anki.js
+++ b/ext/bg/js/settings/anki.js
@@ -16,9 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global getOptionsContext, getOptionsMutable, settingsSaveOptions
-utilBackgroundIsolate, utilAnkiGetDeckNames, utilAnkiGetModelNames, utilAnkiGetModelFieldNames
-onFormOptionsChanged*/
+/* global
+ * getOptionsContext
+ * getOptionsMutable
+ * onFormOptionsChanged
+ * settingsSaveOptions
+ * utilAnkiGetDeckNames
+ * utilAnkiGetModelFieldNames
+ * utilAnkiGetModelNames
+ * utilBackgroundIsolate
+ */
// Private
@@ -154,10 +161,10 @@ async function _ankiFieldsPopulate(tabId, options) {
container.appendChild(fragment);
for (const node of container.querySelectorAll('.anki-field-value')) {
- node.addEventListener('change', (e) => onFormOptionsChanged(e), false);
+ node.addEventListener('change', onFormOptionsChanged, false);
}
for (const node of container.querySelectorAll('.marker-link')) {
- node.addEventListener('click', (e) => _onAnkiMarkerClicked(e), false);
+ node.addEventListener('click', _onAnkiMarkerClicked, false);
}
}
@@ -267,7 +274,7 @@ function ankiGetFieldMarkers(type) {
function ankiInitialize() {
for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
- node.addEventListener('change', (e) => _onAnkiModelChanged(e), false);
+ node.addEventListener('change', _onAnkiModelChanged, false);
}
}
diff --git a/ext/bg/js/settings/audio-ui.js b/ext/bg/js/settings/audio-ui.js
index 555380b4..206539a4 100644
--- a/ext/bg/js/settings/audio-ui.js
+++ b/ext/bg/js/settings/audio-ui.js
@@ -37,7 +37,7 @@ AudioSourceUI.Container = class Container {
this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));
}
- this._clickListener = () => this.onAddAudioSource();
+ this._clickListener = this.onAddAudioSource.bind(this);
this.addButton.addEventListener('click', this._clickListener, false);
}
@@ -105,8 +105,8 @@ AudioSourceUI.AudioSource = class AudioSource {
this.select.value = audioSource;
- this._selectChangeListener = () => this.onSelectChanged();
- this._removeClickListener = () => this.onRemoveClicked();
+ this._selectChangeListener = this.onSelectChanged.bind(this);
+ this._removeClickListener = this.onRemoveClicked.bind(this);
this.select.addEventListener('change', this._selectChangeListener, false);
this.removeButton.addEventListener('click', this._removeClickListener, false);
diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js
index 588d9a11..38dd6349 100644
--- a/ext/bg/js/settings/audio.js
+++ b/ext/bg/js/settings/audio.js
@@ -16,12 +16,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global getOptionsContext, getOptionsMutable, settingsSaveOptions
-AudioSourceUI, audioGetTextToSpeechVoice*/
+/* global
+ * AudioSourceUI
+ * AudioSystem
+ * apiAudioGetUri
+ * getOptionsContext
+ * getOptionsMutable
+ * settingsSaveOptions
+ */
let audioSourceUI = null;
+let audioSystem = null;
async function audioSettingsInitialize() {
+ audioSystem = new AudioSystem({
+ getAudioUri: async (definition, source) => {
+ const optionsContext = getOptionsContext();
+ return await apiAudioGetUri(definition, source, optionsContext);
+ }
+ });
+
const optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
audioSourceUI = new AudioSourceUI.Container(
@@ -29,7 +43,7 @@ async function audioSettingsInitialize() {
document.querySelector('.audio-source-list'),
document.querySelector('.audio-source-add')
);
- audioSourceUI.save = () => settingsSaveOptions();
+ audioSourceUI.save = settingsSaveOptions;
textToSpeechInitialize();
}
@@ -37,11 +51,11 @@ async function audioSettingsInitialize() {
function textToSpeechInitialize() {
if (typeof speechSynthesis === 'undefined') { return; }
- speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false);
+ speechSynthesis.addEventListener('voiceschanged', updateTextToSpeechVoices, false);
updateTextToSpeechVoices();
- document.querySelector('#text-to-speech-voice').addEventListener('change', (e) => onTextToSpeechVoiceChange(e), false);
- document.querySelector('#text-to-speech-voice-test').addEventListener('click', () => textToSpeechTest(), false);
+ document.querySelector('#text-to-speech-voice').addEventListener('change', onTextToSpeechVoiceChange, false);
+ document.querySelector('#text-to-speech-voice-test').addEventListener('click', textToSpeechTest, false);
}
function updateTextToSpeechVoices() {
@@ -100,16 +114,11 @@ function textToSpeechVoiceCompare(a, b) {
function textToSpeechTest() {
try {
const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || '';
- const voiceURI = document.querySelector('#text-to-speech-voice').value;
- const voice = audioGetTextToSpeechVoice(voiceURI);
- if (voice === null) { return; }
-
- const utterance = new SpeechSynthesisUtterance(text);
- utterance.lang = 'ja-JP';
- utterance.voice = voice;
- utterance.volume = 1.0;
+ const voiceUri = document.querySelector('#text-to-speech-voice').value;
- speechSynthesis.speak(utterance);
+ const audio = audioSystem.createTextToSpeechAudio({text, voiceUri});
+ audio.volume = 1.0;
+ audio.play();
} catch (e) {
// NOP
}
diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js
index f4d622a4..21417dfb 100644
--- a/ext/bg/js/settings/backup.js
+++ b/ext/bg/js/settings/backup.js
@@ -16,10 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiOptionsGetFull, apiGetEnvironmentInfo
-utilBackend, utilIsolate, utilBackgroundIsolate, utilReadFileArrayBuffer
-optionsGetDefault, optionsUpdateVersion
-profileOptionsGetDefaultFieldTemplates*/
+/* global
+ * apiGetDefaultAnkiFieldTemplates
+ * apiGetEnvironmentInfo
+ * apiOptionsGetFull
+ * optionsGetDefault
+ * optionsUpdateVersion
+ * utilBackend
+ * utilBackgroundIsolate
+ * utilIsolate
+ * utilReadFileArrayBuffer
+ */
// Exporting
@@ -47,8 +54,7 @@ function _getSettingsExportDateString(date, dateSeparator, dateTimeSeparator, ti
async function _getSettingsExportData(date) {
const optionsFull = await apiOptionsGetFull();
const environment = await apiGetEnvironmentInfo();
-
- const fieldTemplatesDefault = profileOptionsGetDefaultFieldTemplates();
+ const fieldTemplatesDefault = await apiGetDefaultAnkiFieldTemplates();
// Format options
for (const {options} of optionsFull.profiles) {
@@ -122,7 +128,7 @@ async function _onSettingsExportClick() {
// Importing
async function _settingsImportSetOptionsFull(optionsFull) {
- return utilIsolate(await utilBackend().setFullOptions(
+ return utilIsolate(utilBackend().setFullOptions(
utilBackgroundIsolate(optionsFull)
));
}
@@ -364,10 +370,10 @@ async function _onSettingsResetConfirmClick() {
// Setup
-window.addEventListener('DOMContentLoaded', () => {
+function backupInitialize() {
document.querySelector('#settings-export').addEventListener('click', _onSettingsExportClick, false);
document.querySelector('#settings-import').addEventListener('click', _onSettingsImportClick, false);
document.querySelector('#settings-import-file').addEventListener('change', _onSettingsImportFileChange, false);
document.querySelector('#settings-reset').addEventListener('click', _onSettingsResetClick, false);
document.querySelector('#settings-reset-modal-confirm').addEventListener('click', _onSettingsResetConfirmClick, false);
-}, false);
+}
diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js
index 5a271321..9d61d25e 100644
--- a/ext/bg/js/settings/conditions-ui.js
+++ b/ext/bg/js/settings/conditions-ui.js
@@ -16,7 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global conditionsNormalizeOptionValue*/
+/* global
+ * conditionsNormalizeOptionValue
+ */
class ConditionsUI {
static instantiateTemplate(templateSelector) {
@@ -41,7 +43,7 @@ ConditionsUI.Container = class Container {
this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup));
}
- this.addButton.on('click', () => this.onAddConditionGroup());
+ this.addButton.on('click', this.onAddConditionGroup.bind(this));
}
cleanup() {
@@ -127,7 +129,7 @@ ConditionsUI.ConditionGroup = class ConditionGroup {
this.children.push(new ConditionsUI.Condition(this, condition));
}
- this.addButton.on('click', () => this.onAddCondition());
+ this.addButton.on('click', this.onAddCondition.bind(this));
}
cleanup() {
@@ -185,10 +187,10 @@ ConditionsUI.Condition = class Condition {
this.updateOperators();
this.updateInput();
- this.input.on('change', () => this.onInputChanged());
- this.typeSelect.on('change', () => this.onConditionTypeChanged());
- this.operatorSelect.on('change', () => this.onConditionOperatorChanged());
- this.removeButton.on('click', () => this.onRemoveClicked());
+ this.input.on('change', this.onInputChanged.bind(this));
+ this.typeSelect.on('change', this.onConditionTypeChanged.bind(this));
+ this.operatorSelect.on('change', this.onConditionOperatorChanged.bind(this));
+ this.removeButton.on('click', this.onRemoveClicked.bind(this));
}
cleanup() {
@@ -235,10 +237,10 @@ ConditionsUI.Condition = class Condition {
updateInput() {
const conditionDescriptors = this.parent.parent.conditionDescriptors;
const {type, operator} = this.condition;
- const props = {
- placeholder: '',
- type: 'text'
- };
+ const props = new Map([
+ ['placeholder', ''],
+ ['type', 'text']
+ ]);
const objects = [];
if (hasOwn(conditionDescriptors, type)) {
@@ -252,20 +254,20 @@ ConditionsUI.Condition = class Condition {
for (const object of objects) {
if (hasOwn(object, 'placeholder')) {
- props.placeholder = object.placeholder;
+ props.set('placeholder', object.placeholder);
}
if (object.type === 'number') {
- props.type = 'number';
+ props.set('type', 'number');
for (const prop of ['step', 'min', 'max']) {
if (hasOwn(object, prop)) {
- props[prop] = object[prop];
+ props.set(prop, object[prop]);
}
}
}
}
- for (const prop in props) {
- this.input.prop(prop, props[prop]);
+ for (const [prop, value] of props.entries()) {
+ this.input.prop(prop, value);
}
const {valid} = this.validateValue(this.condition.value);
diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js
index 70a22a16..5e59cc3d 100644
--- a/ext/bg/js/settings/dictionaries.js
+++ b/ext/bg/js/settings/dictionaries.js
@@ -16,11 +16,23 @@
* 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*/
+/* global
+ * PageExitPrevention
+ * apiOptionsGet
+ * apiOptionsGetFull
+ * getOptionsContext
+ * getOptionsFullMutable
+ * getOptionsMutable
+ * settingsSaveOptions
+ * storageEstimate
+ * storageUpdateStats
+ * utilBackgroundIsolate
+ * utilDatabaseDeleteDictionary
+ * utilDatabaseGetDictionaryCounts
+ * utilDatabaseGetDictionaryInfo
+ * utilDatabaseImport
+ * utilDatabasePurge
+ */
let dictionaryUI = null;
@@ -36,7 +48,7 @@ class SettingsDictionaryListUI {
this.dictionaryEntries = [];
this.extra = null;
- document.querySelector('#dict-delete-confirm').addEventListener('click', (e) => this.onDictionaryConfirmDelete(e), false);
+ document.querySelector('#dict-delete-confirm').addEventListener('click', this.onDictionaryConfirmDelete.bind(this), false);
}
setOptionsDictionaries(optionsDictionaries) {
@@ -198,10 +210,10 @@ class SettingsDictionaryEntryUI {
this.applyValues();
- 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);
+ this.eventListeners.addEventListener(this.enabledCheckbox, 'change', this.onEnabledChanged.bind(this), false);
+ this.eventListeners.addEventListener(this.allowSecondarySearchesCheckbox, 'change', this.onAllowSecondarySearchesChanged.bind(this), false);
+ this.eventListeners.addEventListener(this.priorityInput, 'change', this.onPriorityChanged.bind(this), false);
+ this.eventListeners.addEventListener(this.deleteButton, 'click', this.onDeleteButtonClicked.bind(this), false);
}
cleanup() {
@@ -341,14 +353,14 @@ async function dictSettingsInitialize() {
document.querySelector('#dict-groups-extra'),
document.querySelector('#dict-extra-template')
);
- dictionaryUI.save = () => settingsSaveOptions();
-
- document.querySelector('#dict-purge-button').addEventListener('click', (e) => onDictionaryPurgeButtonClick(e), false);
- document.querySelector('#dict-purge-confirm').addEventListener('click', (e) => onDictionaryPurge(e), false);
- document.querySelector('#dict-file-button').addEventListener('click', (e) => onDictionaryImportButtonClick(e), false);
- document.querySelector('#dict-file').addEventListener('change', (e) => onDictionaryImport(e), false);
- document.querySelector('#dict-main').addEventListener('change', (e) => onDictionaryMainChanged(e), false);
- document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', (e) => onDatabaseEnablePrefixWildcardSearchesChanged(e), false);
+ dictionaryUI.save = settingsSaveOptions;
+
+ document.querySelector('#dict-purge-button').addEventListener('click', onDictionaryPurgeButtonClick, false);
+ document.querySelector('#dict-purge-confirm').addEventListener('click', onDictionaryPurge, false);
+ document.querySelector('#dict-file-button').addEventListener('click', onDictionaryImportButtonClick, false);
+ document.querySelector('#dict-file').addEventListener('change', onDictionaryImport, false);
+ document.querySelector('#dict-main').addEventListener('change', onDictionaryMainChanged, false);
+ document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', onDatabaseEnablePrefixWildcardSearchesChanged, false);
await onDictionaryOptionsChanged();
await onDatabaseUpdated();
diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
index d1ad2c6b..ebc443df 100644
--- a/ext/bg/js/settings/main.js
+++ b/ext/bg/js/settings/main.js
@@ -16,13 +16,26 @@
* 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
-*/
+/* global
+ * ankiErrorShown
+ * ankiFieldsToDict
+ * ankiInitialize
+ * ankiTemplatesInitialize
+ * ankiTemplatesUpdateValue
+ * apiOptionsSave
+ * appearanceInitialize
+ * audioSettingsInitialize
+ * backupInitialize
+ * dictSettingsInitialize
+ * getOptionsContext
+ * onAnkiOptionsChanged
+ * onDictionaryOptionsChanged
+ * profileOptionsSetup
+ * storageInfoInitialize
+ * utilBackend
+ * utilBackgroundIsolate
+ * utilIsolate
+ */
function getOptionsMutable(optionsContext) {
return utilBackend().getOptions(
@@ -200,7 +213,7 @@ async function formWrite(options) {
}
function formSetupEventListeners() {
- $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change((e) => onFormOptionsChanged(e));
+ $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(onFormOptionsChanged);
}
function formUpdateVisibility(options) {
@@ -262,6 +275,8 @@ function showExtensionInformation() {
async function onReady() {
+ await yomichan.prepare();
+
showExtensionInformation();
formSetupEventListeners();
@@ -271,6 +286,7 @@ async function onReady() {
await dictSettingsInitialize();
ankiInitialize();
ankiTemplatesInitialize();
+ backupInitialize();
storageInfoInitialize();
diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js
index aa2b6100..6a149841 100644
--- a/ext/bg/js/settings/popup-preview-frame.js
+++ b/ext/bg/js/settings/popup-preview-frame.js
@@ -16,7 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiOptionsGet, Popup, PopupProxyHost, Frontend, TextSourceRange*/
+/* global
+ * Frontend
+ * Popup
+ * PopupProxyHost
+ * TextSourceRange
+ * apiOptionsGet
+ */
class SettingsPopupPreview {
constructor() {
@@ -28,6 +34,12 @@ class SettingsPopupPreview {
this.themeChangeTimeout = null;
this.textSource = null;
this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
+
+ this._windowMessageHandlers = new Map([
+ ['setText', ({text}) => this.setText(text)],
+ ['setCustomCss', ({css}) => this.setCustomCss(css)],
+ ['setCustomOuterCss', ({css}) => this.setCustomOuterCss(css)]
+ ]);
}
static create() {
@@ -38,15 +50,12 @@ class SettingsPopupPreview {
async prepare() {
// Setup events
- window.addEventListener('message', (e) => this.onMessage(e), false);
+ window.addEventListener('message', this.onMessage.bind(this), false);
- const themeDarkCheckbox = document.querySelector('#theme-dark-checkbox');
- if (themeDarkCheckbox !== null) {
- themeDarkCheckbox.addEventListener('change', () => this.onThemeDarkCheckboxChanged(themeDarkCheckbox), false);
- }
+ document.querySelector('#theme-dark-checkbox').addEventListener('change', this.onThemeDarkCheckboxChanged.bind(this), false);
// Overwrite API functions
- window.apiOptionsGet = (...args) => this.apiOptionsGet(...args);
+ window.apiOptionsGet = this.apiOptionsGet.bind(this);
// Overwrite frontend
const popupHost = new PopupProxyHost();
@@ -56,7 +65,7 @@ class SettingsPopupPreview {
this.popup.setChildrenSupported(false);
this.popupSetCustomOuterCssOld = this.popup.setCustomOuterCss;
- this.popup.setCustomOuterCss = (...args) => this.popupSetCustomOuterCss(...args);
+ this.popup.setCustomOuterCss = this.popupSetCustomOuterCss.bind(this);
this.frontend = new Frontend(this.popup);
@@ -101,14 +110,14 @@ class SettingsPopupPreview {
if (e.origin !== this._targetOrigin) { return; }
const {action, params} = e.data;
- const handler = SettingsPopupPreview._messageHandlers.get(action);
+ const handler = this._windowMessageHandlers.get(action);
if (typeof handler !== 'function') { return; }
- handler(this, params);
+ handler(params);
}
- onThemeDarkCheckboxChanged(node) {
- document.documentElement.classList.toggle('dark', node.checked);
+ onThemeDarkCheckboxChanged(e) {
+ document.documentElement.classList.toggle('dark', e.target.checked);
if (this.themeChangeTimeout !== null) {
clearTimeout(this.themeChangeTimeout);
}
@@ -171,12 +180,6 @@ class SettingsPopupPreview {
}
}
-SettingsPopupPreview._messageHandlers = new Map([
- ['setText', (self, {text}) => self.setText(text)],
- ['setCustomCss', (self, {css}) => self.setCustomCss(css)],
- ['setCustomOuterCss', (self, {css}) => self.setCustomOuterCss(css)]
-]);
-
SettingsPopupPreview.instance = SettingsPopupPreview.create();
diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js
index 3e589809..b35b6309 100644
--- a/ext/bg/js/settings/profiles.js
+++ b/ext/bg/js/settings/profiles.js
@@ -16,9 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global getOptionsMutable, getOptionsFullMutable, settingsSaveOptions, apiOptionsGetFull
-utilBackgroundIsolate, formWrite
-conditionsClearCaches, ConditionsUI, profileConditionsDescriptor*/
+/* global
+ * ConditionsUI
+ * apiOptionsGetFull
+ * conditionsClearCaches
+ * formWrite
+ * getOptionsFullMutable
+ * getOptionsMutable
+ * profileConditionsDescriptor
+ * settingsSaveOptions
+ * utilBackgroundIsolate
+ */
let currentProfileIndex = 0;
let profileConditionsContainer = null;
@@ -39,16 +47,16 @@ async function profileOptionsSetup() {
}
function profileOptionsSetupEventListeners() {
- $('#profile-target').change((e) => onTargetProfileChanged(e));
- $('#profile-name').change((e) => onProfileNameChanged(e));
- $('#profile-add').click((e) => onProfileAdd(e));
- $('#profile-remove').click((e) => onProfileRemove(e));
- $('#profile-remove-confirm').click((e) => onProfileRemoveConfirm(e));
- $('#profile-copy').click((e) => onProfileCopy(e));
- $('#profile-copy-confirm').click((e) => onProfileCopyConfirm(e));
+ $('#profile-target').change(onTargetProfileChanged);
+ $('#profile-name').change(onProfileNameChanged);
+ $('#profile-add').click(onProfileAdd);
+ $('#profile-remove').click(onProfileRemove);
+ $('#profile-remove-confirm').click(onProfileRemoveConfirm);
+ $('#profile-copy').click(onProfileCopy);
+ $('#profile-copy-confirm').click(onProfileCopyConfirm);
$('#profile-move-up').click(() => onProfileMove(-1));
$('#profile-move-down').click(() => onProfileMove(1));
- $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change((e) => onProfileOptionsChanged(e));
+ $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(onProfileOptionsChanged);
}
function tryGetIntegerValue(selector, min, max) {
diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js
index cbe1bb4d..ae305e22 100644
--- a/ext/bg/js/settings/storage.js
+++ b/ext/bg/js/settings/storage.js
@@ -16,7 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiGetEnvironmentInfo*/
+/* global
+ * apiGetEnvironmentInfo
+ */
function storageBytesToLabeledString(size) {
const base = 1000;
@@ -57,7 +59,7 @@ async function storageInfoInitialize() {
await storageShowInfo();
- document.querySelector('#storage-refresh').addEventListener('click', () => storageShowInfo(), false);
+ document.querySelector('#storage-refresh').addEventListener('click', storageShowInfo, false);
}
async function storageUpdateStats() {
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index a675a9f7..25da9bf0 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -16,12 +16,28 @@
* 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*/
+/* global
+ * Database
+ * Deinflector
+ * dictEnabledSet
+ * dictTagBuildSource
+ * dictTagSanitize
+ * dictTagsSort
+ * dictTermsCompressTags
+ * dictTermsGroup
+ * dictTermsMergeByGloss
+ * dictTermsMergeBySequence
+ * dictTermsSort
+ * dictTermsUndupe
+ * jpConvertAlphabeticToKana
+ * jpConvertHalfWidthKanaToFullWidth
+ * jpConvertNumericTofullWidth
+ * jpDistributeFurigana
+ * jpHiraganaToKatakana
+ * jpIsCodePointJapanese
+ * jpKatakanaToHiragana
+ * requestJson
+ */
class Translator {
constructor() {
@@ -199,8 +215,19 @@ class Translator {
const strayDefinitions = defaultDefinitions.filter((definition, index) => !mergedByTermIndices.has(index));
for (const groupedDefinition of dictTermsGroup(strayDefinitions, dictionaries)) {
- groupedDefinition.expressions = [Translator.createExpression(groupedDefinition.expression, groupedDefinition.reading)];
- definitionsMerged.push(groupedDefinition);
+ // from dictTermsMergeBySequence
+ const {reasons, score, expression, reading, source, dictionary} = groupedDefinition;
+ const compatibilityDefinition = {
+ reasons,
+ score,
+ expression: [expression],
+ reading: [reading],
+ expressions: [Translator.createExpression(groupedDefinition.expression, groupedDefinition.reading)],
+ source,
+ dictionary,
+ definitions: groupedDefinition.definitions
+ };
+ definitionsMerged.push(compatibilityDefinition);
}
await this.buildTermMeta(definitionsMerged, dictionaries);
@@ -610,13 +637,14 @@ class Translator {
static getSearchableText(text, options) {
if (!options.scanning.alphanumeric) {
- const ii = text.length;
- for (let i = 0; i < ii; ++i) {
- if (!jpIsCharCodeJapanese(text.charCodeAt(i))) {
- text = text.substring(0, i);
+ let newText = '';
+ for (const c of text) {
+ if (!jpIsCodePointJapanese(c.codePointAt(0))) {
break;
}
+ newText += c;
}
+ text = newText;
}
return text;
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 5ce4b08c..79c6af06 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -73,7 +73,11 @@ function utilStringHashCode(string) {
}
function utilBackend() {
- return chrome.extension.getBackgroundPage().yomichanBackend;
+ const backend = chrome.extension.getBackgroundPage().yomichanBackend;
+ if (!backend.isPrepared) {
+ throw new Error('Backend not ready yet');
+ }
+ return backend;
}
async function utilAnkiGetModelNames() {
diff --git a/ext/bg/search.html b/ext/bg/search.html
index d6336826..f4c1a737 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -80,7 +80,7 @@
<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/audio-system.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>
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index b048a36c..0db76d71 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -1090,6 +1090,7 @@
<script src="/mixed/js/api.js"></script>
<script src="/bg/js/anki.js"></script>
+ <script src="/bg/js/anki-note-builder.js"></script>
<script src="/bg/js/conditions.js"></script>
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
@@ -1098,7 +1099,7 @@
<script src="/bg/js/page-exit-prevention.js"></script>
<script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/util.js"></script>
- <script src="/mixed/js/audio.js"></script>
+ <script src="/mixed/js/audio-system.js"></script>
<script src="/bg/js/settings/anki.js"></script>
<script src="/bg/js/settings/anki-templates.js"></script>
diff --git a/ext/fg/float.html b/ext/fg/float.html
index 352a866a..7bbed565 100644
--- a/ext/fg/float.html
+++ b/ext/fg/float.html
@@ -46,7 +46,7 @@
<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/audio-system.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>
diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js
index 35861475..490f61bb 100644
--- a/ext/fg/js/document.js
+++ b/ext/fg/js/document.js
@@ -16,7 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global TextSourceElement, TextSourceRange, DOM*/
+/* global
+ * DOM
+ * TextSourceElement
+ * TextSourceRange
+ */
const REGEX_TRANSPARENT_COLOR = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/;
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 8f21a9c5..393c2719 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -16,7 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global popupNestedInitialize, apiForward, apiGetMessageToken, Display*/
+/* global
+ * Display
+ * apiForward
+ * apiGetMessageToken
+ * popupNestedInitialize
+ */
class DisplayFloat extends Display {
constructor() {
@@ -33,8 +38,27 @@ class DisplayFloat extends Display {
this._messageToken = null;
this._messageTokenPromise = null;
- yomichan.on('orphaned', () => this.onOrphaned());
- window.addEventListener('message', (e) => this.onMessage(e), false);
+ this._onKeyDownHandlers = new Map([
+ ['C', (e) => {
+ if (e.ctrlKey && !window.getSelection().toString()) {
+ this.onSelectionCopy();
+ return true;
+ }
+ return false;
+ }],
+ ...this._onKeyDownHandlers
+ ]);
+
+ this._windowMessageHandlers = new Map([
+ ['setContent', ({type, details}) => this.setContent(type, details)],
+ ['clearAutoPlayTimer', () => this.clearAutoPlayTimer()],
+ ['setCustomCss', ({css}) => this.setCustomCss(css)],
+ ['prepare', ({options, popupInfo, url, childrenSupported, scale, uniqueId}) => this.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)],
+ ['setContentScale', ({scale}) => this.setContentScale(scale)]
+ ]);
+
+ yomichan.on('orphaned', this.onOrphaned.bind(this));
+ window.addEventListener('message', this.onMessage.bind(this), false);
}
async prepare(options, popupInfo, url, childrenSupported, scale, uniqueId) {
@@ -96,18 +120,6 @@ class DisplayFloat extends Display {
}
}
- onKeyDown(e) {
- const key = Display.getKeyFromEvent(e);
- const handler = DisplayFloat._onKeyDownHandlers.get(key);
- if (typeof handler === 'function') {
- if (handler(this, e)) {
- e.preventDefault();
- return true;
- }
- }
- return super.onKeyDown(e);
- }
-
async getMessageToken() {
// this._messageTokenPromise is used to ensure that only one call to apiGetMessageToken is made.
if (this._messageTokenPromise === null) {
@@ -126,10 +138,10 @@ class DisplayFloat extends Display {
return;
}
- const handler = DisplayFloat._messageHandlers.get(action);
+ const handler = this._windowMessageHandlers.get(action);
if (typeof handler !== 'function') { return; }
- handler(this, params);
+ handler(params);
}
getOptionsContext() {
@@ -153,22 +165,4 @@ class DisplayFloat extends Display {
}
}
-DisplayFloat._onKeyDownHandlers = new Map([
- ['C', (self, e) => {
- if (e.ctrlKey && !window.getSelection().toString()) {
- self.onSelectionCopy();
- return true;
- }
- return false;
- }]
-]);
-
-DisplayFloat._messageHandlers = new Map([
- ['setContent', (self, {type, details}) => self.setContent(type, details)],
- ['clearAutoPlayTimer', (self) => self.clearAutoPlayTimer()],
- ['setCustomCss', (self, {css}) => self.setCustomCss(css)],
- ['prepare', (self, {options, popupInfo, url, childrenSupported, scale, uniqueId}) => self.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)],
- ['setContentScale', (self, {scale}) => self.setContentScale(scale)]
-]);
-
DisplayFloat.instance = new DisplayFloat();
diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js
index 8dc6aaf3..4431df61 100644
--- a/ext/fg/js/frontend-api-sender.js
+++ b/ext/fg/js/frontend-api-sender.js
@@ -31,6 +31,8 @@ class FrontendApiSender {
invoke(action, params, target) {
if (this.disconnected) {
+ // attempt to reconnect the next time
+ this.disconnected = false;
return Promise.reject(new Error('Disconnected'));
}
@@ -70,6 +72,7 @@ class FrontendApiSender {
onDisconnect() {
this.disconnected = true;
+ this.port = null;
for (const id of this.callbacks.keys()) {
this.onError(id, 'Disconnected');
diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js
index 54b874f2..8424b21d 100644
--- a/ext/fg/js/frontend-initialize.js
+++ b/ext/fg/js/frontend-initialize.js
@@ -16,9 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global PopupProxyHost, PopupProxy, Frontend*/
+/* global
+ * Frontend
+ * PopupProxy
+ * PopupProxyHost
+ */
async function main() {
+ await yomichan.prepare();
+
const data = window.frontendInitializationData || {};
const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data;
@@ -29,7 +35,7 @@ async function main() {
const popupHost = new PopupProxyHost();
await popupHost.prepare();
- popup = popupHost.getOrCreatePopup();
+ popup = popupHost.getOrCreatePopup(null, null, depth);
}
const frontend = new Frontend(popup, ignoreNodes);
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 67045241..768b9326 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -16,7 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiGetZoom, apiOptionsGet, apiTermsFind, apiKanjiFind, docSentenceExtract, TextScanner*/
+/* global
+ * TextScanner
+ * apiGetZoom
+ * apiKanjiFind
+ * apiOptionsGet
+ * apiTermsFind
+ * docSentenceExtract
+ */
class Frontend extends TextScanner {
constructor(popup, ignoreNodes) {
@@ -39,6 +46,15 @@ class Frontend extends TextScanner {
this._contentScale = 1.0;
this._orphaned = true;
this._lastShowPromise = Promise.resolve();
+
+ this._windowMessageHandlers = new Map([
+ ['popupClose', () => this.onSearchClear(true)],
+ ['selectionCopy', () => document.execCommand('copy')]
+ ]);
+
+ this._runtimeMessageHandlers = new Map([
+ ['popupSetVisibleOverride', ({visible}) => { this.popup.setVisibleOverride(visible); }]
+ ]);
}
async prepare() {
@@ -55,9 +71,9 @@ class Frontend extends TextScanner {
window.visualViewport.addEventListener('resize', this.onVisualViewportResize.bind(this));
}
- yomichan.on('orphaned', () => this.onOrphaned());
- yomichan.on('optionsUpdated', () => this.updateOptions());
- yomichan.on('zoomChanged', (e) => this.onZoomChanged(e));
+ yomichan.on('orphaned', this.onOrphaned.bind(this));
+ yomichan.on('optionsUpdated', this.updateOptions.bind(this));
+ yomichan.on('zoomChanged', this.onZoomChanged.bind(this));
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
this._updateContentScale();
@@ -72,17 +88,17 @@ class Frontend extends TextScanner {
onWindowMessage(e) {
const action = e.data;
- const handler = Frontend._windowMessageHandlers.get(action);
+ const handler = this._windowMessageHandlers.get(action);
if (typeof handler !== 'function') { return false; }
- handler(this);
+ handler();
}
onRuntimeMessage({action, params}, sender, callback) {
- const handler = Frontend._runtimeMessageHandlers.get(action);
+ const handler = this._runtimeMessageHandlers.get(action);
if (typeof handler !== 'function') { return false; }
- const result = handler(this, params, sender);
+ const result = handler(params, sender);
callback(result);
return false;
}
@@ -237,12 +253,3 @@ class Frontend extends TextScanner {
return visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0;
}
}
-
-Frontend._windowMessageHandlers = new Map([
- ['popupClose', (self) => self.onSearchClear(true)],
- ['selectionCopy', () => document.execCommand('copy')]
-]);
-
-Frontend._runtimeMessageHandlers = new Map([
- ['popupSetVisibleOverride', (self, {visible}) => { self.popup.setVisibleOverride(visible); }]
-]);
diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js
index 3e5f5b80..06f8fc4b 100644
--- a/ext/fg/js/popup-nested.js
+++ b/ext/fg/js/popup-nested.js
@@ -16,7 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiOptionsGet*/
+/* global
+ * apiOptionsGet
+ */
let popupNestedInitialized = false;
diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js
index e55801ff..793d3949 100644
--- a/ext/fg/js/popup-proxy-host.js
+++ b/ext/fg/js/popup-proxy-host.js
@@ -16,7 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiFrameInformationGet, FrontendApiReceiver, Popup*/
+/* global
+ * FrontendApiReceiver
+ * Popup
+ * apiFrameInformationGet
+ */
class PopupProxyHost {
constructor() {
@@ -34,20 +38,20 @@ class PopupProxyHost {
if (typeof frameId !== 'number') { return; }
this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, new Map([
- ['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)],
- ['setVisibleOverride', ({id, visible}) => this._onApiSetVisibleOverride(id, visible)],
- ['containsPoint', ({id, x, y}) => this._onApiContainsPoint(id, x, y)],
- ['showContent', ({id, elementRect, writingMode, type, details}) => this._onApiShowContent(id, elementRect, writingMode, type, details)],
- ['setCustomCss', ({id, css}) => this._onApiSetCustomCss(id, css)],
- ['clearAutoPlayTimer', ({id}) => this._onApiClearAutoPlayTimer(id)],
- ['setContentScale', ({id, scale}) => this._onApiSetContentScale(id, scale)]
+ ['getOrCreatePopup', this._onApiGetOrCreatePopup.bind(this)],
+ ['setOptions', this._onApiSetOptions.bind(this)],
+ ['hide', this._onApiHide.bind(this)],
+ ['isVisible', this._onApiIsVisibleAsync.bind(this)],
+ ['setVisibleOverride', this._onApiSetVisibleOverride.bind(this)],
+ ['containsPoint', this._onApiContainsPoint.bind(this)],
+ ['showContent', this._onApiShowContent.bind(this)],
+ ['setCustomCss', this._onApiSetCustomCss.bind(this)],
+ ['clearAutoPlayTimer', this._onApiClearAutoPlayTimer.bind(this)],
+ ['setContentScale', this._onApiSetContentScale.bind(this)]
]));
}
- getOrCreatePopup(id=null, parentId=null) {
+ getOrCreatePopup(id=null, parentId=null, depth=null) {
// Find by existing id
if (id !== null) {
const popup = this._popups.get(id);
@@ -76,7 +80,14 @@ class PopupProxyHost {
}
// Create new popup
- const depth = (parent !== null ? parent.depth + 1 : 0);
+ if (parent !== null) {
+ if (depth !== null) {
+ throw new Error('Depth cannot be set when parent exists');
+ }
+ depth = parent.depth + 1;
+ } else if (depth === null) {
+ depth = 0;
+ }
const popup = new Popup(id, depth, this._frameIdPromise);
if (parent !== null) {
popup.setParent(parent);
@@ -87,56 +98,57 @@ class PopupProxyHost {
// Message handlers
- async _onApiGetOrCreatePopup(id, parentId) {
+ async _onApiGetOrCreatePopup({id, parentId}) {
const popup = this.getOrCreatePopup(id, parentId);
return {
id: popup.id
};
}
- async _onApiSetOptions(id, options) {
+ async _onApiSetOptions({id, options}) {
const popup = this._getPopup(id);
return await popup.setOptions(options);
}
- async _onApiHide(id, changeFocus) {
+ async _onApiHide({id, changeFocus}) {
const popup = this._getPopup(id);
return popup.hide(changeFocus);
}
- async _onApiIsVisibleAsync(id) {
+ async _onApiIsVisibleAsync({id}) {
const popup = this._getPopup(id);
return await popup.isVisible();
}
- async _onApiSetVisibleOverride(id, visible) {
+ async _onApiSetVisibleOverride({id, visible}) {
const popup = this._getPopup(id);
return await popup.setVisibleOverride(visible);
}
- async _onApiContainsPoint(id, x, y) {
+ async _onApiContainsPoint({id, x, y}) {
const popup = this._getPopup(id);
+ [x, y] = PopupProxyHost._convertPopupPointToRootPagePoint(popup, x, y);
return await popup.containsPoint(x, y);
}
- async _onApiShowContent(id, elementRect, writingMode, type, details) {
+ async _onApiShowContent({id, elementRect, writingMode, type, details}) {
const popup = this._getPopup(id);
elementRect = PopupProxyHost._convertJsonRectToDOMRect(popup, elementRect);
if (!PopupProxyHost._popupCanShow(popup)) { return; }
return await popup.showContent(elementRect, writingMode, type, details);
}
- async _onApiSetCustomCss(id, css) {
+ async _onApiSetCustomCss({id, css}) {
const popup = this._getPopup(id);
return popup.setCustomCss(css);
}
- async _onApiClearAutoPlayTimer(id) {
+ async _onApiClearAutoPlayTimer({id}) {
const popup = this._getPopup(id);
return popup.clearAutoPlayTimer();
}
- async _onApiSetContentScale(id, scale) {
+ async _onApiSetContentScale({id, scale}) {
const popup = this._getPopup(id);
return popup.setContentScale(scale);
}
@@ -152,14 +164,17 @@ class PopupProxyHost {
}
static _convertJsonRectToDOMRect(popup, jsonRect) {
- let x = jsonRect.x;
- let y = jsonRect.y;
+ const [x, y] = PopupProxyHost._convertPopupPointToRootPagePoint(popup, jsonRect.x, jsonRect.y);
+ return new DOMRect(x, y, jsonRect.width, jsonRect.height);
+ }
+
+ static _convertPopupPointToRootPagePoint(popup, x, y) {
if (popup.parent !== null) {
const popupRect = popup.parent.getContainerRect();
x += popupRect.x;
y += popupRect.y;
}
- return new DOMRect(x, y, jsonRect.width, jsonRect.height);
+ return [x, y];
}
static _popupCanShow(popup) {
diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js
index 093cdd2e..f7cef214 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -16,7 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global FrontendApiSender*/
+/* global
+ * FrontendApiSender
+ */
class PopupProxy {
constructor(id, depth, parentId, parentFrameId, url) {
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index 4927f4bd..d752812e 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -16,7 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global apiInjectStylesheet, apiGetMessageToken*/
+/* global
+ * apiGetMessageToken
+ * apiInjectStylesheet
+ */
class Popup {
constructor(id, depth, frameIdPromise) {
@@ -260,7 +263,7 @@ class Popup {
'mozfullscreenchange',
'webkitfullscreenchange'
];
- const onFullscreenChanged = () => this._onFullscreenChanged();
+ const onFullscreenChanged = this._onFullscreenChanged.bind(this);
for (const eventName of fullscreenEvents) {
this._fullscreenEventListeners.addEventListener(document, eventName, onFullscreenChanged, false);
}
diff --git a/ext/manifest.json b/ext/manifest.json
index fd9b6fec..2a602e3b 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Yomichan (testing)",
- "version": "20.2.24.0",
+ "version": "20.3.14.0",
"description": "Japanese dictionary with Anki integration (testing)",
"icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"},
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css
index 6a5383bc..688a357c 100644
--- a/ext/mixed/css/display.css
+++ b/ext/mixed/css/display.css
@@ -30,8 +30,8 @@
* General
*/
-html:root[data-yomichan-page=float]:not([data-yomichan-theme]),
-html:root[data-yomichan-page=float]:not([data-yomichan-theme]) body {
+:root[data-yomichan-page=float]:not([data-yomichan-theme]),
+:root[data-yomichan-page=float]:not([data-yomichan-theme]) body {
background-color: transparent;
}
@@ -65,10 +65,6 @@ ol, ul {
height: 2.28571428em; /* 14px => 32px */
}
-.invisible {
- visibility: hidden;
-}
-
/*
* Navigation
*/
@@ -82,17 +78,18 @@ ol, ul {
padding: 0.25em 0.5em;
border-bottom-width: 0.07142857em; /* 14px => 1px */
border-bottom-style: solid;
+ z-index: 10;
}
-html:root[data-yomichan-page=search] .navigation-header {
+:root[data-yomichan-page=search] .navigation-header {
position: sticky;
}
-html:root[data-yomichan-page=float] .navigation-header {
+:root[data-yomichan-page=float] .navigation-header {
position: fixed;
}
-html:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation-header-spacer {
+:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation-header-spacer {
height: 2.1em;
}
@@ -136,7 +133,7 @@ html:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation
margin-right: 0.2em;
}
-html:root[data-yomichan-page=search][data-search-mode=popup] .search-input {
+:root[data-yomichan-page=search][data-search-mode=popup] .search-input {
display: none;
}
@@ -150,7 +147,7 @@ html:root[data-yomichan-page=search][data-search-mode=popup] .search-input {
padding-bottom: 0.72em;
}
-html:root[data-yomichan-page=float] .entry {
+:root[data-yomichan-page=float] .entry {
padding-left: 0.72em;
padding-right: 0.72em;
}
@@ -231,7 +228,7 @@ button.action-button {
margin-right: 0.375em;
}
-html:root:not([data-enable-search-tags=true]) .tag[data-category=search] {
+:root:not([data-enable-search-tags=true]) .tag[data-category=search] {
display: none;
}
@@ -280,10 +277,6 @@ html:root:not([data-enable-search-tags=true]) .tag[data-category=search] {
display: inline;
}
-.term-expression-details>.action-play-audio {
- display: none;
-}
-
.term-expression-details>.tags {
display: inline;
}
@@ -321,8 +314,8 @@ html:root:not([data-enable-search-tags=true]) .tag[data-category=search] {
bottom: 0.5em;
}
-.term-expression-list[data-multi=true] .term-expression-details>.action-play-audio {
- display: block;
+.term-expression-list:not([data-multi=true]) .term-expression-details>.action-play-audio {
+ display: none;
}
.term-expression-list[data-multi=true] .term-expression-details>.tags {
diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js
index 7ea68d59..0ab07039 100644
--- a/ext/mixed/js/api.js
+++ b/ext/mixed/js/api.js
@@ -69,8 +69,8 @@ function apiTemplateRender(template, data) {
return _apiInvoke('templateRender', {data, template});
}
-function apiAudioGetUrl(definition, source, optionsContext) {
- return _apiInvoke('audioGetUrl', {definition, source, optionsContext});
+function apiAudioGetUri(definition, source, optionsContext) {
+ return _apiInvoke('audioGetUri', {definition, source, optionsContext});
}
function apiCommandExec(command, params) {
@@ -117,6 +117,10 @@ function apiGetMessageToken() {
return _apiInvoke('getMessageToken');
}
+function apiGetDefaultAnkiFieldTemplates() {
+ return _apiInvoke('getDefaultAnkiFieldTemplates');
+}
+
function _apiInvoke(action, params={}) {
const data = {action, params};
return new Promise((resolve, reject) => {
diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js
new file mode 100644
index 00000000..31c476b1
--- /dev/null
+++ b/ext/mixed/js/audio-system.js
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2019-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/>.
+ */
+
+class TextToSpeechAudio {
+ constructor(text, voice) {
+ this.text = text;
+ this.voice = voice;
+ this._utterance = null;
+ this._volume = 1;
+ }
+
+ get currentTime() {
+ return 0;
+ }
+ set currentTime(value) {
+ // NOP
+ }
+
+ get volume() {
+ return this._volume;
+ }
+ set volume(value) {
+ this._volume = value;
+ if (this._utterance !== null) {
+ this._utterance.volume = value;
+ }
+ }
+
+ play() {
+ try {
+ if (this._utterance === null) {
+ this._utterance = new SpeechSynthesisUtterance(this.text || '');
+ this._utterance.lang = 'ja-JP';
+ this._utterance.volume = this._volume;
+ this._utterance.voice = this.voice;
+ }
+
+ speechSynthesis.cancel();
+ speechSynthesis.speak(this._utterance);
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ pause() {
+ try {
+ speechSynthesis.cancel();
+ } catch (e) {
+ // NOP
+ }
+ }
+}
+
+class AudioSystem {
+ constructor({getAudioUri}) {
+ this._cache = new Map();
+ this._cacheSizeMaximum = 32;
+ this._getAudioUri = getAudioUri;
+
+ if (typeof speechSynthesis !== 'undefined') {
+ // speechSynthesis.getVoices() will not be populated unless some API call is made.
+ speechSynthesis.addEventListener('voiceschanged', this._onVoicesChanged.bind(this));
+ }
+ }
+
+ async getDefinitionAudio(definition, sources, details) {
+ const key = `${definition.expression}:${definition.reading}`;
+ const cacheValue = this._cache.get(definition);
+ if (typeof cacheValue !== 'undefined') {
+ const {audio, uri, source} = cacheValue;
+ return {audio, uri, source};
+ }
+
+ for (const source of sources) {
+ const uri = await this._getAudioUri(definition, source, details);
+ if (uri === null) { continue; }
+
+ try {
+ const audio = await this._createAudio(uri, details);
+ this._cacheCheck();
+ this._cache.set(key, {audio, uri, source});
+ return {audio, uri, source};
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ throw new Error('Could not create audio');
+ }
+
+ createTextToSpeechAudio({text, voiceUri}) {
+ const voice = this._getTextToSpeechVoiceFromVoiceUri(voiceUri);
+ if (voice === null) {
+ throw new Error('Invalid text-to-speech voice');
+ }
+ return new TextToSpeechAudio(text, voice);
+ }
+
+ _onVoicesChanged() {
+ // NOP
+ }
+
+ async _createAudio(uri, details) {
+ const ttsParameters = this._getTextToSpeechParameters(uri);
+ if (ttsParameters !== null) {
+ if (typeof details === 'object' && details !== null) {
+ if (details.tts === false) {
+ throw new Error('Text-to-speech not permitted');
+ }
+ }
+ return this.createTextToSpeechAudio(ttsParameters);
+ }
+
+ return await this._createAudioFromUrl(uri);
+ }
+
+ _createAudioFromUrl(url) {
+ return new Promise((resolve, reject) => {
+ const audio = new Audio(url);
+ audio.addEventListener('loadeddata', () => {
+ const duration = audio.duration;
+ if (duration === 5.694694 || duration === 5.720718) {
+ // Hardcoded values for invalid audio
+ reject(new Error('Could not retrieve audio'));
+ } else {
+ resolve(audio);
+ }
+ });
+ audio.addEventListener('error', () => reject(audio.error));
+ });
+ }
+
+ _getTextToSpeechVoiceFromVoiceUri(voiceUri) {
+ try {
+ for (const voice of speechSynthesis.getVoices()) {
+ if (voice.voiceURI === voiceUri) {
+ return voice;
+ }
+ }
+ } catch (e) {
+ // NOP
+ }
+ return null;
+ }
+
+ _getTextToSpeechParameters(uri) {
+ const m = /^tts:[^#?]*\?([^#]*)/.exec(uri);
+ if (m === null) { return null; }
+
+ const searchParameters = new URLSearchParams(m[1]);
+ const text = searchParameters.get('text');
+ const voiceUri = searchParameters.get('voice');
+ return (text !== null && voiceUri !== null ? {text, voiceUri} : null);
+ }
+
+ _cacheCheck() {
+ const removeCount = this._cache.size - this._cacheSizeMaximum;
+ if (removeCount <= 0) { return; }
+
+ const removeKeys = [];
+ for (const key of this._cache.keys()) {
+ removeKeys.push(key);
+ if (removeKeys.length >= removeCount) { break; }
+ }
+
+ for (const key of removeKeys) {
+ this._cache.delete(key);
+ }
+ }
+}
diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js
deleted file mode 100644
index b5a025be..00000000
--- a/ext/mixed/js/audio.js
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2019-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 apiAudioGetUrl*/
-
-class TextToSpeechAudio {
- constructor(text, voice) {
- this.text = text;
- this.voice = voice;
- this._utterance = null;
- this._volume = 1;
- }
-
- get currentTime() {
- return 0;
- }
- set currentTime(value) {
- // NOP
- }
-
- get volume() {
- return this._volume;
- }
- set volume(value) {
- this._volume = value;
- if (this._utterance !== null) {
- this._utterance.volume = value;
- }
- }
-
- play() {
- try {
- if (this._utterance === null) {
- this._utterance = new SpeechSynthesisUtterance(this.text || '');
- this._utterance.lang = 'ja-JP';
- this._utterance.volume = this._volume;
- this._utterance.voice = this.voice;
- }
-
- speechSynthesis.cancel();
- speechSynthesis.speak(this._utterance);
- } catch (e) {
- // NOP
- }
- }
-
- pause() {
- try {
- speechSynthesis.cancel();
- } catch (e) {
- // NOP
- }
- }
-
- static createFromUri(ttsUri) {
- const m = /^tts:[^#?]*\?([^#]*)/.exec(ttsUri);
- if (m === null) { 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; }
-
- voice = audioGetTextToSpeechVoice(voice);
- if (voice === null) { return null; }
-
- return new TextToSpeechAudio(text, voice);
- }
-}
-
-function audioGetFromUrl(url, willDownload) {
- const tts = TextToSpeechAudio.createFromUri(url);
- if (tts !== null) {
- if (willDownload) {
- throw new Error('AnkiConnect does not support downloading text-to-speech audio.');
- }
- return Promise.resolve(tts);
- }
-
- return new Promise((resolve, reject) => {
- const audio = new Audio(url);
- audio.addEventListener('loadeddata', () => {
- if (audio.duration === 5.694694 || audio.duration === 5.720718) {
- // Hardcoded values for invalid audio
- reject(new Error('Could not retrieve audio'));
- } else {
- resolve(audio);
- }
- });
- audio.addEventListener('error', () => reject(audio.error));
- });
-}
-
-async function audioGetFromSources(expression, sources, optionsContext, willDownload, cache=null) {
- const key = `${expression.expression}:${expression.reading}`;
- if (cache !== null) {
- const cacheValue = cache.get(expression);
- if (typeof cacheValue !== 'undefined') {
- return cacheValue;
- }
- }
-
- for (let i = 0, ii = sources.length; i < ii; ++i) {
- const source = sources[i];
- const url = await apiAudioGetUrl(expression, source, optionsContext);
- if (url === null) {
- continue;
- }
-
- try {
- let audio = await audioGetFromUrl(url, willDownload);
- if (willDownload) {
- // AnkiConnect handles downloading URLs into cards
- audio = null;
- }
- const result = {audio, url, source};
- if (cache !== null) {
- cache.set(key, result);
- }
- return result;
- } catch (e) {
- // NOP
- }
- }
- return {audio: null, url: null, source: null};
-}
-
-function audioGetTextToSpeechVoice(voiceURI) {
- try {
- for (const voice of speechSynthesis.getVoices()) {
- if (voice.voiceURI === voiceURI) {
- return voice;
- }
- }
- } catch (e) {
- // NOP
- }
- return null;
-}
-
-function audioPrepareTextToSpeech(options) {
- if (
- audioPrepareTextToSpeech.state ||
- !options.audio.textToSpeechVoice ||
- !(
- options.audio.sources.includes('text-to-speech') ||
- options.audio.sources.includes('text-to-speech-reading')
- )
- ) {
- // Text-to-speech not in use.
- return;
- }
-
- // Chrome needs this value called once before it will become populated.
- // The first call will return an empty list.
- audioPrepareTextToSpeech.state = true;
- try {
- speechSynthesis.getVoices();
- } catch (e) {
- // NOP
- }
-}
-audioPrepareTextToSpeech.state = false;
diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js
index 83813796..0d50e915 100644
--- a/ext/mixed/js/core.js
+++ b/ext/mixed/js/core.js
@@ -175,21 +175,6 @@ function promiseTimeout(delay, resolveValue) {
return promise;
}
-function stringReplaceAsync(str, regex, replacer) {
- let match;
- let index = 0;
- const parts = [];
- while ((match = regex.exec(str)) !== null) {
- parts.push(str.substring(index, match.index), replacer(...match, match.index, str));
- index = regex.lastIndex;
- }
- if (parts.length === 0) {
- return Promise.resolve(str);
- }
- parts.push(str.substring(index));
- return Promise.all(parts).then((v) => v.join(''));
-}
-
/*
* Common events
@@ -269,7 +254,11 @@ const yomichan = (() => {
constructor() {
super();
+ this._isBackendPreparedResolve = null;
+ this._isBackendPreparedPromise = new Promise((resolve) => (this._isBackendPreparedResolve = resolve));
+
this._messageHandlers = new Map([
+ ['backendPrepared', this._onBackendPrepared.bind(this)],
['getUrl', this._onMessageGetUrl.bind(this)],
['optionsUpdated', this._onMessageOptionsUpdated.bind(this)],
['zoomChanged', this._onMessageZoomChanged.bind(this)]
@@ -280,6 +269,11 @@ const yomichan = (() => {
// Public
+ prepare() {
+ chrome.runtime.sendMessage({action: 'yomichanCoreReady'});
+ return this._isBackendPreparedPromise;
+ }
+
generateId(length) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
@@ -305,6 +299,10 @@ const yomichan = (() => {
return false;
}
+ _onBackendPrepared() {
+ this._isBackendPreparedResolve();
+ }
+
_onMessageGetUrl() {
return {url: window.location.href};
}
diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js
index d7e77cc0..49afc44b 100644
--- a/ext/mixed/js/display-generator.js
+++ b/ext/mixed/js/display-generator.js
@@ -16,7 +16,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-/*global apiGetDisplayTemplatesHtml, TemplateHandler*/
+/* global
+ * TemplateHandler
+ * apiGetDisplayTemplatesHtml
+ */
class DisplayGenerator {
constructor() {
@@ -298,7 +301,7 @@ class DisplayGenerator {
}
static _isCharacterKanji(c) {
- const code = c.charCodeAt(0);
+ const code = c.codePointAt(0);
return (
code >= 0x4e00 && code < 0x9fb0 ||
code >= 0x3400 && code < 0x4dc0
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 5d3076ee..515e28a7 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -16,11 +16,24 @@
* 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*/
+/* global
+ * AudioSystem
+ * DOM
+ * DisplayContext
+ * DisplayGenerator
+ * WindowScroll
+ * apiAudioGetUri
+ * apiDefinitionAdd
+ * apiDefinitionsAddable
+ * apiForward
+ * apiKanjiFind
+ * apiNoteView
+ * apiOptionsGet
+ * apiScreenshotGet
+ * apiTermsFind
+ * docRangeFromPoint
+ * docSentenceExtract
+ */
class Display {
constructor(spinner, container) {
@@ -32,7 +45,7 @@ class Display {
this.index = 0;
this.audioPlaying = null;
this.audioFallback = null;
- this.audioCache = new Map();
+ this.audioSystem = new AudioSystem({getAudioUri: this._getAudioUri.bind(this)});
this.styleNode = null;
this.eventListeners = new EventListenerCollection();
@@ -45,10 +58,115 @@ class Display {
this.displayGenerator = new DisplayGenerator();
this.windowScroll = new WindowScroll();
+ this._onKeyDownHandlers = new Map([
+ ['Escape', () => {
+ this.onSearchClear();
+ return true;
+ }],
+ ['PageUp', (e) => {
+ if (e.altKey) {
+ this.entryScrollIntoView(this.index - 3, null, true);
+ return true;
+ }
+ return false;
+ }],
+ ['PageDown', (e) => {
+ if (e.altKey) {
+ this.entryScrollIntoView(this.index + 3, null, true);
+ return true;
+ }
+ return false;
+ }],
+ ['End', (e) => {
+ if (e.altKey) {
+ this.entryScrollIntoView(this.definitions.length - 1, null, true);
+ return true;
+ }
+ return false;
+ }],
+ ['Home', (e) => {
+ if (e.altKey) {
+ this.entryScrollIntoView(0, null, true);
+ return true;
+ }
+ return false;
+ }],
+ ['ArrowUp', (e) => {
+ if (e.altKey) {
+ this.entryScrollIntoView(this.index - 1, null, true);
+ return true;
+ }
+ return false;
+ }],
+ ['ArrowDown', (e) => {
+ if (e.altKey) {
+ this.entryScrollIntoView(this.index + 1, null, true);
+ return true;
+ }
+ return false;
+ }],
+ ['B', (e) => {
+ if (e.altKey) {
+ this.sourceTermView();
+ return true;
+ }
+ return false;
+ }],
+ ['F', (e) => {
+ if (e.altKey) {
+ this.nextTermView();
+ return true;
+ }
+ return false;
+ }],
+ ['E', (e) => {
+ if (e.altKey) {
+ this.noteTryAdd('term-kanji');
+ return true;
+ }
+ return false;
+ }],
+ ['K', (e) => {
+ if (e.altKey) {
+ this.noteTryAdd('kanji');
+ return true;
+ }
+ return false;
+ }],
+ ['R', (e) => {
+ if (e.altKey) {
+ this.noteTryAdd('term-kana');
+ return true;
+ }
+ return false;
+ }],
+ ['P', (e) => {
+ if (e.altKey) {
+ const index = this.index;
+ if (index < 0 || index >= this.definitions.length) { return; }
+
+ const entry = this.getEntry(index);
+ if (entry !== null && entry.dataset.type === 'term') {
+ this.audioPlay(this.definitions[index], this.firstExpressionIndex, index);
+ }
+ return true;
+ }
+ return false;
+ }],
+ ['V', (e) => {
+ if (e.altKey) {
+ this.noteTryView();
+ return true;
+ }
+ return false;
+ }]
+ ]);
+
this.setInteractive(true);
}
async prepare(options=null) {
+ await yomichan.prepare();
const displayGeneratorPromise = this.displayGenerator.prepare();
const updateOptionsPromise = this.updateOptions(options);
await Promise.all([displayGeneratorPromise, updateOptionsPromise]);
@@ -215,9 +333,9 @@ class Display {
onKeyDown(e) {
const key = Display.getKeyFromEvent(e);
- const handler = Display._onKeyDownHandlers.get(key);
+ const handler = this._onKeyDownHandlers.get(key);
if (typeof handler === 'function') {
- if (handler(this, e)) {
+ if (handler(e)) {
e.preventDefault();
return true;
}
@@ -259,13 +377,12 @@ class Display {
this.updateDocumentOptions(this.options);
this.updateTheme(this.options.general.popupTheme);
this.setCustomCss(this.options.general.customPopupCss);
- audioPrepareTextToSpeech(this.options);
}
updateDocumentOptions(options) {
const data = document.documentElement.dataset;
data.ankiEnabled = `${options.anki.enable}`;
- data.audioEnabled = `${options.audio.enable}`;
+ data.audioEnabled = `${options.audio.enabled}`;
data.compactGlossaries = `${options.general.compactGlossaries}`;
data.enableSearchTags = `${options.scanning.enableSearchTags}`;
data.debug = `${options.general.debugInfo}`;
@@ -520,15 +637,13 @@ class Display {
updateAdderButtons(states) {
for (let i = 0; i < states.length; ++i) {
- const state = states[i];
let noteId = null;
- for (const mode in state) {
+ for (const [mode, info] of Object.entries(states[i])) {
const button = this.adderButtonFind(i, mode);
if (button === null) {
continue;
}
- const info = state[mode];
if (!info.canAdd && noteId === null && info.noteId) {
noteId = info.noteId;
}
@@ -635,7 +750,7 @@ class Display {
this.setSpinnerVisible(true);
const context = {};
- if (this.noteUsesScreenshot()) {
+ if (this.noteUsesScreenshot(mode)) {
const screenshot = await this.getScreenshot();
if (screenshot) {
context.screenshot = screenshot;
@@ -672,16 +787,16 @@ class Display {
}
const sources = this.options.audio.sources;
- let {audio, source} = await audioGetFromSources(expression, sources, this.getOptionsContext(), false, this.audioCache);
- let info;
- if (audio === null) {
+ let audio, source, info;
+ try {
+ ({audio, source} = await this.audioSystem.getDefinitionAudio(expression, sources));
+ info = `From source ${1 + sources.indexOf(source)}: ${source}`;
+ } catch (e) {
if (this.audioFallback === null) {
this.audioFallback = new Audio('/mixed/mp3/button.mp3');
}
audio = this.audioFallback;
info = 'Could not find audio';
- } else {
- info = `From source ${1 + sources.indexOf(source)}: ${source}`;
}
const button = this.audioButtonFindImage(entryIndex);
@@ -705,10 +820,11 @@ class Display {
}
}
- noteUsesScreenshot() {
- const fields = this.options.anki.terms.fields;
- for (const name in fields) {
- if (fields[name].includes('{screenshot}')) {
+ noteUsesScreenshot(mode) {
+ const optionsAnki = this.options.anki;
+ const fields = (mode === 'kanji' ? optionsAnki.kanji : optionsAnki.terms).fields;
+ for (const fieldValue of Object.values(fields)) {
+ if (fieldValue.includes('{screenshot}')) {
return true;
}
}
@@ -814,121 +930,9 @@ class Display {
const key = event.key;
return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : '');
}
-}
-
-Display._onKeyDownHandlers = new Map([
- ['Escape', (self) => {
- self.onSearchClear();
- return true;
- }],
-
- ['PageUp', (self, e) => {
- if (e.altKey) {
- self.entryScrollIntoView(self.index - 3, null, true);
- return true;
- }
- return false;
- }],
-
- ['PageDown', (self, e) => {
- if (e.altKey) {
- self.entryScrollIntoView(self.index + 3, null, true);
- return true;
- }
- return false;
- }],
-
- ['End', (self, e) => {
- if (e.altKey) {
- self.entryScrollIntoView(self.definitions.length - 1, null, true);
- return true;
- }
- return false;
- }],
-
- ['Home', (self, e) => {
- if (e.altKey) {
- self.entryScrollIntoView(0, null, true);
- return true;
- }
- return false;
- }],
- ['ArrowUp', (self, e) => {
- if (e.altKey) {
- self.entryScrollIntoView(self.index - 1, null, true);
- return true;
- }
- return false;
- }],
-
- ['ArrowDown', (self, e) => {
- if (e.altKey) {
- self.entryScrollIntoView(self.index + 1, null, true);
- return true;
- }
- return false;
- }],
-
- ['B', (self, e) => {
- if (e.altKey) {
- self.sourceTermView();
- return true;
- }
- return false;
- }],
-
- ['F', (self, e) => {
- if (e.altKey) {
- self.nextTermView();
- return true;
- }
- return false;
- }],
-
- ['E', (self, e) => {
- if (e.altKey) {
- self.noteTryAdd('term-kanji');
- return true;
- }
- return false;
- }],
-
- ['K', (self, e) => {
- if (e.altKey) {
- self.noteTryAdd('kanji');
- return true;
- }
- return false;
- }],
-
- ['R', (self, e) => {
- if (e.altKey) {
- self.noteTryAdd('term-kana');
- return true;
- }
- return false;
- }],
-
- ['P', (self, e) => {
- if (e.altKey) {
- 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[index], self.firstExpressionIndex, index);
- }
- return true;
- }
- return false;
- }],
-
- ['V', (self, e) => {
- if (e.altKey) {
- self.noteTryView();
- return true;
- }
- return false;
- }]
-]);
+ async _getAudioUri(definition, source) {
+ const optionsContext = this.getOptionsContext();
+ return await apiAudioGetUri(definition, source, optionsContext);
+ }
+}
diff --git a/ext/mixed/js/scroll.js b/ext/mixed/js/scroll.js
index 5829d294..72da8b65 100644
--- a/ext/mixed/js/scroll.js
+++ b/ext/mixed/js/scroll.js
@@ -26,7 +26,7 @@ class WindowScroll {
this.animationEndTime = 0;
this.animationEndX = 0;
this.animationEndY = 0;
- this.requestAnimationFrameCallback = (t) => this.onAnimationFrame(t);
+ this.requestAnimationFrameCallback = this.onAnimationFrame.bind(this);
}
toY(y) {
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index ff0eac8b..a08e09fb 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -16,7 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global docRangeFromPoint, TextSourceRange, DOM*/
+/* global
+ * DOM
+ * TextSourceRange
+ * docRangeFromPoint
+ */
class TextScanner {
constructor(node, ignoreNodes, ignoreElements, ignorePoints) {
diff --git a/package-lock.json b/package-lock.json
index 505c71db..88ba43f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,18 +24,48 @@
"js-tokens": "^4.0.0"
}
},
+ "abab": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",
+ "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==",
+ "dev": true
+ },
"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-globals": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz",
+ "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==",
+ "dev": true,
+ "requires": {
+ "acorn": "^6.0.1",
+ "acorn-walk": "^6.0.1"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
+ "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==",
+ "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
},
+ "acorn-walk": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
+ "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==",
+ "dev": true
+ },
"ajv": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz",
@@ -81,12 +111,45 @@
"sprintf-js": "~1.0.2"
}
},
+ "asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "dev": true
+ },
"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
},
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "dev": true
+ },
+ "aws-sign2": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+ "dev": true
+ },
+ "aws4": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
+ "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==",
+ "dev": true
+ },
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -99,6 +162,15 @@
"integrity": "sha512-UCIPaDJrNNj5jG2ZL+nzJ7czvZV/ZYX6LaIRgfVU1k1edJOQg7dkbiSKzwHkNp6aHEHER/PhlFBrMYnlvJJQEw==",
"dev": true
},
+ "bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "dev": true,
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -109,12 +181,24 @@
"concat-map": "0.0.1"
}
},
+ "browser-process-hrtime": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz",
+ "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==",
+ "dev": true
+ },
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
},
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+ "dev": true
+ },
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -162,6 +246,15 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -174,6 +267,12 @@
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
"dev": true
},
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+ "dev": true
+ },
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -195,6 +294,77 @@
}
}
},
+ "cssom": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
+ "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==",
+ "dev": true
+ },
+ "cssstyle": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.2.0.tgz",
+ "integrity": "sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA==",
+ "dev": true,
+ "requires": {
+ "cssom": "~0.3.6"
+ },
+ "dependencies": {
+ "cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true
+ }
+ }
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "data-urls": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
+ "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
+ "dev": true,
+ "requires": {
+ "abab": "^2.0.3",
+ "whatwg-mimetype": "^2.3.0",
+ "whatwg-url": "^8.0.0"
+ },
+ "dependencies": {
+ "tr46": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz",
+ "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.1"
+ }
+ },
+ "webidl-conversions": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+ "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
+ "dev": true
+ },
+ "whatwg-url": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.0.0.tgz",
+ "integrity": "sha512-41ou2Dugpij8/LPO5Pq64K5q++MnRCBpEHvQr26/mArEKTkCV5aoXIqyhuYtE0pkqScXwhf2JP57rkRTYM29lQ==",
+ "dev": true,
+ "requires": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^2.0.0",
+ "webidl-conversions": "^5.0.0"
+ }
+ }
+ }
+ },
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -204,12 +374,24 @@
"ms": "^2.1.1"
}
},
+ "decimal.js": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz",
+ "integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==",
+ "dev": true
+ },
"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
},
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "dev": true
+ },
"doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -228,6 +410,16 @@
"webidl-conversions": "^4.0.2"
}
},
+ "ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+ "dev": true,
+ "requires": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -240,6 +432,19 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
+ "escodegen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz",
+ "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==",
+ "dev": true,
+ "requires": {
+ "esprima": "^4.0.1",
+ "estraverse": "^4.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1",
+ "source-map": "~0.6.1"
+ }
+ },
"eslint": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
@@ -363,6 +568,12 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
"external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@@ -374,6 +585,12 @@
"tmp": "^0.0.33"
}
},
+ "extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+ "dev": true
+ },
"fake-indexeddb": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-3.0.0.tgz",
@@ -437,6 +654,23 @@
"integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
"dev": true
},
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+ "dev": true
+ },
+ "form-data": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+ "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ }
+ },
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -449,6 +683,15 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true
},
+ "getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
@@ -481,12 +724,48 @@
"type-fest": "^0.8.1"
}
},
+ "har-schema": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+ "dev": true
+ },
+ "har-validator": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+ "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.5.5",
+ "har-schema": "^2.0.0"
+ }
+ },
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
+ "html-encoding-sniffer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.0.tgz",
+ "integrity": "sha512-Y9prnPKkM7FXxQevZ5UH8Z6aVTY0ede1tHquck5UxGmKWDshxXh95gSa2xXYjS8AsGO5iOvrCI5+GttRKnLdNA==",
+ "dev": true,
+ "requires": {
+ "whatwg-encoding": "^1.0.5"
+ }
+ },
+ "http-signature": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ }
+ },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -555,6 +834,12 @@
"through": "^2.3.6"
}
},
+ "ip-regex": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
+ "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
+ "dev": true
+ },
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -576,18 +861,36 @@
"is-extglob": "^2.1.1"
}
},
+ "is-potential-custom-element-name": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz",
+ "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=",
+ "dev": true
+ },
"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
},
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+ "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
},
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+ "dev": true
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -604,6 +907,89 @@
"esprima": "^4.0.0"
}
},
+ "jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+ "dev": true
+ },
+ "jsdom": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.2.0.tgz",
+ "integrity": "sha512-6VaW3UWyKbm9DFVIAgTfhuwnvqiqlRYNg5Rk6dINTVoZT0eKz+N86vQZr+nqt1ny1lSB1TWZJWSEWQAfu8oTpA==",
+ "dev": true,
+ "requires": {
+ "abab": "^2.0.3",
+ "acorn": "^7.1.0",
+ "acorn-globals": "^4.3.4",
+ "cssom": "^0.4.4",
+ "cssstyle": "^2.2.0",
+ "data-urls": "^2.0.0",
+ "decimal.js": "^10.2.0",
+ "domexception": "^2.0.1",
+ "escodegen": "^1.13.0",
+ "html-encoding-sniffer": "^2.0.0",
+ "is-potential-custom-element-name": "^1.0.0",
+ "nwsapi": "^2.2.0",
+ "parse5": "5.1.1",
+ "request": "^2.88.0",
+ "request-promise-native": "^1.0.8",
+ "saxes": "^4.0.2",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^3.0.1",
+ "w3c-hr-time": "^1.0.1",
+ "w3c-xmlserializer": "^2.0.0",
+ "webidl-conversions": "^5.0.0",
+ "whatwg-encoding": "^1.0.5",
+ "whatwg-mimetype": "^2.3.0",
+ "whatwg-url": "^8.0.0",
+ "ws": "^7.2.1",
+ "xml-name-validator": "^3.0.0"
+ },
+ "dependencies": {
+ "domexception": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
+ "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
+ "dev": true,
+ "requires": {
+ "webidl-conversions": "^5.0.0"
+ }
+ },
+ "tr46": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz",
+ "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.1"
+ }
+ },
+ "webidl-conversions": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+ "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
+ "dev": true
+ },
+ "whatwg-url": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.0.0.tgz",
+ "integrity": "sha512-41ou2Dugpij8/LPO5Pq64K5q++MnRCBpEHvQr26/mArEKTkCV5aoXIqyhuYtE0pkqScXwhf2JP57rkRTYM29lQ==",
+ "dev": true,
+ "requires": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^2.0.0",
+ "webidl-conversions": "^5.0.0"
+ }
+ }
+ }
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+ "dev": true
+ },
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -616,6 +1002,24 @@
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+ "dev": true
+ },
+ "jsprim": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+ "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ }
+ },
"levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@@ -638,6 +1042,21 @@
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true
},
+ "mime-db": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
+ "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
+ "dev": true
+ },
+ "mime-types": {
+ "version": "2.1.26",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
+ "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
+ "dev": true,
+ "requires": {
+ "mime-db": "1.43.0"
+ }
+ },
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -692,6 +1111,18 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
+ "nwsapi": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
+ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==",
+ "dev": true
+ },
+ "oauth-sign": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+ "dev": true
+ },
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -739,6 +1170,12 @@
"callsites": "^3.0.0"
}
},
+ "parse5": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
+ "dev": true
+ },
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -751,6 +1188,12 @@
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
"dev": true
},
+ "performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+ "dev": true
+ },
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
@@ -763,12 +1206,24 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
+ "psl": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
+ "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==",
+ "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
},
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "dev": true
+ },
"realistic-structured-clone": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/realistic-structured-clone/-/realistic-structured-clone-2.0.2.tgz",
@@ -787,6 +1242,78 @@
"integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
"dev": true
},
+ "request": {
+ "version": "2.88.2",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
+ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
+ "dev": true,
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.5.0",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ },
+ "dependencies": {
+ "tough-cookie": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "dev": true,
+ "requires": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ }
+ }
+ }
+ },
+ "request-promise-core": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz",
+ "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.15"
+ }
+ },
+ "request-promise-native": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz",
+ "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==",
+ "dev": true,
+ "requires": {
+ "request-promise-core": "1.1.3",
+ "stealthy-require": "^1.1.1",
+ "tough-cookie": "^2.3.3"
+ },
+ "dependencies": {
+ "tough-cookie": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "dev": true,
+ "requires": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ }
+ }
+ }
+ },
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -830,12 +1357,27 @@
"tslib": "^1.9.0"
}
},
+ "safe-buffer": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+ "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
+ "dev": true
+ },
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
+ "saxes": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-4.0.2.tgz",
+ "integrity": "sha512-EZOTeQ4bgkOaGCDaTKux+LaRNcLNbdbvMH7R3/yjEEULPEmqvkFbFub6DJhJTub2iGMT93CfpZ5LTdKZmAbVeQ==",
+ "dev": true,
+ "requires": {
+ "xmlchars": "^2.2.0"
+ }
+ },
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -888,12 +1430,42 @@
}
}
},
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "optional": 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
},
+ "sshpk": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+ "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+ "dev": true,
+ "requires": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ }
+ },
+ "stealthy-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
+ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
+ "dev": true
+ },
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
@@ -948,6 +1520,12 @@
"has-flag": "^3.0.0"
}
},
+ "symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
"table": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
@@ -1006,6 +1584,17 @@
"os-tmpdir": "~1.0.2"
}
},
+ "tough-cookie": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
+ "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
+ "dev": true,
+ "requires": {
+ "ip-regex": "^2.1.0",
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ }
+ },
"tr46": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
@@ -1021,6 +1610,21 @@
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
"dev": true
},
+ "tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "dev": true
+ },
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
@@ -1062,18 +1666,68 @@
"punycode": "^2.1.0"
}
},
+ "uuid": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+ "dev": true
+ },
"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
},
+ "verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "w3c-hr-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",
+ "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=",
+ "dev": true,
+ "requires": {
+ "browser-process-hrtime": "^0.1.2"
+ }
+ },
+ "w3c-xmlserializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
+ "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==",
+ "dev": true,
+ "requires": {
+ "xml-name-validator": "^3.0.0"
+ }
+ },
"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-encoding": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
+ "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==",
+ "dev": true,
+ "requires": {
+ "iconv-lite": "0.4.24"
+ }
+ },
+ "whatwg-mimetype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
+ "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==",
+ "dev": true
+ },
"whatwg-url": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
@@ -1114,6 +1768,24 @@
"requires": {
"mkdirp": "^0.5.1"
}
+ },
+ "ws": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz",
+ "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==",
+ "dev": true
+ },
+ "xml-name-validator": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
+ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
+ "dev": true
+ },
+ "xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
}
}
}
diff --git a/package.json b/package.json
index 17fdfa82..eb449ea9 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,8 @@
},
"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"
+ "test-lint": "eslint . && node ./test/lint/global-declarations.js",
+ "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document.js"
},
"repository": {
"type": "git",
@@ -29,6 +29,7 @@
"devDependencies": {
"eslint": "^6.8.0",
"eslint-plugin-no-unsanitized": "^3.0.2",
- "fake-indexeddb": "^3.0.0"
+ "fake-indexeddb": "^3.0.0",
+ "jsdom": "^16.2.0"
}
}
diff --git a/test/data/html/test-document1.html b/test/data/html/test-document1.html
new file mode 100644
index 00000000..0754a314
--- /dev/null
+++ b/test/data/html/test-document1.html
@@ -0,0 +1,264 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <title>Yomichan Tests</title>
+ <link rel="icon" type="image/gif" href="data:image/gif;base64,R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAImFI6Zpt0B4YkS0TCpq07xbmEgcGVRUpLaI46ZG7ppalY0jDCwUAAAOw==" />
+ <link rel="stylesheet" href="test-stylesheet.css" />
+ </head>
+<body>
+
+ <h1>Yomichan Tests</h1>
+
+ <div
+ class="test"
+ data-test-type="scan"
+ data-element-from-point-selector="span"
+ data-caret-range-from-point-selector="span"
+ data-start-node-selector="span"
+ data-start-offset="0"
+ data-end-node-selector="span"
+ data-end-offset="0"
+ data-result-type="TextSourceRange",
+ data-sentence-extent="100"
+ data-sentence="真白「心配してくださって、ありがとございます」"
+ >
+ <span>真白「心配してくださって、ありがとございます」</span>
+ </div>
+
+ <div
+ class="test"
+ data-test-type="scan"
+ data-element-from-point-selector="span"
+ data-caret-range-from-point-selector="span"
+ data-start-node-selector="span"
+ data-start-offset="5"
+ data-end-node-selector="span"
+ data-end-offset="5"
+ data-result-type="TextSourceRange",
+ data-sentence-extent="100"
+ data-sentence="心配してくださって、ありがとございます"
+ >
+ <span>真白「心配してくださって、ありがとございます」</span>
+ </div>
+
+ <div
+ class="test"
+ data-test-type="scan"
+ data-element-from-point-selector="input"
+ data-caret-range-from-point-selector="input"
+ data-start-node-selector="input"
+ data-start-offset="0"
+ data-end-node-selector="input"
+ data-end-offset="0"
+ data-result-type="TextSourceRange",
+ data-sentence-extent="100"
+ data-sentence="真白「心配してくださって、ありがとございます」"
+ data-has-imposter="true"
+ >
+ <input type="text" value="真白「心配してくださって、ありがとございます」" style="width: 100%; box-sizing: border-box; font-family: inherit; font-size: inherit; border: 1px solid #d8d8d8; padding: 0.2em;" />
+ </div>
+
+ <div
+ class="test"
+ data-test-type="scan"
+ data-element-from-point-selector="textarea"
+ data-caret-range-from-point-selector="textarea"
+ data-start-node-selector="textarea"
+ data-start-offset="0"
+ data-end-node-selector="textarea"
+ data-end-offset="0"
+ data-result-type="TextSourceRange",
+ data-sentence-extent="100"
+ data-sentence="真白「心配してくださって、ありがとございます」"
+ data-has-imposter="true"
+ >
+ <textarea style="width: 100%; height: 3em; box-sizing: border-box; font-family: inherit; font-size: inherit; border: 1px solid #d8d8d8; padding: 0.2em;">真白「心配してくださって、ありがとございます」</textarea>
+ </div>
+
+ <div
+ class="test"
+ data-test-type="scan"
+ data-element-from-point-selector="button"
+ data-caret-range-from-point-selector="button"
+ data-start-node-selector="button"
+ data-start-offset="0"
+ data-end-node-selector="button"
+ data-end-offset="0"
+ data-result-type="TextSourceElement",
+ data-sentence-extent="100"
+ data-sentence="よみちゃん"
+ >
+ <button style="width: 100%; box-sizing: border-box; font-family: inherit; font-size: inherit; border: 1px solid #d8d8d8; background-color: #f0f0f0; padding: 0.2em;">よみちゃん</button>
+ </div>
+
+ <div
+ class="test"
+ data-test-type="scan"
+ data-element-from-point-selector="img"
+ data-caret-range-from-point-selector="img"
+ data-start-node-selector="img"
+ data-start-offset="0"
+ data-end-node-selector="img"
+ data-end-offset="0"
+ data-result-type="TextSourceElement"
+ data-sentence="よみちゃん"
+ >
+ <img src="data:image/gif;base64,R0lGODdhBwAHAIABAAAAAP///ywAAAAABwAHAAACDIRvEaC32FpCbEkKCgA7" alt="よみちゃん" title="よみちゃん" style="width: 70px; height: 70px; image-rendering: crisp-edges; image-rendering: pixelated; display: block;" />
+ </div>
+
+ <div
+ class="test"
+ data-test-type="text-source-range-seek"
+ data-seek-node-selector="span:nth-of-type(1)"
+ data-seek-node-is-text="true"
+ data-seek-offset="0"
+ data-seek-length="149"
+ data-seek-direction="forward"
+ data-expected-result-node-selector="span:nth-of-type(1)"
+ data-expected-result-node-is-text="true"
+ data-expected-result-offset="149"
+ data-expected-result-content="
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ "
+ >
+ <span>
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ </span><span>trailing content</span>
+ </div>
+
+ <div
+ class="test"
+ data-test-type="text-source-range-seek"
+ data-seek-node-selector="span:nth-of-type(1)"
+ data-seek-node-is-text="true"
+ data-seek-offset="149"
+ data-seek-length="149"
+ data-seek-direction="backward"
+ data-expected-result-node-selector="span:nth-of-type(1)"
+ data-expected-result-node-is-text="true"
+ data-expected-result-offset="0"
+ data-expected-result-content="
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ "
+ >
+ <span>
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ </span><span>trailing content</span>
+ </div>
+
+ <div
+ class="test"
+ data-test-type="text-source-range-seek"
+ data-seek-node-selector="span:nth-of-type(1)"
+ data-seek-node-is-text="true"
+ data-seek-offset="0"
+ data-seek-length="150"
+ data-seek-direction="forward"
+ data-expected-result-node-selector="span:nth-of-type(2)"
+ data-expected-result-node-is-text="true"
+ data-expected-result-offset="1"
+ data-expected-result-content="
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ t"
+ >
+ <span>
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ </span><span>trailing content</span>
+ </div>
+
+ <div
+ class="test"
+ data-test-type="text-source-range-seek"
+ data-seek-node-selector="span:nth-of-type(2)"
+ data-seek-node-is-text="true"
+ data-seek-offset="1"
+ data-seek-length="150"
+ data-seek-direction="backward"
+ data-expected-result-node-selector="span:nth-of-type(1)"
+ data-expected-result-node-is-text="true"
+ data-expected-result-offset="0"
+ data-expected-result-content="
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ t"
+ >
+ <span>
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ </span><span>trailing content</span>
+ </div>
+
+</body>
+</html> \ No newline at end of file
diff --git a/test/data/html/test-stylesheet.css b/test/data/html/test-stylesheet.css
new file mode 100644
index 00000000..ab25732e
--- /dev/null
+++ b/test/data/html/test-stylesheet.css
@@ -0,0 +1,32 @@
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ max-width: 680px;
+ padding: 0 1em;
+ box-sizing: border-box;
+ margin: 0 auto;
+ background-color: #f8f8f8;
+ counter-reset: test-id;
+}
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+.test {
+ background-color: #ffffff;
+ margin: 1em 0;
+ padding: 0.5em;
+ box-shadow: rgba(64, 64, 64, 0.3) 0px 1px 2px 0px, rgba(64, 64, 64, 0.15) 0px 1px 3px 1px;
+ border-radius: 4px;
+}
+
+.test:before {
+ content: "Test " counter(test-id);
+ display: block;
+ counter-increment: test-id;
+ margin-bottom: 0.5em;
+ border-bottom: 1px solid #d8d8d8;
+ font-weight: bold;
+}
diff --git a/test/dictionary-validate.js b/test/dictionary-validate.js
index 14eee2ed..6496f2ac 100644
--- a/test/dictionary-validate.js
+++ b/test/dictionary-validate.js
@@ -18,10 +18,12 @@
const fs = require('fs');
const path = require('path');
-const yomichanTest = require('./yomichan-test');
+const {JSZip} = require('./yomichan-test');
+const {VM} = require('./yomichan-vm');
-const JSZip = yomichanTest.JSZip;
-const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+const vm = new VM();
+vm.execute('bg/js/json-schema.js');
+const JsonSchema = vm.get('JsonSchema');
function readSchema(relativeFileName) {
diff --git a/test/lint/global-declarations.js b/test/lint/global-declarations.js
new file mode 100644
index 00000000..2629cc5e
--- /dev/null
+++ b/test/lint/global-declarations.js
@@ -0,0 +1,105 @@
+/*
+ * 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 assert = require('assert');
+const {getAllFiles} = require('../yomichan-test');
+
+
+function countOccurences(string, pattern) {
+ return (string.match(pattern) || []).length;
+}
+
+function getNewline(string) {
+ const count1 = countOccurences(string, /(?:^|[^\r])\n/g);
+ const count2 = countOccurences(string, /\r\n/g);
+ const count3 = countOccurences(string, /\r(?:[^\n]|$)/g);
+ if (count2 > count1) {
+ return (count3 > count2) ? '\r' : '\r\n';
+ } else {
+ return (count3 > count1) ? '\r' : '\n';
+ }
+}
+
+
+function validateGlobals(fileName, fix) {
+ const pattern = /\/\*\s*global\s+([\w\W]*?)\*\//g;
+ const trimPattern = /^[\s,*]+|[\s,*]+$/g;
+ const splitPattern = /[\s,*]+/;
+ const source = fs.readFileSync(fileName, {encoding: 'utf8'});
+ let match;
+ let first = true;
+ let endIndex = 0;
+ let newSource = '';
+ const newline = getNewline(source);
+ while ((match = pattern.exec(source)) !== null) {
+ if (!first) {
+ console.error(`Encountered more than one global declaration in ${fileName}`);
+ return false;
+ }
+ first = false;
+
+ const parts = match[1].replace(trimPattern, '').split(splitPattern);
+ parts.sort();
+
+ const actual = match[0];
+ const expected = `/* global${parts.map((v) => `${newline} * ${v}`).join('')}${newline} */`;
+
+ try {
+ assert.strictEqual(actual, expected);
+ } catch (e) {
+ console.error(`Global declaration error encountered in ${fileName}:`);
+ console.error(e.message);
+ if (!fix) {
+ return false;
+ }
+ }
+
+ newSource += source.substring(0, match.index);
+ newSource += expected;
+ endIndex = match.index + match[0].length;
+ }
+
+ newSource += source.substring(endIndex);
+
+ if (fix) {
+ fs.writeFileSync(fileName, newSource, {encoding: 'utf8'});
+ }
+
+ return true;
+}
+
+
+function main() {
+ const fix = (process.argv.length >= 2 && process.argv[2] === '--fix');
+ const directory = path.resolve(__dirname, '..', '..', 'ext');
+ const pattern = /\.js$/;
+ const ignorePattern = /[\\/]ext[\\/]mixed[\\/]lib[\\/]/;
+ const fileNames = getAllFiles(directory, (f) => pattern.test(f) && !ignorePattern.test(f));
+ for (const fileName of fileNames) {
+ if (!validateGlobals(fileName, fix)) {
+ process.exit(-1);
+ return;
+ }
+ }
+ process.exit(0);
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/schema-validate.js b/test/schema-validate.js
index a4f2d94c..eb31aa8d 100644
--- a/test/schema-validate.js
+++ b/test/schema-validate.js
@@ -17,9 +17,11 @@
*/
const fs = require('fs');
-const yomichanTest = require('./yomichan-test');
+const {VM} = require('./yomichan-vm');
-const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+const vm = new VM();
+vm.execute('bg/js/json-schema.js');
+const JsonSchema = vm.get('JsonSchema');
function main() {
diff --git a/test/test-database.js b/test/test-database.js
index c2317881..833aa75d 100644
--- a/test/test-database.js
+++ b/test/test-database.js
@@ -21,6 +21,7 @@ const url = require('url');
const path = require('path');
const assert = require('assert');
const yomichanTest = require('./yomichan-test');
+const {VM} = require('./yomichan-vm');
require('fake-indexeddb/auto');
const chrome = {
@@ -30,6 +31,9 @@ const chrome = {
},
getURL(path2) {
return url.pathToFileURL(path.join(__dirname, '..', 'ext', path2.replace(/^\//, '')));
+ },
+ sendMessage() {
+ // NOP
}
}
};
@@ -88,24 +92,24 @@ class XMLHttpRequest {
}
}
-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 = {
+const vm = new VM({
chrome,
- JsonSchema,
- requestJson,
- stringReverse,
- hasOwn,
- dictFieldSplit,
- dictTagSanitize,
+ XMLHttpRequest,
indexedDB: global.indexedDB,
+ IDBKeyRange: global.IDBKeyRange,
JSZip: yomichanTest.JSZip
-};
-databaseGlobals.window = databaseGlobals;
-const {Database} = yomichanTest.requireScript('ext/bg/js/database.js', ['Database'], databaseGlobals);
+});
+vm.context.window = vm.context;
+
+vm.execute([
+ 'bg/js/json-schema.js',
+ 'bg/js/dictionary.js',
+ 'mixed/js/core.js',
+ 'bg/js/request.js',
+ 'bg/js/database.js'
+]);
+const Database = vm.get('Database');
function countTermsWithExpression(terms, expression) {
@@ -213,20 +217,20 @@ async function testDatabase1() {
},
{prefixWildcardsSupported: true}
);
- assert.deepStrictEqual(errors, []);
- assert.deepStrictEqual(result, expectedSummary);
+ vm.assert.deepStrictEqual(errors, []);
+ vm.assert.deepStrictEqual(result, expectedSummary);
assert.ok(progressEvent);
// Get info summary
const info = await database.getDictionaryInfo();
- assert.deepStrictEqual(info, [expectedSummary]);
+ vm.assert.deepStrictEqual(info, [expectedSummary]);
// Get counts
const counts = await database.getDictionaryCounts(
info.map((v) => v.title),
true
);
- assert.deepStrictEqual(counts, {
+ vm.assert.deepStrictEqual(counts, {
counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}],
total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}
});
@@ -249,10 +253,10 @@ async function testDatabase1() {
async function testDatabaseEmpty1(database) {
const info = await database.getDictionaryInfo();
- assert.deepStrictEqual(info, []);
+ vm.assert.deepStrictEqual(info, []);
const counts = await database.getDictionaryCounts([], true);
- assert.deepStrictEqual(counts, {
+ vm.assert.deepStrictEqual(counts, {
counts: [],
total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0}
});
@@ -825,7 +829,7 @@ async function testFindTagForTitle1(database, title) {
for (const {inputs, expectedResults} of data) {
for (const {name} of inputs) {
const result = await database.findTagForTitle(name, title);
- assert.deepStrictEqual(result, expectedResults.value);
+ vm.assert.deepStrictEqual(result, expectedResults.value);
}
}
}
diff --git a/test/test-document.js b/test/test-document.js
new file mode 100644
index 00000000..80b9719d
--- /dev/null
+++ b/test/test-document.js
@@ -0,0 +1,240 @@
+/*
+ * 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 assert = require('assert');
+const {JSDOM} = require('jsdom');
+const {VM} = require('./yomichan-vm');
+
+
+// DOMRect class definition
+class DOMRect {
+ constructor(x, y, width, height) {
+ this._x = x;
+ this._y = y;
+ this._width = width;
+ this._height = height;
+ }
+
+ get x() { return this._x; }
+ get y() { return this._y; }
+ get width() { return this._width; }
+ get height() { return this._height; }
+ get left() { return this._x + Math.min(0, this._width); }
+ get right() { return this._x + Math.max(0, this._width); }
+ get top() { return this._y + Math.min(0, this._height); }
+ get bottom() { return this._y + Math.max(0, this._height); }
+}
+
+
+function createJSDOM(fileName) {
+ const domSource = fs.readFileSync(fileName, {encoding: 'utf8'});
+ const dom = new JSDOM(domSource);
+ const document = dom.window.document;
+ const window = dom.window;
+
+ // Define innerText setter as an alias for textContent setter
+ Object.defineProperty(window.HTMLDivElement.prototype, 'innerText', {
+ set(value) { this.textContent = value; }
+ });
+
+ // Placeholder for feature detection
+ document.caretRangeFromPoint = () => null;
+
+ return dom;
+}
+
+function querySelectorChildOrSelf(element, selector) {
+ return selector ? element.querySelector(selector) : element;
+}
+
+function getChildTextNodeOrSelf(dom, node) {
+ if (node === null) { return null; }
+ const Node = dom.window.Node;
+ const childNode = node.firstChild;
+ return (childNode !== null && childNode.nodeType === Node.TEXT_NODE ? childNode : node);
+}
+
+function getPrototypeOfOrNull(value) {
+ try {
+ return Object.getPrototypeOf(value);
+ } catch (e) {
+ return null;
+ }
+}
+
+function findImposterElement(document) {
+ // Finds the imposter element based on it's z-index style
+ return document.querySelector('div[style*="2147483646"]>*');
+}
+
+
+async function testDocument1() {
+ const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-document1.html'));
+ const window = dom.window;
+ const document = window.document;
+ const Node = window.Node;
+ const Range = window.Range;
+
+ const vm = new VM({document, window, Range, Node});
+ vm.execute([
+ 'mixed/js/dom.js',
+ 'fg/js/source.js',
+ 'fg/js/document.js'
+ ]);
+ const [TextSourceRange, TextSourceElement, docRangeFromPoint, docSentenceExtract] = vm.get([
+ 'TextSourceRange',
+ 'TextSourceElement',
+ 'docRangeFromPoint',
+ 'docSentenceExtract'
+ ]);
+
+ try {
+ await testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSentenceExtract, TextSourceRange, TextSourceElement});
+ await testTextSourceRangeSeekFunctions(dom, {TextSourceRange});
+ } finally {
+ window.close();
+ }
+}
+
+async function testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSentenceExtract, TextSourceRange, TextSourceElement}) {
+ const document = dom.window.document;
+
+ for (const testElement of document.querySelectorAll('.test[data-test-type=scan]')) {
+ // Get test parameters
+ let {
+ elementFromPointSelector,
+ caretRangeFromPointSelector,
+ startNodeSelector,
+ startOffset,
+ endNodeSelector,
+ endOffset,
+ resultType,
+ sentenceExtent,
+ sentence,
+ hasImposter
+ } = testElement.dataset;
+
+ const elementFromPointValue = querySelectorChildOrSelf(testElement, elementFromPointSelector);
+ const caretRangeFromPointValue = querySelectorChildOrSelf(testElement, caretRangeFromPointSelector);
+ const startNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, startNodeSelector));
+ const endNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, endNodeSelector));
+
+ startOffset = parseInt(startOffset, 10);
+ endOffset = parseInt(endOffset, 10);
+ sentenceExtent = parseInt(sentenceExtent, 10);
+
+ assert.notStrictEqual(elementFromPointValue, null);
+ assert.notStrictEqual(caretRangeFromPointValue, null);
+ assert.notStrictEqual(startNode, null);
+ assert.notStrictEqual(endNode, null);
+
+ // Setup functions
+ document.elementFromPoint = () => elementFromPointValue;
+
+ document.caretRangeFromPoint = (x, y) => {
+ const imposter = getChildTextNodeOrSelf(dom, findImposterElement(document));
+ assert.strictEqual(!!imposter, hasImposter === 'true');
+
+ const range = document.createRange();
+ range.setStart(imposter ? imposter : startNode, startOffset);
+ range.setEnd(imposter ? imposter : startNode, endOffset);
+
+ // Override getClientRects to return a rect guaranteed to contain (x, y)
+ range.getClientRects = () => [new DOMRect(x - 1, y - 1, 2, 2)];
+ return range;
+ };
+
+ // Test docRangeFromPoint
+ const source = docRangeFromPoint(0, 0, false);
+ switch (resultType) {
+ case 'TextSourceRange':
+ assert.strictEqual(getPrototypeOfOrNull(source), TextSourceRange.prototype);
+ break;
+ case 'TextSourceElement':
+ assert.strictEqual(getPrototypeOfOrNull(source), TextSourceElement.prototype);
+ break;
+ case 'null':
+ assert.strictEqual(source, null);
+ break;
+ default:
+ assert.ok(false);
+ break;
+ }
+ if (source === null) { continue; }
+
+ // Test docSentenceExtract
+ const sentenceActual = docSentenceExtract(source, sentenceExtent).text;
+ assert.strictEqual(sentenceActual, sentence);
+
+ // Clean
+ source.cleanup();
+ }
+}
+
+async function testTextSourceRangeSeekFunctions(dom, {TextSourceRange}) {
+ const document = dom.window.document;
+
+ for (const testElement of document.querySelectorAll('.test[data-test-type=text-source-range-seek]')) {
+ // Get test parameters
+ let {
+ seekNodeSelector,
+ seekNodeIsText,
+ seekOffset,
+ seekLength,
+ seekDirection,
+ expectedResultNodeSelector,
+ expectedResultNodeIsText,
+ expectedResultOffset,
+ expectedResultContent
+ } = testElement.dataset;
+
+ seekOffset = parseInt(seekOffset, 10);
+ seekLength = parseInt(seekLength, 10);
+ expectedResultOffset = parseInt(expectedResultOffset, 10);
+
+ let seekNode = testElement.querySelector(seekNodeSelector);
+ if (seekNodeIsText === 'true') {
+ seekNode = seekNode.firstChild;
+ }
+
+ let expectedResultNode = testElement.querySelector(expectedResultNodeSelector);
+ if (expectedResultNodeIsText === 'true') {
+ expectedResultNode = expectedResultNode.firstChild;
+ }
+
+ const {node, offset, content} = (
+ seekDirection === 'forward' ?
+ TextSourceRange.seekForward(seekNode, seekOffset, seekLength) :
+ TextSourceRange.seekBackward(seekNode, seekOffset, seekLength)
+ );
+
+ assert.strictEqual(node, expectedResultNode);
+ assert.strictEqual(offset, expectedResultOffset);
+ assert.strictEqual(content, expectedResultContent);
+ }
+}
+
+
+async function main() {
+ await testDocument1();
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/test-schema.js b/test/test-schema.js
index f4612f86..5f9915fd 100644
--- a/test/test-schema.js
+++ b/test/test-schema.js
@@ -17,9 +17,11 @@
*/
const assert = require('assert');
-const yomichanTest = require('./yomichan-test');
+const {VM} = require('./yomichan-vm');
-const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+const vm = new VM();
+vm.execute('bg/js/json-schema.js');
+const JsonSchema = vm.get('JsonSchema');
function testValidate1() {
@@ -138,7 +140,7 @@ function testGetValidValueOrDefault1() {
for (const [value, expected] of testData) {
const actual = JsonSchema.getValidValueOrDefault(schema, value);
- assert.deepStrictEqual(actual, expected);
+ vm.assert.deepStrictEqual(actual, expected);
}
}
@@ -177,7 +179,7 @@ function testGetValidValueOrDefault2() {
for (const [value, expected] of testData) {
const actual = JsonSchema.getValidValueOrDefault(schema, value);
- assert.deepStrictEqual(actual, expected);
+ vm.assert.deepStrictEqual(actual, expected);
}
}
@@ -235,7 +237,7 @@ function testGetValidValueOrDefault3() {
for (const [value, expected] of testData) {
const actual = JsonSchema.getValidValueOrDefault(schema, value);
- assert.deepStrictEqual(actual, expected);
+ vm.assert.deepStrictEqual(actual, expected);
}
}
diff --git a/test/yomichan-test.js b/test/yomichan-test.js
index 78bfb9c6..5fa7730b 100644
--- a/test/yomichan-test.js
+++ b/test/yomichan-test.js
@@ -22,18 +22,6 @@ 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) {
@@ -62,9 +50,29 @@ function createTestDictionaryArchive(dictionary, dictionaryName) {
return archive;
}
+function getAllFiles(baseDirectory, predicate=null) {
+ const results = [];
+ const directories = [path.resolve(baseDirectory)];
+ while (directories.length > 0) {
+ const directory = directories.shift();
+ for (const fileName of fs.readdirSync(directory)) {
+ const fullFileName = path.resolve(directory, fileName);
+ const stats = fs.statSync(fullFileName);
+ if (stats.isFile()) {
+ if (typeof predicate !== 'function' || predicate(fullFileName, directory, baseDirectory)) {
+ results.push(fullFileName);
+ }
+ } else if (stats.isDirectory()) {
+ directories.push(fullFileName);
+ }
+ }
+ }
+ return results;
+}
+
module.exports = {
- requireScript,
createTestDictionaryArchive,
+ getAllFiles,
get JSZip() { return getJSZip(); }
};
diff --git a/test/yomichan-vm.js b/test/yomichan-vm.js
new file mode 100644
index 00000000..ff478844
--- /dev/null
+++ b/test/yomichan-vm.js
@@ -0,0 +1,174 @@
+/*
+ * 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 vm = require('vm');
+const path = require('path');
+const assert = require('assert');
+
+
+function getContextEnvironmentRecords(context, names) {
+ // Enables export of values from the declarative environment record
+ if (!Array.isArray(names) || names.length === 0) {
+ return [];
+ }
+
+ let scriptSource = '(() => {\n "use strict";\n const results = [];';
+ for (const name of names) {
+ scriptSource += `\n try { results.push(${name}); } catch (e) { results.push(void 0); }`;
+ }
+ scriptSource += '\n return results;\n})();';
+
+ const script = new vm.Script(scriptSource, {filename: 'getContextEnvironmentRecords'});
+
+ const contextHasNames = Object.prototype.hasOwnProperty.call(context, 'names');
+ const contextNames = context.names;
+ context.names = names;
+
+ const results = script.runInContext(context, {});
+
+ if (contextHasNames) {
+ context.names = contextNames;
+ } else {
+ delete context.names;
+ }
+
+ return Array.from(results);
+}
+
+function isDeepStrictEqual(val1, val2) {
+ if (val1 === val2) { return true; }
+
+ if (Array.isArray(val1)) {
+ if (Array.isArray(val2)) {
+ return isArrayDeepStrictEqual(val1, val2);
+ }
+ } else if (typeof val1 === 'object' && val1 !== null) {
+ if (typeof val2 === 'object' && val2 !== null) {
+ return isObjectDeepStrictEqual(val1, val2);
+ }
+ }
+
+ return false;
+}
+
+function isArrayDeepStrictEqual(val1, val2) {
+ const ii = val1.length;
+ if (ii !== val2.length) { return false; }
+
+ for (let i = 0; i < ii; ++i) {
+ if (!isDeepStrictEqual(val1[i], val2[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function isObjectDeepStrictEqual(val1, val2) {
+ const keys1 = Object.keys(val1);
+ const keys2 = Object.keys(val2);
+
+ if (keys1.length !== keys2.length) { return false; }
+
+ const keySet = new Set(keys1);
+ for (const key of keys2) {
+ if (!keySet.delete(key)) { return false; }
+ }
+
+ for (const key of keys1) {
+ if (!isDeepStrictEqual(val1[key], val2[key])) {
+ return false;
+ }
+ }
+
+ const tag1 = Object.prototype.toString.call(val1);
+ const tag2 = Object.prototype.toString.call(val2);
+ if (tag1 !== tag2) { return false; }
+
+ return true;
+}
+
+function deepStrictEqual(actual, expected) {
+ try {
+ // This will fail on prototype === comparison on cross context objects
+ assert.deepStrictEqual(actual, expected);
+ } catch (e) {
+ if (!isDeepStrictEqual(actual, expected)) {
+ throw e;
+ }
+ }
+}
+
+
+class VM {
+ constructor(context={}) {
+ this._context = vm.createContext(context);
+ this._assert = {
+ deepStrictEqual
+ };
+ }
+
+ get context() {
+ return this._context;
+ }
+
+ get assert() {
+ return this._assert;
+ }
+
+ get(names) {
+ if (typeof names === 'string') {
+ return getContextEnvironmentRecords(this._context, [names])[0];
+ } else if (Array.isArray(names)) {
+ return getContextEnvironmentRecords(this._context, names);
+ } else {
+ throw new Error('Invalid argument');
+ }
+ }
+
+ set(values) {
+ if (typeof values === 'object' && values !== null) {
+ Object.assign(this._context, values);
+ } else {
+ throw new Error('Invalid argument');
+ }
+ }
+
+ execute(fileNames) {
+ const single = !Array.isArray(fileNames);
+ if (single) {
+ fileNames = [fileNames];
+ }
+
+ const results = [];
+ for (const fileName of fileNames) {
+ const absoluteFileName = path.resolve(__dirname, '..', 'ext', fileName);
+ const source = fs.readFileSync(absoluteFileName, {encoding: 'utf8'});
+ const script = new vm.Script(source, {filename: absoluteFileName});
+ results.push(script.runInContext(this._context, {}));
+ }
+
+ return single ? results[0] : results;
+ }
+}
+
+
+module.exports = {
+ VM
+};