diff options
Diffstat (limited to 'ext/bg')
36 files changed, 0 insertions, 10859 deletions
diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js deleted file mode 100644 index e1399f66..00000000 --- a/ext/bg/js/anki-note-builder.js +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * TemplateRendererProxy - */ - -class AnkiNoteBuilder { - constructor(enabled) { - this._markerPattern = /\{([\w-]+)\}/g; - this._templateRenderer = enabled ? new TemplateRendererProxy() : null; - } - - async createNote({ - definition, - mode, - context, - templates, - deckName, - modelName, - fields, - tags=[], - injectedMedia=null, - checkForDuplicates=true, - duplicateScope='collection', - resultOutputMode='split', - glossaryLayoutMode='default', - compactTags=false, - errors=null - }) { - let duplicateScopeDeckName = null; - let duplicateScopeCheckChildren = false; - if (duplicateScope === 'deck-root') { - duplicateScope = 'deck'; - duplicateScopeDeckName = this.getRootDeckName(deckName); - duplicateScopeCheckChildren = true; - } - - const data = { - definition, - mode, - context, - resultOutputMode, - glossaryLayoutMode, - compactTags, - injectedMedia - }; - const formattedFieldValuePromises = []; - for (const [, fieldValue] of fields) { - const formattedFieldValuePromise = this._formatField(fieldValue, data, templates, errors); - formattedFieldValuePromises.push(formattedFieldValuePromise); - } - - const formattedFieldValues = await Promise.all(formattedFieldValuePromises); - const noteFields = {}; - for (let i = 0, ii = fields.length; i < ii; ++i) { - const fieldName = fields[i][0]; - const formattedFieldValue = formattedFieldValues[i]; - noteFields[fieldName] = formattedFieldValue; - } - - return { - fields: noteFields, - tags, - deckName, - modelName, - options: { - allowDuplicate: !checkForDuplicates, - duplicateScope, - duplicateScopeOptions: { - deckName: duplicateScopeDeckName, - checkChildren: duplicateScopeCheckChildren - } - } - }; - } - - containsMarker(fields, marker) { - marker = `{${marker}}`; - for (const [, fieldValue] of fields) { - if (fieldValue.includes(marker)) { - return true; - } - } - return false; - } - - containsAnyMarker(field) { - const result = this._markerPattern.test(field); - this._markerPattern.lastIndex = 0; - return result; - } - - getRootDeckName(deckName) { - const index = deckName.indexOf('::'); - return index >= 0 ? deckName.substring(0, index) : deckName; - } - - // Private - - async _formatField(field, data, templates, errors=null) { - return await this._stringReplaceAsync(field, this._markerPattern, async (g0, marker) => { - try { - return await this._renderTemplate(templates, data, marker); - } catch (e) { - if (errors) { - const error = new Error(`Template render error for {${marker}}`); - error.data = {error: e}; - errors.push(error); - } - return `{${marker}-render-error}`; - } - }); - } - - async _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 str; - } - parts.push(str.substring(index)); - return (await Promise.all(parts)).join(''); - } - - async _renderTemplate(template, data, marker) { - return await this._templateRenderer.render(template, {data, marker}, 'ankiNote'); - } -} diff --git a/ext/bg/js/anki-note-data.js b/ext/bg/js/anki-note-data.js deleted file mode 100644 index a7d0f9f6..00000000 --- a/ext/bg/js/anki-note-data.js +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (C) 2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * DictionaryDataUtil - */ - -/** - * This class represents the data that is exposed to the Anki template renderer. - * The public properties and data should be backwards compatible. - */ -class AnkiNoteData { - constructor({ - definition, - resultOutputMode, - mode, - glossaryLayoutMode, - compactTags, - context, - injectedMedia=null - }, marker) { - this._definition = definition; - this._resultOutputMode = resultOutputMode; - this._mode = mode; - this._glossaryLayoutMode = glossaryLayoutMode; - this._compactTags = compactTags; - this._context = context; - this._marker = marker; - this._injectedMedia = injectedMedia; - this._pitches = null; - this._pitchCount = null; - this._uniqueExpressions = null; - this._uniqueReadings = null; - this._publicContext = null; - this._cloze = null; - - this._prepareDefinition(definition, injectedMedia, context); - } - - get marker() { - return this._marker; - } - - set marker(value) { - this._marker = value; - } - - get definition() { - return this._definition; - } - - get uniqueExpressions() { - if (this._uniqueExpressions === null) { - this._uniqueExpressions = this._getUniqueExpressions(); - } - return this._uniqueExpressions; - } - - get uniqueReadings() { - if (this._uniqueReadings === null) { - this._uniqueReadings = this._getUniqueReadings(); - } - return this._uniqueReadings; - } - - get pitches() { - if (this._pitches === null) { - this._pitches = DictionaryDataUtil.getPitchAccentInfos(this._definition); - } - return this._pitches; - } - - get pitchCount() { - if (this._pitchCount === null) { - this._pitchCount = this.pitches.reduce((i, v) => i + v.pitches.length, 0); - } - return this._pitchCount; - } - - get group() { - return this._resultOutputMode === 'group'; - } - - get merge() { - return this._resultOutputMode === 'merge'; - } - - get modeTermKanji() { - return this._mode === 'term-kanji'; - } - - get modeTermKana() { - return this._mode === 'term-kana'; - } - - get modeKanji() { - return this._mode === 'kanji'; - } - - get compactGlossaries() { - return this._glossaryLayoutMode === 'compact'; - } - - get glossaryLayoutMode() { - return this._glossaryLayoutMode; - } - - get compactTags() { - return this._compactTags; - } - - get context() { - if (this._publicContext === null) { - this._publicContext = this._getPublicContext(); - } - return this._publicContext; - } - - createPublic() { - const self = this; - return { - get marker() { return self.marker; }, - set marker(value) { self.marker = value; }, - get definition() { return self.definition; }, - get glossaryLayoutMode() { return self.glossaryLayoutMode; }, - get compactTags() { return self.compactTags; }, - get group() { return self.group; }, - get merge() { return self.merge; }, - get modeTermKanji() { return self.modeTermKanji; }, - get modeTermKana() { return self.modeTermKana; }, - get modeKanji() { return self.modeKanji; }, - get compactGlossaries() { return self.compactGlossaries; }, - get uniqueExpressions() { return self.uniqueExpressions; }, - get uniqueReadings() { return self.uniqueReadings; }, - get pitches() { return self.pitches; }, - get pitchCount() { return self.pitchCount; }, - get context() { return self.context; } - }; - } - - // Private - - _asObject(value) { - return (typeof value === 'object' && value !== null ? value : {}); - } - - _getUniqueExpressions() { - const results = new Set(); - const definition = this._definition; - if (definition.type !== 'kanji') { - for (const {expression} of definition.expressions) { - results.add(expression); - } - } - return [...results]; - } - - _getUniqueReadings() { - const results = new Set(); - const definition = this._definition; - if (definition.type !== 'kanji') { - for (const {reading} of definition.expressions) { - results.add(reading); - } - } - return [...results]; - } - - _getPublicContext() { - let {documentTitle} = this._asObject(this._context); - if (typeof documentTitle !== 'string') { documentTitle = ''; } - - return { - document: { - title: documentTitle - } - }; - } - - _getCloze() { - const {sentence} = this._asObject(this._context); - let {text, offset} = this._asObject(sentence); - if (typeof text !== 'string') { text = ''; } - if (typeof offset !== 'number') { offset = 0; } - - const definition = this._definition; - const source = definition.type === 'kanji' ? definition.character : definition.rawSource; - - return { - sentence: text, - prefix: text.substring(0, offset), - body: text.substring(offset, offset + source.length), - suffix: text.substring(offset + source.length) - }; - } - - _getClozeCached() { - if (this._cloze === null) { - this._cloze = this._getCloze(); - } - return this._cloze; - } - - _prepareDefinition(definition, injectedMedia, context) { - const { - screenshotFileName=null, - clipboardImageFileName=null, - clipboardText=null, - audioFileName=null - } = this._asObject(injectedMedia); - - let {url} = this._asObject(context); - if (typeof url !== 'string') { url = ''; } - - definition.screenshotFileName = screenshotFileName; - definition.clipboardImageFileName = clipboardImageFileName; - definition.clipboardText = clipboardText; - definition.audioFileName = audioFileName; - definition.url = url; - Object.defineProperty(definition, 'cloze', { - configurable: true, - enumerable: true, - get: this._getClozeCached.bind(this) - }); - } -} diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js deleted file mode 100644 index 251e0e0c..00000000 --- a/ext/bg/js/anki.js +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (C) 2016-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class AnkiConnect { - constructor() { - this._enabled = false; - this._server = null; - this._localVersion = 2; - this._remoteVersion = 0; - this._versionCheckPromise = null; - } - - get server() { - return this._server; - } - - set server(value) { - this._server = value; - } - - get enabled() { - return this._enabled; - } - - set enabled(value) { - this._enabled = value; - } - - async isConnected() { - try { - await this._invoke('version'); - return true; - } catch (e) { - return false; - } - } - - async getVersion() { - if (!this._enabled) { return null; } - await this._checkVersion(); - return await this._invoke('version', {}); - } - - async addNote(note) { - if (!this._enabled) { return null; } - await this._checkVersion(); - return await this._invoke('addNote', {note}); - } - - async canAddNotes(notes) { - if (!this._enabled) { return []; } - await this._checkVersion(); - return await this._invoke('canAddNotes', {notes}); - } - - async getDeckNames() { - if (!this._enabled) { return []; } - await this._checkVersion(); - return await this._invoke('deckNames'); - } - - async getModelNames() { - if (!this._enabled) { return []; } - await this._checkVersion(); - return await this._invoke('modelNames'); - } - - async getModelFieldNames(modelName) { - if (!this._enabled) { return []; } - await this._checkVersion(); - return await this._invoke('modelFieldNames', {modelName}); - } - - async guiBrowse(query) { - if (!this._enabled) { return []; } - await this._checkVersion(); - return await this._invoke('guiBrowse', {query}); - } - - async guiBrowseNote(noteId) { - return await this.guiBrowse(`nid:${noteId}`); - } - - async storeMediaFile(fileName, dataBase64) { - if (!this._enabled) { - throw new Error('AnkiConnect not enabled'); - } - await this._checkVersion(); - return await this._invoke('storeMediaFile', {filename: fileName, data: dataBase64}); - } - - async findNoteIds(notes) { - if (!this._enabled) { return []; } - await this._checkVersion(); - const actions = notes.map((note) => { - let query = ''; - switch (this._getDuplicateScopeFromNote(note)) { - case 'deck': - query = `"deck:${this._escapeQuery(note.deckName)}" `; - break; - case 'deck-root': - query = `"deck:${this._escapeQuery(this.getRootDeckName(note.deckName))}" `; - break; - } - query += this._fieldsToQuery(note.fields); - return {action: 'findNotes', params: {query}}; - }); - return await this._invoke('multi', {actions}); - } - - async suspendCards(cardIds) { - if (!this._enabled) { return false; } - await this._checkVersion(); - return await this._invoke('suspend', {cards: cardIds}); - } - - async findCards(query) { - if (!this._enabled) { return []; } - await this._checkVersion(); - return await this._invoke('findCards', {query}); - } - - async findCardsForNote(noteId) { - return await this.findCards(`nid:${noteId}`); - } - - getRootDeckName(deckName) { - const index = deckName.indexOf('::'); - return index >= 0 ? deckName.substring(0, index) : deckName; - } - - // Private - - async _checkVersion() { - if (this._remoteVersion < this._localVersion) { - if (this._versionCheckPromise === null) { - const promise = this._invoke('version'); - promise - .catch(() => {}) - .finally(() => { this._versionCheckPromise = null; }); - this._versionCheckPromise = promise; - } - this._remoteVersion = await this._versionCheckPromise; - if (this._remoteVersion < this._localVersion) { - throw new Error('Extension and plugin versions incompatible'); - } - } - } - - async _invoke(action, params) { - let response; - try { - response = await fetch(this._server, { - method: 'POST', - mode: 'cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer', - body: JSON.stringify({action, params, version: this._localVersion}) - }); - } catch (e) { - const error = new Error('Anki connection failure'); - error.data = {action, params}; - throw error; - } - - if (!response.ok) { - const error = new Error(`Anki connection error: ${response.status}`); - error.data = {action, params, status: response.status}; - throw error; - } - - let responseText = null; - let result; - try { - responseText = await response.text(); - result = JSON.parse(responseText); - } catch (e) { - const error = new Error('Invalid Anki response'); - error.data = {action, params, status: response.status, responseText}; - throw error; - } - - if (isObject(result)) { - const apiError = result.error; - if (typeof apiError !== 'undefined') { - const error = new Error(`Anki error: ${apiError}`); - error.data = {action, params, status: response.status, apiError}; - throw error; - } - } - - return result; - } - - _escapeQuery(text) { - return text.replace(/"/g, ''); - } - - _fieldsToQuery(fields) { - const fieldNames = Object.keys(fields); - if (fieldNames.length === 0) { - return ''; - } - - const key = fieldNames[0]; - return `"${key.toLowerCase()}:${this._escapeQuery(fields[key])}"`; - } - - _getDuplicateScopeFromNote(note) { - const {options} = note; - if (typeof options === 'object' && options !== null) { - const {duplicateScope} = options; - if (typeof duplicateScope !== 'undefined') { - return duplicateScope; - } - } - return null; - } -} diff --git a/ext/bg/js/audio-downloader.js b/ext/bg/js/audio-downloader.js deleted file mode 100644 index 4e77419b..00000000 --- a/ext/bg/js/audio-downloader.js +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright (C) 2017-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * JsonSchemaValidator - * NativeSimpleDOMParser - * SimpleDOMParser - */ - -class AudioDownloader { - constructor({japaneseUtil, requestBuilder}) { - this._japaneseUtil = japaneseUtil; - this._requestBuilder = requestBuilder; - this._customAudioListSchema = null; - this._schemaValidator = null; - this._getInfoHandlers = new Map([ - ['jpod101', this._getInfoJpod101.bind(this)], - ['jpod101-alternate', this._getInfoJpod101Alternate.bind(this)], - ['jisho', this._getInfoJisho.bind(this)], - ['text-to-speech', this._getInfoTextToSpeech.bind(this)], - ['text-to-speech-reading', this._getInfoTextToSpeechReading.bind(this)], - ['custom', this._getInfoCustom.bind(this)] - ]); - } - - async getExpressionAudioInfoList(source, expression, reading, details) { - const handler = this._getInfoHandlers.get(source); - if (typeof handler === 'function') { - try { - return await handler(expression, reading, details); - } catch (e) { - // NOP - } - } - return []; - } - - async downloadExpressionAudio(sources, expression, reading, details) { - for (const source of sources) { - const infoList = await this.getExpressionAudioInfoList(source, expression, reading, details); - for (const info of infoList) { - switch (info.type) { - case 'url': - try { - return await this._downloadAudioFromUrl(info.url, source); - } catch (e) { - // NOP - } - break; - } - } - } - - throw new Error('Could not download audio'); - } - - // Private - - _normalizeUrl(url, base) { - return new URL(url, base).href; - } - - async _getInfoJpod101(expression, reading) { - let kana = reading; - let kanji = expression; - - if (!kana && this._japaneseUtil.isStringEntirelyKana(kanji)) { - kana = kanji; - kanji = null; - } - - const params = []; - if (kanji) { - params.push(`kanji=${encodeURIComponent(kanji)}`); - } - if (kana) { - params.push(`kana=${encodeURIComponent(kana)}`); - } - - const url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`; - return [{type: 'url', url}]; - } - - async _getInfoJpod101Alternate(expression, reading) { - const fetchUrl = 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'; - const data = `post=dictionary_reference&match_type=exact&search_query=${encodeURIComponent(expression)}&vulgar=true`; - const response = await this._requestBuilder.fetchAnonymous(fetchUrl, { - method: 'POST', - mode: 'cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: data - }); - const responseText = await response.text(); - - const dom = this._createSimpleDOMParser(responseText); - for (const row of dom.getElementsByClassName('dc-result-row')) { - try { - const audio = dom.getElementByTagName('audio', row); - if (audio === null) { continue; } - - const source = dom.getElementByTagName('source', audio); - if (source === null) { continue; } - - let url = dom.getAttribute(source, 'src'); - if (url === null) { continue; } - - const htmlReadings = dom.getElementsByClassName('dc-vocab_kana'); - if (htmlReadings.length === 0) { continue; } - - const htmlReading = dom.getTextContent(htmlReadings[0]); - if (htmlReading && (!reading || reading === htmlReading)) { - url = this._normalizeUrl(url, response.url); - return [{type: 'url', url}]; - } - } catch (e) { - // NOP - } - } - - throw new Error('Failed to find audio URL'); - } - - async _getInfoJisho(expression, reading) { - const fetchUrl = `https://jisho.org/search/${expression}`; - const response = await this._requestBuilder.fetchAnonymous(fetchUrl, { - method: 'GET', - mode: 'cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }); - const responseText = await response.text(); - - const dom = this._createSimpleDOMParser(responseText); - try { - const audio = dom.getElementById(`audio_${expression}:${reading}`); - if (audio !== null) { - const source = dom.getElementByTagName('source', audio); - if (source !== null) { - let url = dom.getAttribute(source, 'src'); - if (url !== null) { - url = this._normalizeUrl(url, response.url); - return [{type: 'url', url}]; - } - } - } - } catch (e) { - // NOP - } - - throw new Error('Failed to find audio URL'); - } - - async _getInfoTextToSpeech(expression, reading, {textToSpeechVoice}) { - if (!textToSpeechVoice) { - throw new Error('No voice'); - } - return [{type: 'tts', text: expression, voice: textToSpeechVoice}]; - } - - async _getInfoTextToSpeechReading(expression, reading, {textToSpeechVoice}) { - if (!textToSpeechVoice) { - throw new Error('No voice'); - } - return [{type: 'tts', text: reading || expression, voice: textToSpeechVoice}]; - } - - async _getInfoCustom(expression, reading, {customSourceUrl, customSourceType}) { - if (typeof customSourceUrl !== 'string') { - throw new Error('No custom URL defined'); - } - const data = {expression, reading}; - const url = customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (Object.prototype.hasOwnProperty.call(data, m1) ? `${data[m1]}` : m0)); - - switch (customSourceType) { - case 'json': - return await this._getInfoCustomJson(url); - default: - return [{type: 'url', url}]; - } - } - - async _getInfoCustomJson(url) { - const response = await this._requestBuilder.fetchAnonymous(url, { - method: 'GET', - mode: 'cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }); - - if (!response.ok) { - throw new Error(`Invalid response: ${response.status}`); - } - - const responseJson = await response.json(); - - const schema = await this._getCustomAudioListSchema(); - if (this._schemaValidator === null) { - this._schemaValidator = new JsonSchemaValidator(); - } - this._schemaValidator.validate(responseJson, schema); - - const results = []; - for (const {url: url2, name} of responseJson.audioSources) { - const info = {type: 'url', url: url2}; - if (typeof name === 'string') { info.name = name; } - results.push(info); - } - return results; - } - - async _downloadAudioFromUrl(url, source) { - const response = await this._requestBuilder.fetchAnonymous(url, { - method: 'GET', - mode: 'cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }); - - if (!response.ok) { - throw new Error(`Invalid response: ${response.status}`); - } - - const arrayBuffer = await response.arrayBuffer(); - - if (!await this._isAudioBinaryValid(arrayBuffer, source)) { - throw new Error('Could not retrieve audio'); - } - - const data = this._arrayBufferToBase64(arrayBuffer); - const contentType = response.headers.get('Content-Type'); - return {data, contentType}; - } - - async _isAudioBinaryValid(arrayBuffer, source) { - switch (source) { - case 'jpod101': - { - const digest = await this._arrayBufferDigest(arrayBuffer); - switch (digest) { - case 'ae6398b5a27bc8c0a771df6c907ade794be15518174773c58c7c7ddd17098906': // Invalid audio - return false; - default: - return true; - } - } - default: - return true; - } - } - - async _arrayBufferDigest(arrayBuffer) { - const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array(arrayBuffer))); - let digest = ''; - for (const byte of hash) { - digest += byte.toString(16).padStart(2, '0'); - } - return digest; - } - - _arrayBufferToBase64(arrayBuffer) { - return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); - } - - _createSimpleDOMParser(content) { - if (typeof NativeSimpleDOMParser !== 'undefined' && NativeSimpleDOMParser.isSupported()) { - return new NativeSimpleDOMParser(content); - } else if (typeof SimpleDOMParser !== 'undefined' && SimpleDOMParser.isSupported()) { - return new SimpleDOMParser(content); - } else { - throw new Error('DOM parsing not supported'); - } - } - - async _getCustomAudioListSchema() { - let schema = this._customAudioListSchema; - if (schema === null) { - const url = chrome.runtime.getURL('/data/schemas/custom-audio-list-schema.json'); - const response = await fetch(url, { - method: 'GET', - mode: 'no-cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }); - schema = await response.json(); - this._customAudioListSchema = schema; - } - return schema; - } -} diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js deleted file mode 100644 index 3bb23310..00000000 --- a/ext/bg/js/backend.js +++ /dev/null @@ -1,2053 +0,0 @@ -/* - * Copyright (C) 2016-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * AnkiConnect - * AudioDownloader - * ClipboardMonitor - * ClipboardReader - * DictionaryDatabase - * Environment - * JapaneseUtil - * JsonSchemaValidator - * Mecab - * MediaUtility - * ObjectPropertyAccessor - * OptionsUtil - * PermissionsUtil - * ProfileConditions - * RequestBuilder - * Translator - * wanakana - */ - -class Backend { - constructor() { - this._japaneseUtil = new JapaneseUtil(wanakana); - this._environment = new Environment(); - this._dictionaryDatabase = new DictionaryDatabase(); - this._translator = new Translator({ - japaneseUtil: this._japaneseUtil, - database: this._dictionaryDatabase - }); - this._anki = new AnkiConnect(); - this._mecab = new Mecab(); - this._mediaUtility = new MediaUtility(); - this._clipboardReader = new ClipboardReader({ - // eslint-disable-next-line no-undef - document: (typeof document === 'object' && document !== null ? document : null), - pasteTargetSelector: '#clipboard-paste-target', - imagePasteTargetSelector: '#clipboard-image-paste-target', - mediaUtility: this._mediaUtility - }); - this._clipboardMonitor = new ClipboardMonitor({ - japaneseUtil: this._japaneseUtil, - clipboardReader: this._clipboardReader - }); - this._options = null; - this._profileConditionsSchemaValidator = new JsonSchemaValidator(); - this._profileConditionsSchemaCache = []; - this._profileConditionsUtil = new ProfileConditions(); - this._defaultAnkiFieldTemplates = null; - this._requestBuilder = new RequestBuilder(); - this._audioDownloader = new AudioDownloader({ - japaneseUtil: this._japaneseUtil, - requestBuilder: this._requestBuilder - }); - this._optionsUtil = new OptionsUtil(); - - this._searchPopupTabId = null; - this._searchPopupTabCreatePromise = null; - - this._isPrepared = false; - this._prepareError = false; - this._preparePromise = null; - const {promise, resolve, reject} = deferPromise(); - this._prepareCompletePromise = promise; - this._prepareCompleteResolve = resolve; - this._prepareCompleteReject = reject; - - this._defaultBrowserActionTitle = null; - this._badgePrepareDelayTimer = null; - this._logErrorLevel = null; - this._permissions = null; - this._permissionsUtil = new PermissionsUtil(); - - this._messageHandlers = new Map([ - ['requestBackendReadySignal', {async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this)}], - ['optionsGet', {async: false, contentScript: true, handler: this._onApiOptionsGet.bind(this)}], - ['optionsGetFull', {async: false, contentScript: true, handler: this._onApiOptionsGetFull.bind(this)}], - ['kanjiFind', {async: true, contentScript: true, handler: this._onApiKanjiFind.bind(this)}], - ['termsFind', {async: true, contentScript: true, handler: this._onApiTermsFind.bind(this)}], - ['textParse', {async: true, contentScript: true, handler: this._onApiTextParse.bind(this)}], - ['getAnkiConnectVersion', {async: true, contentScript: true, handler: this._onApGetAnkiConnectVersion.bind(this)}], - ['isAnkiConnected', {async: true, contentScript: true, handler: this._onApiIsAnkiConnected.bind(this)}], - ['addAnkiNote', {async: true, contentScript: true, handler: this._onApiAddAnkiNote.bind(this)}], - ['getAnkiNoteInfo', {async: true, contentScript: true, handler: this._onApiGetAnkiNoteInfo.bind(this)}], - ['injectAnkiNoteMedia', {async: true, contentScript: true, handler: this._onApiInjectAnkiNoteMedia.bind(this)}], - ['noteView', {async: true, contentScript: true, handler: this._onApiNoteView.bind(this)}], - ['suspendAnkiCardsForNote', {async: true, contentScript: true, handler: this._onApiSuspendAnkiCardsForNote.bind(this)}], - ['commandExec', {async: false, contentScript: true, handler: this._onApiCommandExec.bind(this)}], - ['getExpressionAudioInfoList', {async: true, contentScript: true, handler: this._onApiGetExpressionAudioInfoList.bind(this)}], - ['sendMessageToFrame', {async: false, contentScript: true, handler: this._onApiSendMessageToFrame.bind(this)}], - ['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}], - ['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}], - ['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}], - ['getStylesheetContent', {async: true, contentScript: true, handler: this._onApiGetStylesheetContent.bind(this)}], - ['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}], - ['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}], - ['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}], - ['getZoom', {async: true, contentScript: true, handler: this._onApiGetZoom.bind(this)}], - ['getDefaultAnkiFieldTemplates', {async: false, contentScript: true, handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this)}], - ['getDictionaryInfo', {async: true, contentScript: false, handler: this._onApiGetDictionaryInfo.bind(this)}], - ['getDictionaryCounts', {async: true, contentScript: false, handler: this._onApiGetDictionaryCounts.bind(this)}], - ['purgeDatabase', {async: true, contentScript: false, handler: this._onApiPurgeDatabase.bind(this)}], - ['getMedia', {async: true, contentScript: true, handler: this._onApiGetMedia.bind(this)}], - ['log', {async: false, contentScript: true, handler: this._onApiLog.bind(this)}], - ['logIndicatorClear', {async: false, contentScript: true, handler: this._onApiLogIndicatorClear.bind(this)}], - ['createActionPort', {async: false, contentScript: true, handler: this._onApiCreateActionPort.bind(this)}], - ['modifySettings', {async: true, contentScript: true, handler: this._onApiModifySettings.bind(this)}], - ['getSettings', {async: false, contentScript: true, handler: this._onApiGetSettings.bind(this)}], - ['setAllSettings', {async: true, contentScript: false, handler: this._onApiSetAllSettings.bind(this)}], - ['getOrCreateSearchPopup', {async: true, contentScript: true, handler: this._onApiGetOrCreateSearchPopup.bind(this)}], - ['isTabSearchPopup', {async: true, contentScript: true, handler: this._onApiIsTabSearchPopup.bind(this)}], - ['triggerDatabaseUpdated', {async: false, contentScript: true, handler: this._onApiTriggerDatabaseUpdated.bind(this)}], - ['testMecab', {async: true, contentScript: true, handler: this._onApiTestMecab.bind(this)}] - ]); - this._messageHandlersWithProgress = new Map([ - ]); - - this._commandHandlers = new Map([ - ['toggleTextScanning', this._onCommandToggleTextScanning.bind(this)], - ['openInfoPage', this._onCommandOpenInfoPage.bind(this)], - ['openSettingsPage', this._onCommandOpenSettingsPage.bind(this)], - ['openSearchPage', this._onCommandOpenSearchPage.bind(this)], - ['openPopupWindow', this._onCommandOpenPopupWindow.bind(this)] - ]); - } - - prepare() { - if (this._preparePromise === null) { - const promise = this._prepareInternal(); - promise.then( - (value) => { - this._isPrepared = true; - this._prepareCompleteResolve(value); - }, - (error) => { - this._prepareError = true; - this._prepareCompleteReject(error); - } - ); - promise.finally(() => this._updateBadge()); - this._preparePromise = promise; - } - return this._prepareCompletePromise; - } - - // Private - - _prepareInternalSync() { - if (isObject(chrome.commands) && isObject(chrome.commands.onCommand)) { - const onCommand = this._onWebExtensionEventWrapper(this._onCommand.bind(this)); - chrome.commands.onCommand.addListener(onCommand); - } - - if (isObject(chrome.tabs) && isObject(chrome.tabs.onZoomChange)) { - const onZoomChange = this._onWebExtensionEventWrapper(this._onZoomChange.bind(this)); - chrome.tabs.onZoomChange.addListener(onZoomChange); - } - - const onConnect = this._onWebExtensionEventWrapper(this._onConnect.bind(this)); - chrome.runtime.onConnect.addListener(onConnect); - - const onMessage = this._onMessageWrapper.bind(this); - chrome.runtime.onMessage.addListener(onMessage); - - const onPermissionsChanged = this._onWebExtensionEventWrapper(this._onPermissionsChanged.bind(this)); - chrome.permissions.onAdded.addListener(onPermissionsChanged); - chrome.permissions.onRemoved.addListener(onPermissionsChanged); - } - - async _prepareInternal() { - try { - this._prepareInternalSync(); - - this._permissions = await this._permissionsUtil.getAllPermissions(); - this._defaultBrowserActionTitle = await this._getBrowserIconTitle(); - this._badgePrepareDelayTimer = setTimeout(() => { - this._badgePrepareDelayTimer = null; - this._updateBadge(); - }, 1000); - this._updateBadge(); - - yomichan.on('log', this._onLog.bind(this)); - - await this._requestBuilder.prepare(); - await this._environment.prepare(); - this._clipboardReader.browser = this._environment.getInfo().browser; - - try { - await this._dictionaryDatabase.prepare(); - } catch (e) { - yomichan.logError(e); - } - - const deinflectionReasions = await this._fetchAsset('/data/deinflect.json', true); - this._translator.prepare(deinflectionReasions); - - await this._optionsUtil.prepare(); - this._defaultAnkiFieldTemplates = (await this._fetchAsset('/data/templates/default-anki-field-templates.handlebars')).trim(); - this._options = await this._optionsUtil.load(); - - this._applyOptions('background'); - - const options = this._getProfileOptions({current: true}); - if (options.general.showGuide) { - this._openWelcomeGuidePage(); - } - - this._clipboardMonitor.on('change', this._onClipboardTextChange.bind(this)); - - this._sendMessageAllTabsIgnoreResponse('backendReady', {}); - this._sendMessageIgnoreResponse({action: 'backendReady', params: {}}); - } catch (e) { - yomichan.logError(e); - throw e; - } finally { - if (this._badgePrepareDelayTimer !== null) { - clearTimeout(this._badgePrepareDelayTimer); - this._badgePrepareDelayTimer = null; - } - } - } - - // Event handlers - - async _onClipboardTextChange({text}) { - const {clipboard: {maximumSearchLength}} = this._getProfileOptions({current: true}); - if (text.length > maximumSearchLength) { - text = text.substring(0, maximumSearchLength); - } - try { - const {tab, created} = await this._getOrCreateSearchPopup(); - await this._focusTab(tab); - await this._updateSearchQuery(tab.id, text, !created); - } catch (e) { - // NOP - } - } - - _onLog({level}) { - const levelValue = this._getErrorLevelValue(level); - if (levelValue <= this._getErrorLevelValue(this._logErrorLevel)) { return; } - - this._logErrorLevel = level; - this._updateBadge(); - } - - // WebExtension event handlers (with prepared checks) - - _onWebExtensionEventWrapper(handler) { - return (...args) => { - if (this._isPrepared) { - handler(...args); - return; - } - - this._prepareCompletePromise.then( - () => { handler(...args); }, - () => {} // NOP - ); - }; - } - - _onMessageWrapper(message, sender, sendResponse) { - if (this._isPrepared) { - return this._onMessage(message, sender, sendResponse); - } - - this._prepareCompletePromise.then( - () => { this._onMessage(message, sender, sendResponse); }, - () => { sendResponse(); } - ); - return true; - } - - // WebExtension event handlers - - _onCommand(command) { - this._runCommand(command); - } - - _onMessage({action, params}, sender, callback) { - const messageHandler = this._messageHandlers.get(action); - if (typeof messageHandler === 'undefined') { return false; } - - if (!messageHandler.contentScript) { - try { - this._validatePrivilegedMessageSender(sender); - } catch (error) { - callback({error: serializeError(error)}); - return false; - } - } - - return yomichan.invokeMessageHandler(messageHandler, params, callback, sender); - } - - _onConnect(port) { - try { - let details; - try { - details = JSON.parse(port.name); - } catch (e) { - return; - } - if (details.name !== 'background-cross-frame-communication-port') { return; } - - const senderTabId = (port.sender && port.sender.tab ? port.sender.tab.id : null); - if (typeof senderTabId !== 'number') { - throw new Error('Port does not have an associated tab ID'); - } - const senderFrameId = port.sender.frameId; - if (typeof senderFrameId !== 'number') { - throw new Error('Port does not have an associated frame ID'); - } - let {targetTabId, targetFrameId} = details; - if (typeof targetTabId !== 'number') { - targetTabId = senderTabId; - } - - const details2 = { - name: 'cross-frame-communication-port', - sourceTabId: senderTabId, - sourceFrameId: senderFrameId - }; - let forwardPort = chrome.tabs.connect(targetTabId, {frameId: targetFrameId, name: JSON.stringify(details2)}); - - const cleanup = () => { - this._checkLastError(chrome.runtime.lastError); - if (forwardPort !== null) { - forwardPort.disconnect(); - forwardPort = null; - } - if (port !== null) { - port.disconnect(); - port = null; - } - }; - - port.onMessage.addListener((message) => { forwardPort.postMessage(message); }); - forwardPort.onMessage.addListener((message) => { port.postMessage(message); }); - port.onDisconnect.addListener(cleanup); - forwardPort.onDisconnect.addListener(cleanup); - } catch (e) { - port.disconnect(); - yomichan.logError(e); - } - } - - _onZoomChange({tabId, oldZoomFactor, newZoomFactor}) { - this._sendMessageTabIgnoreResponse(tabId, {action: 'zoomChanged', params: {oldZoomFactor, newZoomFactor}}); - } - - _onPermissionsChanged() { - this._checkPermissions(); - } - - // Message handlers - - _onApiRequestBackendReadySignal(_params, sender) { - // tab ID isn't set in background (e.g. browser_action) - const data = {action: 'backendReady', params: {}}; - if (typeof sender.tab === 'undefined') { - this._sendMessageIgnoreResponse(data); - return false; - } else { - this._sendMessageTabIgnoreResponse(sender.tab.id, data); - return true; - } - } - - _onApiOptionsGet({optionsContext}) { - return this._getProfileOptions(optionsContext); - } - - _onApiOptionsGetFull() { - return this._getOptionsFull(); - } - - async _onApiKanjiFind({text, optionsContext}) { - const options = this._getProfileOptions(optionsContext); - const {general: {maxResults}} = options; - const findKanjiOptions = this._getTranslatorFindKanjiOptions(options); - const definitions = await this._translator.findKanji(text, findKanjiOptions); - definitions.splice(maxResults); - return definitions; - } - - async _onApiTermsFind({text, details, optionsContext}) { - const options = this._getProfileOptions(optionsContext); - const {general: {resultOutputMode: mode, maxResults}} = options; - const findTermsOptions = this._getTranslatorFindTermsOptions(details, options); - const [definitions, length] = await this._translator.findTerms(mode, text, findTermsOptions); - definitions.splice(maxResults); - return {length, definitions}; - } - - async _onApiTextParse({text, optionsContext}) { - const options = this._getProfileOptions(optionsContext); - const results = []; - - if (options.parsing.enableScanningParser) { - results.push({ - source: 'scanning-parser', - id: 'scan', - content: await this._textParseScanning(text, options) - }); - } - - if (options.parsing.enableMecabParser) { - const mecabResults = await this._textParseMecab(text, options); - for (const [mecabDictName, mecabDictResults] of mecabResults) { - results.push({ - source: 'mecab', - dictionary: mecabDictName, - id: `mecab-${mecabDictName}`, - content: mecabDictResults - }); - } - } - - return results; - } - - async _onApGetAnkiConnectVersion() { - return await this._anki.getVersion(); - } - - async _onApiIsAnkiConnected() { - return await this._anki.isConnected(); - } - - async _onApiAddAnkiNote({note}) { - return await this._anki.addNote(note); - } - - async _onApiGetAnkiNoteInfo({notes}) { - const results = []; - const cannotAdd = []; - const canAddArray = await this._anki.canAddNotes(notes); - - for (let i = 0; i < notes.length; ++i) { - const note = notes[i]; - const canAdd = canAddArray[i]; - const info = {canAdd, noteIds: null}; - results.push(info); - if (!canAdd) { - cannotAdd.push({note, info}); - } - } - - if (cannotAdd.length > 0) { - const cannotAddNotes = cannotAdd.map(({note}) => note); - const noteIdsArray = await this._anki.findNoteIds(cannotAddNotes); - for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) { - const noteIds = noteIdsArray[i]; - if (noteIds.length > 0) { - cannotAdd[i].info.noteIds = noteIds; - } - } - } - - return results; - } - - async _onApiInjectAnkiNoteMedia({timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails}) { - return await this._injectAnkNoteMedia( - this._anki, - timestamp, - definitionDetails, - audioDetails, - screenshotDetails, - clipboardDetails - ); - } - - async _onApiNoteView({noteId}) { - return await this._anki.guiBrowseNote(noteId); - } - - async _onApiSuspendAnkiCardsForNote({noteId}) { - const cardIds = await this._anki.findCardsForNote(noteId); - const count = cardIds.length; - if (count > 0) { - const okay = await this._anki.suspendCards(cardIds); - if (!okay) { return 0; } - } - return count; - } - - _onApiCommandExec({command, params}) { - return this._runCommand(command, params); - } - - async _onApiGetExpressionAudioInfoList({source, expression, reading, details}) { - return await this._audioDownloader.getExpressionAudioInfoList(source, expression, reading, details); - } - - _onApiSendMessageToFrame({frameId: targetFrameId, action, params}, sender) { - if (!(sender && sender.tab)) { - return false; - } - - const tabId = sender.tab.id; - const frameId = sender.frameId; - this._sendMessageTabIgnoreResponse(tabId, {action, params, frameId}, {frameId: targetFrameId}); - return true; - } - - _onApiBroadcastTab({action, params}, sender) { - if (!(sender && sender.tab)) { - return false; - } - - const tabId = sender.tab.id; - const frameId = sender.frameId; - this._sendMessageTabIgnoreResponse(tabId, {action, params, frameId}); - return true; - } - - _onApiFrameInformationGet(params, sender) { - const tab = sender.tab; - const tabId = tab ? tab.id : void 0; - const frameId = sender.frameId; - return Promise.resolve({tabId, frameId}); - } - - _onApiInjectStylesheet({type, value}, sender) { - return this._injectStylesheet(type, value, sender); - } - - async _onApiGetStylesheetContent({url}) { - if (!url.startsWith('/') || url.startsWith('//') || !url.endsWith('.css')) { - throw new Error('Invalid URL'); - } - return await this._fetchAsset(url); - } - - _onApiGetEnvironmentInfo() { - return this._environment.getInfo(); - } - - async _onApiClipboardGet() { - return this._clipboardReader.getText(); - } - - async _onApiGetDisplayTemplatesHtml() { - return await this._fetchAsset('/display-templates.html'); - } - - _onApiGetZoom(params, sender) { - if (!sender || !sender.tab) { - return Promise.reject(new Error('Invalid tab')); - } - - return new Promise((resolve, reject) => { - const tabId = sender.tab.id; - if (!( - chrome.tabs !== null && - typeof chrome.tabs === 'object' && - typeof chrome.tabs.getZoom === 'function' - )) { - // Not supported - resolve({zoomFactor: 1.0}); - return; - } - chrome.tabs.getZoom(tabId, (zoomFactor) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve({zoomFactor}); - } - }); - }); - } - - _onApiGetDefaultAnkiFieldTemplates() { - return this._defaultAnkiFieldTemplates; - } - - async _onApiGetDictionaryInfo() { - return await this._dictionaryDatabase.getDictionaryInfo(); - } - - async _onApiGetDictionaryCounts({dictionaryNames, getTotal}) { - return await this._dictionaryDatabase.getDictionaryCounts(dictionaryNames, getTotal); - } - - async _onApiPurgeDatabase() { - await this._dictionaryDatabase.purge(); - this._triggerDatabaseUpdated('dictionary', 'purge'); - } - - async _onApiGetMedia({targets}) { - return await this._dictionaryDatabase.getMedia(targets); - } - - _onApiLog({error, level, context}) { - yomichan.log(deserializeError(error), level, context); - } - - _onApiLogIndicatorClear() { - if (this._logErrorLevel === null) { return; } - this._logErrorLevel = null; - this._updateBadge(); - } - - _onApiCreateActionPort(params, sender) { - if (!sender || !sender.tab) { throw new Error('Invalid sender'); } - const tabId = sender.tab.id; - if (typeof tabId !== 'number') { throw new Error('Sender has invalid tab ID'); } - - const frameId = sender.frameId; - const id = generateId(16); - const details = { - name: 'action-port', - id - }; - - const port = chrome.tabs.connect(tabId, {name: JSON.stringify(details), frameId}); - try { - this._createActionListenerPort(port, sender, this._messageHandlersWithProgress); - } catch (e) { - port.disconnect(); - throw e; - } - - return details; - } - - _onApiModifySettings({targets, source}) { - return this._modifySettings(targets, source); - } - - _onApiGetSettings({targets}) { - const results = []; - for (const target of targets) { - try { - const result = this._getSetting(target); - results.push({result: clone(result)}); - } catch (e) { - results.push({error: serializeError(e)}); - } - } - return results; - } - - async _onApiSetAllSettings({value, source}) { - this._optionsUtil.validate(value); - this._options = clone(value); - await this._saveOptions(source); - } - - async _onApiGetOrCreateSearchPopup({focus=false, text=null}) { - const {tab, created} = await this._getOrCreateSearchPopup(); - if (focus === true || (focus === 'ifCreated' && created)) { - await this._focusTab(tab); - } - if (typeof text === 'string') { - await this._updateSearchQuery(tab.id, text, !created); - } - return {tabId: tab.id, windowId: tab.windowId}; - } - - async _onApiIsTabSearchPopup({tabId}) { - const baseUrl = chrome.runtime.getURL('/search.html'); - const tab = typeof tabId === 'number' ? await this._checkTabUrl(tabId, (url) => url.startsWith(baseUrl)) : null; - return (tab !== null); - } - - _onApiTriggerDatabaseUpdated({type, cause}) { - this._triggerDatabaseUpdated(type, cause); - } - - async _onApiTestMecab() { - if (!this._mecab.isEnabled()) { - throw new Error('MeCab not enabled'); - } - - let permissionsOkay = false; - try { - permissionsOkay = await this._permissionsUtil.hasPermissions({permissions: ['nativeMessaging']}); - } catch (e) { - // NOP - } - if (!permissionsOkay) { - throw new Error('Insufficient permissions'); - } - - const disconnect = !this._mecab.isConnected(); - try { - const version = await this._mecab.getVersion(); - if (version === null) { - throw new Error('Could not connect to native MeCab component'); - } - - const localVersion = this._mecab.getLocalVersion(); - if (version !== localVersion) { - throw new Error(`MeCab component version not supported: ${version}`); - } - } finally { - // Disconnect if the connection was previously disconnected - if (disconnect && this._mecab.isEnabled() && this._mecab.isActive()) { - this._mecab.disconnect(); - } - } - - return true; - } - - // Command handlers - - async _onCommandOpenSearchPage(params) { - const {mode='existingOrNewTab', query} = params || {}; - - const baseUrl = chrome.runtime.getURL('/search.html'); - const queryParams = {}; - if (query && query.length > 0) { queryParams.query = query; } - const queryString = new URLSearchParams(queryParams).toString(); - let url = baseUrl; - if (queryString.length > 0) { - url += `?${queryString}`; - } - - const predicate = ({url: url2}) => { - if (url2 === null || !url2.startsWith(baseUrl)) { return false; } - const parsedUrl = new URL(url2); - const baseUrl2 = `${parsedUrl.origin}${parsedUrl.pathname}`; - const mode2 = parsedUrl.searchParams.get('mode'); - return baseUrl2 === baseUrl && (mode2 === mode || (!mode2 && mode === 'existingOrNewTab')); - }; - - const openInTab = async () => { - const tab = await this._findTabs(1000, false, predicate, false); - if (tab !== null) { - await this._focusTab(tab); - if (queryParams.query) { - await this._updateSearchQuery(tab.id, queryParams.query, true); - } - return true; - } - }; - - switch (mode) { - case 'existingOrNewTab': - try { - if (await openInTab()) { return; } - } catch (e) { - // NOP - } - await this._createTab(url); - return; - case 'newTab': - await this._createTab(url); - return; - } - } - - async _onCommandOpenInfoPage() { - await this._openInfoPage(); - } - - async _onCommandOpenSettingsPage(params) { - const {mode='existingOrNewTab'} = params || {}; - await this._openSettingsPage(mode); - } - - async _onCommandToggleTextScanning() { - const options = this._getProfileOptions({current: true}); - await this._modifySettings([{ - action: 'set', - path: 'general.enable', - value: !options.general.enable, - scope: 'profile', - optionsContext: {current: true} - }], 'backend'); - } - - async _onCommandOpenPopupWindow() { - await this._onApiGetOrCreateSearchPopup({focus: true}); - } - - // Utilities - - async _modifySettings(targets, source) { - const results = []; - for (const target of targets) { - try { - const result = this._modifySetting(target); - results.push({result: clone(result)}); - } catch (e) { - results.push({error: serializeError(e)}); - } - } - await this._saveOptions(source); - return results; - } - - _getOrCreateSearchPopup() { - if (this._searchPopupTabCreatePromise === null) { - const promise = this._getOrCreateSearchPopup2(); - this._searchPopupTabCreatePromise = promise; - promise.then(() => { this._searchPopupTabCreatePromise = null; }); - } - return this._searchPopupTabCreatePromise; - } - - async _getOrCreateSearchPopup2() { - // Use existing tab - const baseUrl = chrome.runtime.getURL('/search.html'); - const urlPredicate = (url) => url !== null && url.startsWith(baseUrl); - if (this._searchPopupTabId !== null) { - const tab = await this._checkTabUrl(this._searchPopupTabId, urlPredicate); - if (tab !== null) { - return {tab, created: false}; - } - this._searchPopupTabId = null; - } - - // Find existing tab - const existingTabInfo = await this._findSearchPopupTab(urlPredicate); - if (existingTabInfo !== null) { - const existingTab = existingTabInfo.tab; - this._searchPopupTabId = existingTab.id; - return {tab: existingTab, created: false}; - } - - // chrome.windows not supported (e.g. on Firefox mobile) - if (!isObject(chrome.windows)) { - throw new Error('Window creation not supported'); - } - - // Create a new window - const options = this._getProfileOptions({current: true}); - const createData = this._getSearchPopupWindowCreateData(baseUrl, options); - const {popupWindow: {windowState}} = options; - const popupWindow = await this._createWindow(createData); - if (windowState !== 'normal') { - await this._updateWindow(popupWindow.id, {state: windowState}); - } - - const {tabs} = popupWindow; - if (tabs.length === 0) { - throw new Error('Created window did not contain a tab'); - } - - const tab = tabs[0]; - await this._waitUntilTabFrameIsReady(tab.id, 0, 2000); - - await this._sendMessageTabPromise( - tab.id, - {action: 'setMode', params: {mode: 'popup'}}, - {frameId: 0} - ); - - this._searchPopupTabId = tab.id; - return {tab, created: true}; - } - - async _findSearchPopupTab(urlPredicate) { - const predicate = async ({url, tab}) => { - if (!urlPredicate(url)) { return false; } - try { - const mode = await this._sendMessageTabPromise( - tab.id, - {action: 'getMode', params: {}}, - {frameId: 0} - ); - return mode === 'popup'; - } catch (e) { - return false; - } - }; - return await this._findTabs(1000, false, predicate, true); - } - - _getSearchPopupWindowCreateData(url, options) { - const {popupWindow: {width, height, left, top, useLeft, useTop, windowType}} = options; - return { - url, - width, - height, - left: useLeft ? left : void 0, - top: useTop ? top : void 0, - type: windowType, - state: 'normal' - }; - } - - _createWindow(createData) { - return new Promise((resolve, reject) => { - chrome.windows.create( - createData, - (result) => { - const error = chrome.runtime.lastError; - if (error) { - reject(new Error(error.message)); - } else { - resolve(result); - } - } - ); - }); - } - - _updateWindow(windowId, updateInfo) { - return new Promise((resolve, reject) => { - chrome.windows.update( - windowId, - updateInfo, - (result) => { - const error = chrome.runtime.lastError; - if (error) { - reject(new Error(error.message)); - } else { - resolve(result); - } - } - ); - }); - } - - _updateSearchQuery(tabId, text, animate) { - return this._sendMessageTabPromise( - tabId, - {action: 'updateSearchQuery', params: {text, animate}}, - {frameId: 0} - ); - } - - _applyOptions(source) { - const options = this._getProfileOptions({current: true}); - this._updateBadge(); - - this._anki.server = options.anki.server; - this._anki.enabled = options.anki.enable; - - this._mecab.setEnabled(options.parsing.enableMecabParser); - - if (options.clipboard.enableBackgroundMonitor) { - this._clipboardMonitor.start(); - } else { - this._clipboardMonitor.stop(); - } - - this._sendMessageAllTabsIgnoreResponse('optionsUpdated', {source}); - } - - _getOptionsFull(useSchema=false) { - const options = this._options; - return useSchema ? this._optionsUtil.createValidatingProxy(options) : options; - } - - _getProfileOptions(optionsContext, useSchema=false) { - return this._getProfile(optionsContext, useSchema).options; - } - - _getProfile(optionsContext, useSchema=false) { - const options = this._getOptionsFull(useSchema); - const profiles = options.profiles; - if (optionsContext.current) { - return profiles[options.profileCurrent]; - } - if (typeof optionsContext.index === 'number') { - return profiles[optionsContext.index]; - } - const profile = this._getProfileFromContext(options, optionsContext); - return profile !== null ? profile : profiles[options.profileCurrent]; - } - - _getProfileFromContext(options, optionsContext) { - optionsContext = this._profileConditionsUtil.normalizeContext(optionsContext); - - let index = 0; - for (const profile of options.profiles) { - const conditionGroups = profile.conditionGroups; - - let schema; - if (index < this._profileConditionsSchemaCache.length) { - schema = this._profileConditionsSchemaCache[index]; - } else { - schema = this._profileConditionsUtil.createSchema(conditionGroups); - this._profileConditionsSchemaCache.push(schema); - } - - if (conditionGroups.length > 0 && this._profileConditionsSchemaValidator.isValid(optionsContext, schema)) { - return profile; - } - ++index; - } - - return null; - } - - _clearProfileConditionsSchemaCache() { - this._profileConditionsSchemaCache = []; - this._profileConditionsSchemaValidator.clearCache(); - } - - _checkLastError() { - // NOP - } - - _runCommand(command, params) { - const handler = this._commandHandlers.get(command); - if (typeof handler !== 'function') { return false; } - - handler(params); - return true; - } - - async _textParseScanning(text, options) { - const jp = this._japaneseUtil; - const {scanning: {length: scanningLength}, parsing: {readingMode}} = options; - const findTermsOptions = this._getTranslatorFindTermsOptions({wildcard: null}, options); - const results = []; - while (text.length > 0) { - const term = []; - const [definitions, sourceLength] = await this._translator.findTerms( - 'simple', - text.substring(0, scanningLength), - findTermsOptions - ); - if (definitions.length > 0 && sourceLength > 0) { - const {expression, reading} = definitions[0]; - const source = text.substring(0, sourceLength); - for (const {text: text2, furigana} of jp.distributeFuriganaInflected(expression, reading, source)) { - const reading2 = jp.convertReading(text2, furigana, readingMode); - term.push({text: text2, reading: reading2}); - } - text = text.substring(source.length); - } else { - const reading = jp.convertReading(text[0], '', readingMode); - term.push({text: text[0], reading}); - text = text.substring(1); - } - results.push(term); - } - return results; - } - - async _textParseMecab(text, options) { - const jp = this._japaneseUtil; - const {parsing: {readingMode}} = options; - - let parseTextResults; - try { - parseTextResults = await this._mecab.parseText(text); - } catch (e) { - return []; - } - - const results = []; - for (const {name, lines} of parseTextResults) { - const result = []; - for (const line of lines) { - for (const {expression, reading, source} of line) { - const term = []; - for (const {text: text2, furigana} of jp.distributeFuriganaInflected( - expression.length > 0 ? expression : source, - jp.convertKatakanaToHiragana(reading), - source - )) { - const reading2 = jp.convertReading(text2, furigana, readingMode); - term.push({text: text2, reading: reading2}); - } - result.push(term); - } - result.push([{text: '\n', reading: ''}]); - } - results.push([name, result]); - } - return results; - } - - _createActionListenerPort(port, sender, handlers) { - let hasStarted = false; - let messageString = ''; - - const onProgress = (...data) => { - try { - if (port === null) { return; } - port.postMessage({type: 'progress', data}); - } catch (e) { - // NOP - } - }; - - const onMessage = (message) => { - if (hasStarted) { return; } - - try { - const {action, data} = message; - switch (action) { - case 'fragment': - messageString += data; - break; - case 'invoke': - { - hasStarted = true; - port.onMessage.removeListener(onMessage); - - const messageData = JSON.parse(messageString); - messageString = null; - onMessageComplete(messageData); - } - break; - } - } catch (e) { - cleanup(e); - } - }; - - const onMessageComplete = async (message) => { - try { - const {action, params} = message; - port.postMessage({type: 'ack'}); - - const messageHandler = handlers.get(action); - if (typeof messageHandler === 'undefined') { - throw new Error('Invalid action'); - } - const {handler, async, contentScript} = messageHandler; - - if (!contentScript) { - this._validatePrivilegedMessageSender(sender); - } - - const promiseOrResult = handler(params, sender, onProgress); - const result = async ? await promiseOrResult : promiseOrResult; - port.postMessage({type: 'complete', data: result}); - } catch (e) { - cleanup(e); - } - }; - - const onDisconnect = () => { - cleanup(null); - }; - - const cleanup = (error) => { - if (port === null) { return; } - if (error !== null) { - port.postMessage({type: 'error', data: serializeError(error)}); - } - if (!hasStarted) { - port.onMessage.removeListener(onMessage); - } - port.onDisconnect.removeListener(onDisconnect); - port = null; - handlers = null; - }; - - port.onMessage.addListener(onMessage); - port.onDisconnect.addListener(onDisconnect); - } - - _getErrorLevelValue(errorLevel) { - switch (errorLevel) { - case 'info': return 0; - case 'debug': return 0; - case 'warn': return 1; - case 'error': return 2; - default: return 0; - } - } - - _getModifySettingObject(target) { - const scope = target.scope; - switch (scope) { - case 'profile': - if (!isObject(target.optionsContext)) { throw new Error('Invalid optionsContext'); } - return this._getProfileOptions(target.optionsContext, true); - case 'global': - return this._getOptionsFull(true); - default: - throw new Error(`Invalid scope: ${scope}`); - } - } - - _getSetting(target) { - const options = this._getModifySettingObject(target); - const accessor = new ObjectPropertyAccessor(options); - const {path} = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - return accessor.get(ObjectPropertyAccessor.getPathArray(path)); - } - - _modifySetting(target) { - const options = this._getModifySettingObject(target); - const accessor = new ObjectPropertyAccessor(options); - const action = target.action; - switch (action) { - case 'set': - { - const {path, value} = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - const pathArray = ObjectPropertyAccessor.getPathArray(path); - accessor.set(pathArray, value); - return accessor.get(pathArray); - } - case 'delete': - { - const {path} = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - accessor.delete(ObjectPropertyAccessor.getPathArray(path)); - return true; - } - case 'swap': - { - const {path1, path2} = target; - if (typeof path1 !== 'string') { throw new Error('Invalid path1'); } - if (typeof path2 !== 'string') { throw new Error('Invalid path2'); } - accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2)); - return true; - } - case 'splice': - { - const {path, start, deleteCount, items} = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); } - if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); } - if (!Array.isArray(items)) { throw new Error('Invalid items'); } - const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); - if (!Array.isArray(array)) { throw new Error('Invalid target type'); } - return array.splice(start, deleteCount, ...items); - } - default: - throw new Error(`Unknown action: ${action}`); - } - } - - _validatePrivilegedMessageSender(sender) { - const url = sender.url; - if (!(typeof url === 'string' && yomichan.isExtensionUrl(url))) { - throw new Error('Invalid message sender'); - } - } - - _getBrowserIconTitle() { - return ( - isObject(chrome.browserAction) && - typeof chrome.browserAction.getTitle === 'function' ? - new Promise((resolve) => chrome.browserAction.getTitle({}, resolve)) : - Promise.resolve('') - ); - } - - _updateBadge() { - let title = this._defaultBrowserActionTitle; - if (title === null || !isObject(chrome.browserAction)) { - // Not ready or invalid - return; - } - - let text = ''; - let color = null; - let status = null; - - if (this._logErrorLevel !== null) { - switch (this._logErrorLevel) { - case 'error': - text = '!!'; - color = '#f04e4e'; - status = 'Error'; - break; - default: // 'warn' - text = '!'; - color = '#f0ad4e'; - status = 'Warning'; - break; - } - } else if (!this._isPrepared) { - if (this._prepareError) { - text = '!!'; - color = '#f04e4e'; - status = 'Error'; - } else if (this._badgePrepareDelayTimer === null) { - text = '...'; - color = '#f0ad4e'; - status = 'Loading'; - } - } else { - const options = this._getProfileOptions({current: true}); - if (!options.general.enable) { - text = 'off'; - color = '#555555'; - status = 'Disabled'; - } else if (!this._hasRequiredPermissionsForSettings(options)) { - text = '!'; - color = '#f0ad4e'; - status = 'Some settings require additional permissions'; - } else if (!this._isAnyDictionaryEnabled(options)) { - text = '!'; - color = '#f0ad4e'; - status = 'No dictionaries installed'; - } - } - - if (color !== null && typeof chrome.browserAction.setBadgeBackgroundColor === 'function') { - chrome.browserAction.setBadgeBackgroundColor({color}); - } - if (text !== null && typeof chrome.browserAction.setBadgeText === 'function') { - chrome.browserAction.setBadgeText({text}); - } - if (typeof chrome.browserAction.setTitle === 'function') { - if (status !== null) { - title = `${title} - ${status}`; - } - chrome.browserAction.setTitle({title}); - } - } - - _isAnyDictionaryEnabled(options) { - for (const {enabled} of Object.values(options.dictionaries)) { - if (enabled) { - return true; - } - } - return false; - } - - _anyOptionsMatches(predicate) { - for (const {options} of this._options.profiles) { - const value = predicate(options); - if (value) { return value; } - } - return false; - } - - async _getTabUrl(tabId) { - try { - const {url} = await this._sendMessageTabPromise( - tabId, - {action: 'getUrl', params: {}}, - {frameId: 0} - ); - if (typeof url === 'string') { - return url; - } - } catch (e) { - // NOP - } - return null; - } - - _getAllTabs() { - return new Promise((resolve, reject) => { - chrome.tabs.query({}, (tabs) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(tabs); - } - }); - }); - } - - async _findTabs(timeout, multiple, predicate, predicateIsAsync) { - // This function works around the need to have the "tabs" permission to access tab.url. - const tabs = await this._getAllTabs(); - - let done = false; - const checkTab = async (tab, add) => { - const url = await this._getTabUrl(tab.id); - - if (done) { return; } - - let okay = false; - const item = {tab, url}; - try { - okay = predicate(item); - if (predicateIsAsync) { okay = await okay; } - } catch (e) { - // NOP - } - - if (okay && !done) { - if (add(item)) { - done = true; - } - } - }; - - if (multiple) { - const results = []; - const add = (value) => { - results.push(value); - return false; - }; - const checkTabPromises = tabs.map((tab) => checkTab(tab, add)); - await Promise.race([ - Promise.all(checkTabPromises), - promiseTimeout(timeout) - ]); - return results; - } else { - const {promise, resolve} = deferPromise(); - let result = null; - const add = (value) => { - result = value; - resolve(); - return true; - }; - const checkTabPromises = tabs.map((tab) => checkTab(tab, add)); - await Promise.race([ - promise, - Promise.all(checkTabPromises), - promiseTimeout(timeout) - ]); - resolve(); - return result; - } - } - - async _focusTab(tab) { - await new Promise((resolve, reject) => { - chrome.tabs.update(tab.id, {active: true}, () => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(); - } - }); - }); - - if (!(typeof chrome.windows === 'object' && chrome.windows !== null)) { - // Windows not supported (e.g. on Firefox mobile) - return; - } - - try { - const tabWindow = await new Promise((resolve, reject) => { - chrome.windows.get(tab.windowId, {}, (value) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(value); - } - }); - }); - if (!tabWindow.focused) { - await new Promise((resolve, reject) => { - chrome.windows.update(tab.windowId, {focused: true}, () => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(); - } - }); - }); - } - } catch (e) { - // Edge throws exception for no reason here. - } - } - - _waitUntilTabFrameIsReady(tabId, frameId, timeout=null) { - return new Promise((resolve, reject) => { - let timer = null; - let onMessage = (message, sender) => { - if ( - !sender.tab || - sender.tab.id !== tabId || - sender.frameId !== frameId || - !isObject(message) || - message.action !== 'yomichanReady' - ) { - return; - } - - cleanup(); - resolve(); - }; - const cleanup = () => { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - if (onMessage !== null) { - chrome.runtime.onMessage.removeListener(onMessage); - onMessage = null; - } - }; - - chrome.runtime.onMessage.addListener(onMessage); - - this._sendMessageTabPromise(tabId, {action: 'isReady'}, {frameId}) - .then( - (value) => { - if (!value) { return; } - cleanup(); - resolve(); - }, - () => {} // NOP - ); - - if (timeout !== null) { - timer = setTimeout(() => { - timer = null; - cleanup(); - reject(new Error('Timeout')); - }, timeout); - } - }); - } - - async _fetchAsset(url, json=false) { - const response = await fetch(chrome.runtime.getURL(url), { - method: 'GET', - mode: 'no-cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }); - if (!response.ok) { - throw new Error(`Failed to fetch ${url}: ${response.status}`); - } - return await (json ? response.json() : response.text()); - } - - _sendMessageIgnoreResponse(...args) { - const callback = () => this._checkLastError(chrome.runtime.lastError); - chrome.runtime.sendMessage(...args, callback); - } - - _sendMessageTabIgnoreResponse(...args) { - const callback = () => this._checkLastError(chrome.runtime.lastError); - chrome.tabs.sendMessage(...args, callback); - } - - _sendMessageAllTabsIgnoreResponse(action, params) { - const callback = () => this._checkLastError(chrome.runtime.lastError); - chrome.tabs.query({}, (tabs) => { - for (const tab of tabs) { - chrome.tabs.sendMessage(tab.id, {action, params}, callback); - } - }); - } - - _sendMessageTabPromise(...args) { - return new Promise((resolve, reject) => { - const callback = (response) => { - try { - resolve(yomichan.getMessageResponseResult(response)); - } catch (error) { - reject(error); - } - }; - - chrome.tabs.sendMessage(...args, callback); - }); - } - - async _checkTabUrl(tabId, urlPredicate) { - let tab; - try { - tab = await this._getTabById(tabId); - } catch (e) { - return null; - } - - const url = await this._getTabUrl(tabId); - const isValidTab = urlPredicate(url); - return isValidTab ? tab : null; - } - - async _getScreenshot(tabId, frameId, format, quality) { - const tab = await this._getTabById(tabId); - const {windowId} = tab; - - let token = null; - try { - if (typeof tabId === 'number' && typeof frameId === 'number') { - const action = 'setAllVisibleOverride'; - const params = {value: false, priority: 0, awaitFrame: true}; - token = await this._sendMessageTabPromise(tabId, {action, params}, {frameId}); - } - - return await new Promise((resolve, reject) => { - chrome.tabs.captureVisibleTab(windowId, {format, quality}, (result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - }); - }); - } finally { - if (token !== null) { - const action = 'clearAllVisibleOverride'; - const params = {token}; - try { - await this._sendMessageTabPromise(tabId, {action, params}, {frameId}); - } catch (e) { - // NOP - } - } - } - } - - async _downloadDefinitionAudio(sources, expression, reading, details) { - return await this._audioDownloader.downloadExpressionAudio(sources, expression, reading, details); - } - - async _injectAnkNoteMedia(ankiConnect, timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails) { - let screenshotFileName = null; - let clipboardImageFileName = null; - let clipboardText = null; - let audioFileName = null; - const errors = []; - - try { - if (screenshotDetails !== null) { - screenshotFileName = await this._injectAnkNoteScreenshot(ankiConnect, timestamp, definitionDetails, screenshotDetails); - } - } catch (e) { - errors.push(serializeError(e)); - } - - try { - if (clipboardDetails !== null && clipboardDetails.image) { - clipboardImageFileName = await this._injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails); - } - } catch (e) { - errors.push(serializeError(e)); - } - - try { - if (clipboardDetails !== null && clipboardDetails.text) { - clipboardText = await this._clipboardReader.getText(); - } - } catch (e) { - errors.push(serializeError(e)); - } - - try { - if (audioDetails !== null) { - audioFileName = await this._injectAnkNoteAudio(ankiConnect, timestamp, definitionDetails, audioDetails); - } - } catch (e) { - errors.push(serializeError(e)); - } - - return { - result: { - screenshotFileName, - clipboardImageFileName, - clipboardText, - audioFileName - }, - errors - }; - } - - async _injectAnkNoteAudio(ankiConnect, timestamp, definitionDetails, details) { - const {type, expression, reading} = definitionDetails; - if ( - type === 'kanji' || - typeof expression !== 'string' || - typeof reading !== 'string' || - (expression.length === 0 && reading.length === 0) - ) { - return null; - } - - const {sources, customSourceUrl, customSourceType} = details; - let data; - let contentType; - try { - ({data, contentType} = await this._downloadDefinitionAudio( - sources, - expression, - reading, - { - textToSpeechVoice: null, - customSourceUrl, - customSourceType, - binary: true, - disableCache: true - } - )); - } catch (e) { - // No audio - return null; - } - - let extension = this._mediaUtility.getFileExtensionFromAudioMediaType(contentType); - if (extension === null) { extension = '.mp3'; } - let fileName = this._generateAnkiNoteMediaFileName('yomichan_audio', extension, timestamp, definitionDetails); - fileName = fileName.replace(/\]/g, ''); - await ankiConnect.storeMediaFile(fileName, data); - - return fileName; - } - - async _injectAnkNoteScreenshot(ankiConnect, timestamp, definitionDetails, details) { - const {tabId, frameId, format, quality} = details; - const dataUrl = await this._getScreenshot(tabId, frameId, format, quality); - - const {mediaType, data} = this._getDataUrlInfo(dataUrl); - const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType); - if (extension === null) { - throw new Error('Unknown media type for screenshot image'); - } - - const fileName = this._generateAnkiNoteMediaFileName('yomichan_browser_screenshot', extension, timestamp, definitionDetails); - await ankiConnect.storeMediaFile(fileName, data); - - return fileName; - } - - async _injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails) { - const dataUrl = await this._clipboardReader.getImage(); - if (dataUrl === null) { - return null; - } - - const {mediaType, data} = this._getDataUrlInfo(dataUrl); - const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType); - if (extension === null) { - throw new Error('Unknown media type for clipboard image'); - } - - const fileName = this._generateAnkiNoteMediaFileName('yomichan_clipboard_image', extension, timestamp, definitionDetails); - await ankiConnect.storeMediaFile(fileName, data); - - return fileName; - } - - _generateAnkiNoteMediaFileName(prefix, extension, timestamp, definitionDetails) { - let fileName = prefix; - - switch (definitionDetails.type) { - case 'kanji': - { - const {character} = definitionDetails; - if (character) { fileName += `_${character}`; } - } - break; - default: - { - const {reading, expression} = definitionDetails; - if (reading) { fileName += `_${reading}`; } - if (expression) { fileName += `_${expression}`; } - } - break; - } - - fileName += `_${this._ankNoteDateToString(new Date(timestamp))}`; - fileName += extension; - - fileName = this._replaceInvalidFileNameCharacters(fileName); - - return fileName; - } - - _replaceInvalidFileNameCharacters(fileName) { - // eslint-disable-next-line no-control-regex - return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-'); - } - - _ankNoteDateToString(date) { - const year = date.getUTCFullYear(); - const month = date.getUTCMonth().toString().padStart(2, '0'); - const day = date.getUTCDate().toString().padStart(2, '0'); - const hours = date.getUTCHours().toString().padStart(2, '0'); - const minutes = date.getUTCMinutes().toString().padStart(2, '0'); - const seconds = date.getUTCSeconds().toString().padStart(2, '0'); - return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; - } - - _getDataUrlInfo(dataUrl) { - const match = /^data:([^,]*?)(;base64)?,/.exec(dataUrl); - if (match === null) { - throw new Error('Invalid data URL'); - } - - let mediaType = match[1]; - if (mediaType.length === 0) { mediaType = 'text/plain'; } - - let data = dataUrl.substring(match[0].length); - if (typeof match[2] === 'undefined') { data = btoa(data); } - - return {mediaType, data}; - } - - _triggerDatabaseUpdated(type, cause) { - this._translator.clearDatabaseCaches(); - this._sendMessageAllTabsIgnoreResponse('databaseUpdated', {type, cause}); - } - - async _saveOptions(source) { - this._clearProfileConditionsSchemaCache(); - const options = this._getOptionsFull(); - await this._optionsUtil.save(options); - this._applyOptions(source); - } - - _getTranslatorFindTermsOptions(details, options) { - const {wildcard} = details; - const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options); - const { - general: {mainDictionary}, - scanning: {alphanumeric}, - translation: { - convertHalfWidthCharacters, - convertNumericCharacters, - convertAlphabeticCharacters, - convertHiraganaToKatakana, - convertKatakanaToHiragana, - collapseEmphaticSequences, - textReplacements: textReplacementsOptions - } - } = options; - const textReplacements = this._getTranslatorTextReplacements(textReplacementsOptions); - return { - wildcard, - mainDictionary, - alphanumeric, - convertHalfWidthCharacters, - convertNumericCharacters, - convertAlphabeticCharacters, - convertHiraganaToKatakana, - convertKatakanaToHiragana, - collapseEmphaticSequences, - textReplacements, - enabledDictionaryMap - }; - } - - _getTranslatorFindKanjiOptions(options) { - const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options); - return {enabledDictionaryMap}; - } - - _getTranslatorEnabledDictionaryMap(options) { - const enabledDictionaryMap = new Map(); - for (const [title, {enabled, priority, allowSecondarySearches}] of Object.entries(options.dictionaries)) { - if (!enabled) { continue; } - enabledDictionaryMap.set(title, {priority, allowSecondarySearches}); - } - return enabledDictionaryMap; - } - - _getTranslatorTextReplacements(textReplacementsOptions) { - const textReplacements = []; - for (const group of textReplacementsOptions.groups) { - const textReplacementsEntries = []; - for (let {pattern, ignoreCase, replacement} of group) { - try { - pattern = new RegExp(pattern, ignoreCase ? 'gi' : 'g'); - } catch (e) { - // Invalid pattern - continue; - } - textReplacementsEntries.push({pattern, replacement}); - } - if (textReplacementsEntries.length > 0) { - textReplacements.push(textReplacementsEntries); - } - } - if (textReplacements.length === 0 || textReplacementsOptions.searchOriginal) { - textReplacements.unshift(null); - } - return textReplacements; - } - - async _openWelcomeGuidePage() { - await this._createTab(chrome.runtime.getURL('/welcome.html')); - } - - async _openInfoPage() { - await this._createTab(chrome.runtime.getURL('/info.html')); - } - - async _openSettingsPage(mode) { - const {useSettingsV2} = this._options.global; - const manifest = chrome.runtime.getManifest(); - const url = chrome.runtime.getURL(useSettingsV2 ? manifest.options_ui.page : '/settings-old.html'); - switch (mode) { - case 'existingOrNewTab': - if (useSettingsV2) { - const predicate = ({url: url2}) => (url2 !== null && url2.startsWith(url)); - const tab = await this._findTabs(1000, false, predicate, false); - if (tab !== null) { - await this._focusTab(tab); - } else { - await this._createTab(url); - } - } else { - await new Promise((resolve, reject) => { - chrome.runtime.openOptionsPage(() => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(); - } - }); - }); - } - break; - case 'newTab': - await this._createTab(url); - break; - } - } - - _createTab(url) { - return new Promise((resolve, reject) => { - chrome.tabs.create({url}, (tab) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(tab); - } - }); - }); - } - - _injectStylesheet(type, value, target) { - if (isObject(chrome.tabs) && typeof chrome.tabs.insertCSS === 'function') { - return this._injectStylesheetMV2(type, value, target); - } else if (isObject(chrome.scripting) && typeof chrome.scripting.insertCSS === 'function') { - return this._injectStylesheetMV3(type, value, target); - } else { - return Promise.reject(new Error('insertCSS function not available')); - } - } - - _injectStylesheetMV2(type, value, target) { - return new Promise((resolve, reject) => { - if (!target.tab) { - reject(new Error('Invalid tab')); - return; - } - - const tabId = target.tab.id; - const frameId = target.frameId; - const details = ( - type === 'file' ? - { - file: value, - runAt: 'document_start', - cssOrigin: 'author', - allFrames: false, - matchAboutBlank: true - } : - { - code: value, - runAt: 'document_start', - cssOrigin: 'user', - allFrames: false, - matchAboutBlank: true - } - ); - if (typeof frameId === 'number') { - details.frameId = frameId; - } - - chrome.tabs.insertCSS(tabId, details, () => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(); - } - }); - }); - } - - _injectStylesheetMV3(type, value, target) { - return new Promise((resolve, reject) => { - if (!target.tab) { - reject(new Error('Invalid tab')); - return; - } - - const tabId = target.tab.id; - const frameId = target.frameId; - const details = ( - type === 'file' ? - {origin: chrome.scripting.StyleOrigin.AUTHOR, files: [value]} : - {origin: chrome.scripting.StyleOrigin.USER, css: value} - ); - details.target = { - tabId, - allFrames: false - }; - if (typeof frameId === 'number') { - details.target.frameIds = [frameId]; - } - - chrome.scripting.insertCSS(details, () => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(); - } - }); - }); - } - - _getTabById(tabId) { - return new Promise((resolve, reject) => { - chrome.tabs.get( - tabId, - (result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - } - ); - }); - } - - async _checkPermissions() { - this._permissions = await this._permissionsUtil.getAllPermissions(); - this._updateBadge(); - } - - _hasRequiredPermissionsForSettings(options) { - return this._permissions === null || this._permissionsUtil.hasRequiredPermissionsForOptions(this._permissions, options); - } -} diff --git a/ext/bg/js/background-main.js b/ext/bg/js/background-main.js deleted file mode 100644 index 01e57d0f..00000000 --- a/ext/bg/js/background-main.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * Backend - */ - -(() => { - const backend = new Backend(); - backend.prepare(); -})(); diff --git a/ext/bg/js/clipboard-monitor.js b/ext/bg/js/clipboard-monitor.js deleted file mode 100644 index 7379d7ad..00000000 --- a/ext/bg/js/clipboard-monitor.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class ClipboardMonitor extends EventDispatcher { - constructor({japaneseUtil, clipboardReader}) { - super(); - this._japaneseUtil = japaneseUtil; - this._clipboardReader = clipboardReader; - this._timerId = null; - this._timerToken = null; - this._interval = 250; - this._previousText = null; - } - - 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 call - // will exit early if the reference has changed. - let canChange = false; - const token = {}; - const intervalCallback = async () => { - this._timerId = null; - - let text = null; - try { - text = await this._clipboardReader.getText(); - } catch (e) { - // NOP - } - if (this._timerToken !== token) { return; } - - if ( - typeof text === 'string' && - (text = text.trim()).length > 0 && - text !== this._previousText - ) { - this._previousText = text; - if (canChange && this._japaneseUtil.isStringPartiallyJapanese(text)) { - this.trigger('change', {text}); - } - } - - canChange = true; - this._timerId = setTimeout(intervalCallback, this._interval); - }; - - this._timerToken = token; - - intervalCallback(); - } - - stop() { - this._timerToken = null; - this._previousText = null; - if (this._timerId !== null) { - clearTimeout(this._timerId); - this._timerId = null; - } - } - - setPreviousText(text) { - this._previousText = text; - } -} diff --git a/ext/bg/js/clipboard-reader.js b/ext/bg/js/clipboard-reader.js deleted file mode 100644 index 275c2d60..00000000 --- a/ext/bg/js/clipboard-reader.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/** - * Class which can read text and images from the clipboard. - */ -class ClipboardReader { - /** - * Creates a new instances of a clipboard reader. - * @param document The Document object to be used, or null for no support. - * @param pasteTargetSelector The selector for the paste target element. - * @param imagePasteTargetSelector The selector for the image paste target element. - */ - constructor({document=null, pasteTargetSelector=null, imagePasteTargetSelector=null, mediaUtility=null}) { - this._document = document; - this._browser = null; - this._pasteTarget = null; - this._pasteTargetSelector = pasteTargetSelector; - this._imagePasteTarget = null; - this._imagePasteTargetSelector = imagePasteTargetSelector; - this._mediaUtility = mediaUtility; - } - - /** - * Gets the browser being used. - */ - get browser() { - return this._browser; - } - - /** - * Assigns the browser being used. - */ - set browser(value) { - this._browser = value; - } - - /** - * Gets the text in the clipboard. - * @returns A string containing the clipboard text. - * @throws Error if not supported. - */ - async getText() { - /* - Notes: - document.execCommand('paste') doesn't work on Firefox. - See: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985 - Therefore, navigator.clipboard.readText() is used on Firefox. - - navigator.clipboard.readText() can't be used in Chrome for two reasons: - * Requires page to be focused, else it rejects with an exception. - * When the page is focused, Chrome will request clipboard permission, despite already - being an extension with clipboard permissions. It effectively asks for the - non-extension permission for clipboard access. - */ - if (this._isFirefox()) { - try { - return await navigator.clipboard.readText(); - } catch (e) { - // Error is undefined, due to permissions - throw new Error('Cannot read clipboard text; check extension permissions'); - } - } - - const document = this._document; - if (document === null) { - throw new Error('Clipboard reading not supported in this context'); - } - - let target = this._pasteTarget; - if (target === null) { - target = document.querySelector(this._pasteTargetSelector); - if (target === null) { - throw new Error('Clipboard paste target does not exist'); - } - this._pasteTarget = target; - } - - target.value = ''; - target.focus(); - document.execCommand('paste'); - const result = target.value; - target.value = ''; - return (typeof result === 'string' ? result : ''); - } - - /** - * Gets the first image in the clipboard. - * @returns A string containing a data URL of the image file, or null if no image was found. - * @throws Error if not supported. - */ - async getImage() { - // See browser-specific notes in getText - if ( - this._isFirefox() && - this._mediaUtility !== null && - typeof navigator.clipboard !== 'undefined' && - typeof navigator.clipboard.read === 'function' - ) { - // This function is behind the Firefox flag: dom.events.asyncClipboard.dataTransfer - let files; - try { - ({files} = await navigator.clipboard.read()); - } catch (e) { - return null; - } - - for (const file of files) { - if (this._mediaUtility.getFileExtensionFromImageMediaType(file.type) !== null) { - return await this._readFileAsDataURL(file); - } - } - return null; - } - - const document = this._document; - if (document === null) { - throw new Error('Clipboard reading not supported in this context'); - } - - let target = this._imagePasteTarget; - if (target === null) { - target = document.querySelector(this._imagePasteTargetSelector); - if (target === null) { - throw new Error('Clipboard paste target does not exist'); - } - this._imagePasteTarget = target; - } - - target.focus(); - document.execCommand('paste'); - const image = target.querySelector('img[src^="data:"]'); - const result = (image !== null ? image.getAttribute('src') : null); - for (const image2 of target.querySelectorAll('img')) { - image2.removeAttribute('src'); - } - target.textContent = ''; - return result; - } - - // Private - - _isFirefox() { - return (this._browser === 'firefox' || this._browser === 'firefox-mobile'); - } - - _readFileAsDataURL(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = () => reject(reader.error); - reader.readAsDataURL(file); - }); - } -} diff --git a/ext/bg/js/context-main.js b/ext/bg/js/context-main.js deleted file mode 100644 index 5cc56745..00000000 --- a/ext/bg/js/context-main.js +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (C) 2017-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * HotkeyHelpController - * PermissionsUtil - * api - */ - -class DisplayController { - constructor() { - this._optionsFull = null; - this._permissionsUtil = new PermissionsUtil(); - } - - async prepare() { - const manifest = chrome.runtime.getManifest(); - - this._showExtensionInfo(manifest); - this._setupEnvironment(); - this._setupButtonEvents('.action-open-search', 'openSearchPage', chrome.runtime.getURL('/search.html')); - this._setupButtonEvents('.action-open-info', 'openInfoPage', chrome.runtime.getURL('/info.html')); - - const optionsFull = await api.optionsGetFull(); - this._optionsFull = optionsFull; - - this._setupHotkeys(); - - const optionsPageUrl = optionsFull.global.useSettingsV2 ? manifest.options_ui.page : '/settings-old.html'; - this._setupButtonEvents('.action-open-settings', 'openSettingsPage', chrome.runtime.getURL(optionsPageUrl)); - this._setupButtonEvents('.action-open-permissions', null, chrome.runtime.getURL('/permissions.html')); - - const {profiles, profileCurrent} = optionsFull; - const primaryProfile = (profileCurrent >= 0 && profileCurrent < profiles.length) ? profiles[profileCurrent] : null; - if (primaryProfile !== null) { - this._setupOptions(primaryProfile); - } - - document.querySelector('.action-select-profile').hidden = (profiles.length <= 1); - - this._updateProfileSelect(profiles, profileCurrent); - - setTimeout(() => { - document.body.dataset.loaded = 'true'; - }, 10); - } - - // Private - - _showExtensionInfo(manifest) { - const node = document.getElementById('extension-info'); - if (node === null) { return; } - - node.textContent = `${manifest.name} v${manifest.version}`; - } - - _setupButtonEvents(selector, command, url) { - const nodes = document.querySelectorAll(selector); - for (const node of nodes) { - if (typeof command === 'string') { - node.addEventListener('click', (e) => { - if (e.button !== 0) { return; } - api.commandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'}); - e.preventDefault(); - }, false); - node.addEventListener('auxclick', (e) => { - if (e.button !== 1) { return; } - api.commandExec(command, {mode: 'newTab'}); - e.preventDefault(); - }, false); - } - - if (typeof url === 'string') { - node.href = url; - node.target = '_blank'; - node.rel = 'noopener'; - } - } - } - - async _setupEnvironment() { - const urlSearchParams = new URLSearchParams(location.search); - let mode = urlSearchParams.get('mode'); - switch (mode) { - case 'full': - case 'mini': - break; - default: - { - let tab; - try { - tab = await this._getCurrentTab(); - } catch (e) { - // NOP - } - mode = (tab ? 'full' : 'mini'); - } - break; - } - - document.documentElement.dataset.mode = mode; - } - - _getCurrentTab() { - return new Promise((resolve, reject) => { - chrome.tabs.getCurrent((result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - }); - }); - } - - _setupOptions({options}) { - const extensionEnabled = options.general.enable; - const onToggleChanged = () => api.commandExec('toggleTextScanning'); - for (const toggle of document.querySelectorAll('#enable-search,#enable-search2')) { - toggle.checked = extensionEnabled; - toggle.addEventListener('change', onToggleChanged, false); - } - this._updateDictionariesEnabledWarnings(options); - this._updatePermissionsWarnings(options); - } - - async _setupHotkeys() { - const hotkeyHelpController = new HotkeyHelpController(); - await hotkeyHelpController.prepare(); - - const {profiles, profileCurrent} = this._optionsFull; - const primaryProfile = (profileCurrent >= 0 && profileCurrent < profiles.length) ? profiles[profileCurrent] : null; - if (primaryProfile !== null) { - hotkeyHelpController.setOptions(primaryProfile.options); - } - - hotkeyHelpController.setupNode(document.documentElement); - } - - _updateProfileSelect(profiles, profileCurrent) { - const select = document.querySelector('#profile-select'); - const optionGroup = document.querySelector('#profile-select-option-group'); - const fragment = document.createDocumentFragment(); - for (let i = 0, ii = profiles.length; i < ii; ++i) { - const {name} = profiles[i]; - const option = document.createElement('option'); - option.textContent = name; - option.value = `${i}`; - fragment.appendChild(option); - } - optionGroup.textContent = ''; - optionGroup.appendChild(fragment); - select.value = `${profileCurrent}`; - - select.addEventListener('change', this._onProfileSelectChange.bind(this), false); - } - - _onProfileSelectChange(e) { - const value = parseInt(e.currentTarget.value, 10); - if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= this._optionsFull.profiles.length) { - this._setPrimaryProfileIndex(value); - } - } - - async _setPrimaryProfileIndex(value) { - return await api.modifySettings( - [{ - action: 'set', - path: 'profileCurrent', - value, - scope: 'global' - }] - ); - } - - async _updateDictionariesEnabledWarnings(options) { - const noDictionariesEnabledWarnings = document.querySelectorAll('.no-dictionaries-enabled-warning'); - const dictionaries = await api.getDictionaryInfo(); - - let enabledCount = 0; - for (const {title} of dictionaries) { - if ( - Object.prototype.hasOwnProperty.call(options.dictionaries, title) && - options.dictionaries[title].enabled - ) { - ++enabledCount; - } - } - - const hasEnabledDictionary = (enabledCount > 0); - for (const node of noDictionariesEnabledWarnings) { - node.hidden = hasEnabledDictionary; - } - } - - async _updatePermissionsWarnings(options) { - const permissions = await this._permissionsUtil.getAllPermissions(); - if (this._permissionsUtil.hasRequiredPermissionsForOptions(permissions, options)) { return; } - - const warnings = document.querySelectorAll('.action-open-permissions,.permissions-required-warning'); - for (const node of warnings) { - console.log(node); - node.hidden = false; - } - } -} - -(async () => { - api.forwardLogsToBackend(); - await yomichan.backendReady(); - - api.logIndicatorClear(); - - const displayController = new DisplayController(); - displayController.prepare(); - - yomichan.ready(); -})(); diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js deleted file mode 100644 index 068f4a5f..00000000 --- a/ext/bg/js/database.js +++ /dev/null @@ -1,327 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class Database { - constructor() { - this._db = null; - this._isOpening = false; - } - - // Public - - async open(databaseName, version, structure) { - if (this._db !== null) { - throw new Error('Database already open'); - } - if (this._isOpening) { - throw new Error('Already opening'); - } - - try { - this._isOpening = true; - this._db = await this._open(databaseName, version, (db, transaction, oldVersion) => { - this._upgrade(db, transaction, oldVersion, structure); - }); - } finally { - this._isOpening = false; - } - } - - close() { - if (this._db === null) { - throw new Error('Database is not open'); - } - - this._db.close(); - this._db = null; - } - - isOpening() { - return this._isOpening; - } - - isOpen() { - return this._db !== null; - } - - transaction(storeNames, mode) { - if (this._db === null) { - throw new Error(this._isOpening ? 'Database not ready' : 'Database not open'); - } - return this._db.transaction(storeNames, mode); - } - - bulkAdd(objectStoreName, items, start, count) { - return new Promise((resolve, reject) => { - if (start + count > items.length) { - count = items.length - start; - } - - if (count <= 0) { - resolve(); - return; - } - - const end = start + count; - let completedCount = 0; - const onError = (e) => reject(e.target.error); - const onSuccess = () => { - if (++completedCount >= count) { - resolve(); - } - }; - - const transaction = this.transaction([objectStoreName], 'readwrite'); - const objectStore = transaction.objectStore(objectStoreName); - for (let i = start; i < end; ++i) { - const request = objectStore.add(items[i]); - request.onerror = onError; - request.onsuccess = onSuccess; - } - }); - } - - getAll(objectStoreOrIndex, query, resolve, reject) { - if (typeof objectStoreOrIndex.getAll === 'function') { - this._getAllFast(objectStoreOrIndex, query, resolve, reject); - } else { - this._getAllUsingCursor(objectStoreOrIndex, query, resolve, reject); - } - } - - getAllKeys(objectStoreOrIndex, query, resolve, reject) { - if (typeof objectStoreOrIndex.getAll === 'function') { - this._getAllKeysFast(objectStoreOrIndex, query, resolve, reject); - } else { - this._getAllKeysUsingCursor(objectStoreOrIndex, query, resolve, reject); - } - } - - find(objectStoreName, indexName, query, predicate=null, defaultValue) { - return new Promise((resolve, reject) => { - const transaction = this.transaction([objectStoreName], 'readonly'); - const objectStore = transaction.objectStore(objectStoreName); - const objectStoreOrIndex = indexName !== null ? objectStore.index(indexName) : objectStore; - const request = objectStoreOrIndex.openCursor(query, 'next'); - request.onerror = (e) => reject(e.target.error); - request.onsuccess = (e) => { - const cursor = e.target.result; - if (cursor) { - const value = cursor.value; - if (typeof predicate !== 'function' || predicate(value)) { - resolve(value); - } else { - cursor.continue(); - } - } else { - resolve(defaultValue); - } - }; - }); - } - - bulkCount(targets, resolve, reject) { - const targetCount = targets.length; - if (targetCount <= 0) { - resolve(); - return; - } - - let completedCount = 0; - const results = new Array(targetCount).fill(null); - - const onError = (e) => reject(e.target.error); - const onSuccess = (e, index) => { - const count = e.target.result; - results[index] = count; - if (++completedCount >= targetCount) { - resolve(results); - } - }; - - for (let i = 0; i < targetCount; ++i) { - const index = i; - const [objectStoreOrIndex, query] = targets[i]; - const request = objectStoreOrIndex.count(query); - request.onerror = onError; - request.onsuccess = (e) => onSuccess(e, index); - } - } - - delete(objectStoreName, key) { - return new Promise((resolve, reject) => { - const transaction = this.transaction([objectStoreName], 'readwrite'); - const objectStore = transaction.objectStore(objectStoreName); - const request = objectStore.delete(key); - request.onerror = (e) => reject(e.target.error); - request.onsuccess = () => resolve(); - }); - } - - bulkDelete(objectStoreName, indexName, query, filterKeys=null, onProgress=null) { - return new Promise((resolve, reject) => { - const transaction = this.transaction([objectStoreName], 'readwrite'); - const objectStore = transaction.objectStore(objectStoreName); - const objectStoreOrIndex = indexName !== null ? objectStore.index(indexName) : objectStore; - - const onGetKeys = (keys) => { - try { - if (typeof filterKeys === 'function') { - keys = filterKeys(keys); - } - this._bulkDeleteInternal(objectStore, keys, onProgress, resolve, reject); - } catch (e) { - reject(e); - } - }; - - this.getAllKeys(objectStoreOrIndex, query, onGetKeys, reject); - }); - } - - static deleteDatabase(databaseName) { - return new Promise((resolve, reject) => { - const request = indexedDB.deleteDatabase(databaseName); - request.onerror = (e) => reject(e.target.error); - request.onsuccess = () => resolve(); - request.onblocked = () => reject(new Error('Database deletion blocked')); - }); - } - - // Private - - _open(name, version, onUpgradeNeeded) { - return new Promise((resolve, reject) => { - const request = indexedDB.open(name, version); - - request.onupgradeneeded = (event) => { - try { - request.transaction.onerror = (e) => reject(e.target.error); - onUpgradeNeeded(request.result, request.transaction, event.oldVersion, event.newVersion); - } catch (e) { - reject(e); - } - }; - - request.onerror = (e) => reject(e.target.error); - request.onsuccess = () => resolve(request.result); - }); - } - - _upgrade(db, transaction, oldVersion, upgrades) { - for (const {version, stores} of upgrades) { - if (oldVersion >= version) { continue; } - - for (const [objectStoreName, {primaryKey, indices}] of Object.entries(stores)) { - const existingObjectStoreNames = transaction.objectStoreNames || db.objectStoreNames; - const objectStore = ( - this._listContains(existingObjectStoreNames, objectStoreName) ? - transaction.objectStore(objectStoreName) : - db.createObjectStore(objectStoreName, primaryKey) - ); - const existingIndexNames = objectStore.indexNames; - - for (const indexName of indices) { - if (this._listContains(existingIndexNames, indexName)) { continue; } - - objectStore.createIndex(indexName, indexName, {}); - } - } - } - } - - _listContains(list, value) { - for (let i = 0, ii = list.length; i < ii; ++i) { - if (list[i] === value) { return true; } - } - return false; - } - - _getAllFast(objectStoreOrIndex, query, resolve, reject) { - const request = objectStoreOrIndex.getAll(query); - request.onerror = (e) => reject(e.target.error); - request.onsuccess = (e) => resolve(e.target.result); - } - - _getAllUsingCursor(objectStoreOrIndex, query, resolve, reject) { - const results = []; - const request = objectStoreOrIndex.openCursor(query, 'next'); - request.onerror = (e) => reject(e.target.error); - request.onsuccess = (e) => { - const cursor = e.target.result; - if (cursor) { - results.push(cursor.value); - cursor.continue(); - } else { - resolve(results); - } - }; - } - - _getAllKeysFast(objectStoreOrIndex, query, resolve, reject) { - const request = objectStoreOrIndex.getAllKeys(query); - request.onerror = (e) => reject(e.target.error); - request.onsuccess = (e) => resolve(e.target.result); - } - - _getAllKeysUsingCursor(objectStoreOrIndex, query, resolve, reject) { - const results = []; - const request = objectStoreOrIndex.openKeyCursor(query, 'next'); - request.onerror = (e) => reject(e.target.error); - request.onsuccess = (e) => { - const cursor = e.target.result; - if (cursor) { - results.push(cursor.primaryKey); - cursor.continue(); - } else { - resolve(results); - } - }; - } - - _bulkDeleteInternal(objectStore, keys, onProgress, resolve, reject) { - const count = keys.length; - if (count === 0) { - resolve(); - return; - } - - let completedCount = 0; - const hasProgress = (typeof onProgress === 'function'); - - const onError = (e) => reject(e.target.error); - const onSuccess = () => { - ++completedCount; - if (hasProgress) { - try { - onProgress(completedCount, count); - } catch (e) { - // NOP - } - } - if (completedCount >= count) { - resolve(); - } - }; - - for (const key of keys) { - const request = objectStore.delete(key); - request.onerror = onError; - request.onsuccess = onSuccess; - } - } -} diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js deleted file mode 100644 index 8fee3f01..00000000 --- a/ext/bg/js/deinflector.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2016-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - - -class Deinflector { - constructor(reasons) { - this.reasons = Deinflector.normalizeReasons(reasons); - } - - deinflect(source, rawSource) { - const results = [{ - source, - rawSource, - term: source, - rules: 0, - reasons: [], - databaseDefinitions: [] - }]; - for (let i = 0; i < results.length; ++i) { - const {rules, term, reasons} = results[i]; - for (const [reason, variants] of this.reasons) { - for (const [kanaIn, kanaOut, rulesIn, rulesOut] of variants) { - if ( - (rules !== 0 && (rules & rulesIn) === 0) || - !term.endsWith(kanaIn) || - (term.length - kanaIn.length + kanaOut.length) <= 0 - ) { - continue; - } - - results.push({ - source, - rawSource, - term: term.substring(0, term.length - kanaIn.length) + kanaOut, - rules: rulesOut, - reasons: [reason, ...reasons], - databaseDefinitions: [] - }); - } - } - } - return results; - } - - static normalizeReasons(reasons) { - const normalizedReasons = []; - for (const [reason, reasonInfo] of Object.entries(reasons)) { - const variants = []; - for (const {kanaIn, kanaOut, rulesIn, rulesOut} of reasonInfo) { - variants.push([ - kanaIn, - kanaOut, - Deinflector.rulesToRuleFlags(rulesIn), - Deinflector.rulesToRuleFlags(rulesOut) - ]); - } - normalizedReasons.push([reason, variants]); - } - return normalizedReasons; - } - - static rulesToRuleFlags(rules) { - const ruleTypes = Deinflector.ruleTypes; - let value = 0; - for (const rule of rules) { - const ruleBits = ruleTypes.get(rule); - if (typeof ruleBits === 'undefined') { continue; } - value |= ruleBits; - } - return value; - } -} - -Deinflector.ruleTypes = new Map([ - ['v1', 0b00000001], // Verb ichidan - ['v5', 0b00000010], // Verb godan - ['vs', 0b00000100], // Verb suru - ['vk', 0b00001000], // Verb kuru - ['vz', 0b00010000], // Verb zuru - ['adj-i', 0b00100000], // Adjective i - ['iru', 0b01000000] // Intermediate -iru endings for progressive or perfect tense -]); diff --git a/ext/bg/js/dictionary-database.js b/ext/bg/js/dictionary-database.js deleted file mode 100644 index b363ed25..00000000 --- a/ext/bg/js/dictionary-database.js +++ /dev/null @@ -1,484 +0,0 @@ -/* - * Copyright (C) 2016-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * Database - */ - -class DictionaryDatabase { - constructor() { - this._db = new Database(); - this._dbName = 'dict'; - this._schemas = new Map(); - } - - // Public - - async prepare() { - await this._db.open( - this._dbName, - 60, - [ - { - version: 20, - stores: { - terms: { - primaryKey: {keyPath: 'id', autoIncrement: true}, - indices: ['dictionary', 'expression', 'reading'] - }, - kanji: { - primaryKey: {autoIncrement: true}, - indices: ['dictionary', 'character'] - }, - tagMeta: { - primaryKey: {autoIncrement: true}, - indices: ['dictionary'] - }, - dictionaries: { - primaryKey: {autoIncrement: true}, - indices: ['title', 'version'] - } - } - }, - { - version: 30, - stores: { - termMeta: { - primaryKey: {autoIncrement: true}, - indices: ['dictionary', 'expression'] - }, - kanjiMeta: { - primaryKey: {autoIncrement: true}, - indices: ['dictionary', 'character'] - }, - tagMeta: { - primaryKey: {autoIncrement: true}, - indices: ['dictionary', 'name'] - } - } - }, - { - version: 40, - stores: { - terms: { - primaryKey: {keyPath: 'id', autoIncrement: true}, - indices: ['dictionary', 'expression', 'reading', 'sequence'] - } - } - }, - { - version: 50, - stores: { - terms: { - primaryKey: {keyPath: 'id', autoIncrement: true}, - indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse'] - } - } - }, - { - version: 60, - stores: { - media: { - primaryKey: {keyPath: 'id', autoIncrement: true}, - indices: ['dictionary', 'path'] - } - } - } - ] - ); - } - - async close() { - this._db.close(); - } - - isPrepared() { - return this._db.isOpen(); - } - - async purge() { - if (this._db.isOpening()) { - throw new Error('Cannot purge database while opening'); - } - if (this._db.isOpen()) { - this._db.close(); - } - let result = false; - try { - await Database.deleteDatabase(this._dbName); - result = true; - } catch (e) { - yomichan.logError(e); - } - await this.prepare(); - return result; - } - - async deleteDictionary(dictionaryName, progressSettings, onProgress) { - const targets = [ - ['dictionaries', 'title'], - ['kanji', 'dictionary'], - ['kanjiMeta', 'dictionary'], - ['terms', 'dictionary'], - ['termMeta', 'dictionary'], - ['tagMeta', 'dictionary'], - ['media', 'dictionary'] - ]; - - const {rate} = progressSettings; - const progressData = { - count: 0, - processed: 0, - storeCount: targets.length, - storesProcesed: 0 - }; - - const filterKeys = (keys) => { - ++progressData.storesProcesed; - progressData.count += keys.length; - onProgress(progressData); - return keys; - }; - const onProgress2 = () => { - const processed = progressData.processed + 1; - progressData.processed = processed; - if ((processed % rate) === 0 || processed === progressData.count) { - onProgress(progressData); - } - }; - - const promises = []; - for (const [objectStoreName, indexName] of targets) { - const query = IDBKeyRange.only(dictionaryName); - const promise = this._db.bulkDelete(objectStoreName, indexName, query, filterKeys, onProgress2); - promises.push(promise); - } - await Promise.all(promises); - } - - findTermsBulk(termList, dictionaries, wildcard) { - return new Promise((resolve, reject) => { - const results = []; - const count = termList.length; - if (count === 0) { - resolve(results); - return; - } - - const visited = new Set(); - const useWildcard = !!wildcard; - const prefixWildcard = wildcard === 'prefix'; - - const transaction = this._db.transaction(['terms'], 'readonly'); - const terms = transaction.objectStore('terms'); - const index1 = terms.index(prefixWildcard ? 'expressionReverse' : 'expression'); - const index2 = terms.index(prefixWildcard ? 'readingReverse' : 'reading'); - - const count2 = count * 2; - let completeCount = 0; - for (let i = 0; i < count; ++i) { - const inputIndex = i; - const term = prefixWildcard ? stringReverse(termList[i]) : termList[i]; - const query = useWildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term); - - const onGetAll = (rows) => { - for (const row of rows) { - if (dictionaries.has(row.dictionary) && !visited.has(row.id)) { - visited.add(row.id); - results.push(this._createTerm(row, inputIndex)); - } - } - if (++completeCount >= count2) { - resolve(results); - } - }; - - this._db.getAll(index1, query, onGetAll, reject); - this._db.getAll(index2, query, onGetAll, reject); - } - }); - } - - findTermsExactBulk(termList, readingList, dictionaries) { - return new Promise((resolve, reject) => { - const results = []; - const count = termList.length; - if (count === 0) { - resolve(results); - return; - } - - const transaction = this._db.transaction(['terms'], 'readonly'); - const terms = transaction.objectStore('terms'); - const index = terms.index('expression'); - - let completeCount = 0; - for (let i = 0; i < count; ++i) { - const inputIndex = i; - const reading = readingList[i]; - const query = IDBKeyRange.only(termList[i]); - - const onGetAll = (rows) => { - for (const row of rows) { - if (row.reading === reading && dictionaries.has(row.dictionary)) { - results.push(this._createTerm(row, inputIndex)); - } - } - if (++completeCount >= count) { - resolve(results); - } - }; - - this._db.getAll(index, query, onGetAll, reject); - } - }); - } - - findTermsBySequenceBulk(sequenceList, mainDictionary) { - return new Promise((resolve, reject) => { - const results = []; - const count = sequenceList.length; - if (count === 0) { - resolve(results); - return; - } - - const transaction = this._db.transaction(['terms'], 'readonly'); - const terms = transaction.objectStore('terms'); - const index = terms.index('sequence'); - - let completeCount = 0; - for (let i = 0; i < count; ++i) { - const inputIndex = i; - const query = IDBKeyRange.only(sequenceList[i]); - - const onGetAll = (rows) => { - for (const row of rows) { - if (row.dictionary === mainDictionary) { - results.push(this._createTerm(row, inputIndex)); - } - } - if (++completeCount >= count) { - resolve(results); - } - }; - - this._db.getAll(index, query, onGetAll, reject); - } - }); - } - - findTermMetaBulk(termList, dictionaries) { - return this._findGenericBulk('termMeta', 'expression', termList, dictionaries, this._createTermMeta.bind(this)); - } - - findKanjiBulk(kanjiList, dictionaries) { - return this._findGenericBulk('kanji', 'character', kanjiList, dictionaries, this._createKanji.bind(this)); - } - - findKanjiMetaBulk(kanjiList, dictionaries) { - return this._findGenericBulk('kanjiMeta', 'character', kanjiList, dictionaries, this._createKanjiMeta.bind(this)); - } - - findTagForTitle(name, title) { - const query = IDBKeyRange.only(name); - return this._db.find('tagMeta', 'name', query, (row) => (row.dictionary === title), null); - } - - getMedia(targets) { - return new Promise((resolve, reject) => { - const count = targets.length; - const results = new Array(count).fill(null); - if (count === 0) { - resolve(results); - return; - } - - let completeCount = 0; - const transaction = this._db.transaction(['media'], 'readonly'); - const objectStore = transaction.objectStore('media'); - const index = objectStore.index('path'); - - for (let i = 0; i < count; ++i) { - const inputIndex = i; - const {path, dictionaryName} = targets[i]; - const query = IDBKeyRange.only(path); - - const onGetAll = (rows) => { - for (const row of rows) { - if (row.dictionary !== dictionaryName) { continue; } - results[inputIndex] = this._createMedia(row, inputIndex); - } - if (++completeCount >= count) { - resolve(results); - } - }; - - this._db.getAll(index, query, onGetAll, reject); - } - }); - } - - getDictionaryInfo() { - return new Promise((resolve, reject) => { - const transaction = this._db.transaction(['dictionaries'], 'readonly'); - const objectStore = transaction.objectStore('dictionaries'); - this._db.getAll(objectStore, null, resolve, reject); - }); - } - - getDictionaryCounts(dictionaryNames, getTotal) { - return new Promise((resolve, reject) => { - const targets = [ - ['kanji', 'dictionary'], - ['kanjiMeta', 'dictionary'], - ['terms', 'dictionary'], - ['termMeta', 'dictionary'], - ['tagMeta', 'dictionary'], - ['media', 'dictionary'] - ]; - const objectStoreNames = targets.map(([objectStoreName]) => objectStoreName); - const transaction = this._db.transaction(objectStoreNames, 'readonly'); - const databaseTargets = targets.map(([objectStoreName, indexName]) => { - const objectStore = transaction.objectStore(objectStoreName); - const index = objectStore.index(indexName); - return {objectStore, index}; - }); - - const countTargets = []; - if (getTotal) { - for (const {objectStore} of databaseTargets) { - countTargets.push([objectStore, null]); - } - } - for (const dictionaryName of dictionaryNames) { - const query = IDBKeyRange.only(dictionaryName); - for (const {index} of databaseTargets) { - countTargets.push([index, query]); - } - } - - const onCountComplete = (results) => { - const resultCount = results.length; - const targetCount = targets.length; - const counts = []; - for (let i = 0; i < resultCount; i += targetCount) { - const countGroup = {}; - for (let j = 0; j < targetCount; ++j) { - countGroup[targets[j][0]] = results[i + j]; - } - counts.push(countGroup); - } - const total = getTotal ? counts.shift() : null; - resolve({total, counts}); - }; - - this._db.bulkCount(countTargets, onCountComplete, reject); - }); - } - - async dictionaryExists(title) { - const query = IDBKeyRange.only(title); - const result = await this._db.find('dictionaries', 'title', query); - return typeof result !== 'undefined'; - } - - bulkAdd(objectStoreName, items, start, count) { - return this._db.bulkAdd(objectStoreName, items, start, count); - } - - // Private - - async _findGenericBulk(objectStoreName, indexName, indexValueList, dictionaries, createResult) { - return new Promise((resolve, reject) => { - const results = []; - const count = indexValueList.length; - if (count === 0) { - resolve(results); - return; - } - - const transaction = this._db.transaction([objectStoreName], 'readonly'); - const terms = transaction.objectStore(objectStoreName); - const index = terms.index(indexName); - - let completeCount = 0; - for (let i = 0; i < count; ++i) { - const inputIndex = i; - const query = IDBKeyRange.only(indexValueList[i]); - - const onGetAll = (rows) => { - for (const row of rows) { - if (dictionaries.has(row.dictionary)) { - results.push(createResult(row, inputIndex)); - } - } - if (++completeCount >= count) { - resolve(results); - } - }; - - this._db.getAll(index, query, onGetAll, reject); - } - }); - } - - _createTerm(row, index) { - return { - index, - expression: row.expression, - reading: row.reading, - definitionTags: this._splitField(row.definitionTags || row.tags || ''), - termTags: this._splitField(row.termTags || ''), - rules: this._splitField(row.rules), - glossary: row.glossary, - score: row.score, - dictionary: row.dictionary, - id: row.id, - sequence: typeof row.sequence === 'undefined' ? -1 : row.sequence - }; - } - - _createKanji(row, index) { - return { - index, - character: row.character, - onyomi: this._splitField(row.onyomi), - kunyomi: this._splitField(row.kunyomi), - tags: this._splitField(row.tags), - glossary: row.meanings, - stats: row.stats, - dictionary: row.dictionary - }; - } - - _createTermMeta({expression, mode, data, dictionary}, index) { - return {expression, mode, data, dictionary, index}; - } - - _createKanjiMeta({character, mode, data, dictionary}, index) { - return {character, mode, data, dictionary, index}; - } - - _createMedia(row, index) { - return Object.assign({}, row, {index}); - } - - _splitField(field) { - return field.length === 0 ? [] : field.split(' '); - } -} diff --git a/ext/bg/js/dictionary-importer.js b/ext/bg/js/dictionary-importer.js deleted file mode 100644 index 4cb608db..00000000 --- a/ext/bg/js/dictionary-importer.js +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * JSZip - * JsonSchemaValidator - * MediaUtility - */ - -class DictionaryImporter { - constructor() { - this._schemas = new Map(); - this._jsonSchemaValidator = new JsonSchemaValidator(); - this._mediaUtility = new MediaUtility(); - } - - async importDictionary(dictionaryDatabase, archiveSource, details, onProgress) { - if (!dictionaryDatabase) { - throw new Error('Invalid database'); - } - if (!dictionaryDatabase.isPrepared()) { - throw new Error('Database is not ready'); - } - - const hasOnProgress = (typeof onProgress === 'function'); - - // Read archive - const archive = await JSZip.loadAsync(archiveSource); - - // Read and validate index - const indexFileName = 'index.json'; - const indexFile = archive.files[indexFileName]; - if (!indexFile) { - throw new Error('No dictionary index found in archive'); - } - - const index = JSON.parse(await indexFile.async('string')); - - const indexSchema = await this._getSchema('/data/schemas/dictionary-index-schema.json'); - this._validateJsonSchema(index, indexSchema, indexFileName); - - const dictionaryTitle = index.title; - const version = index.format || index.version; - - if (!dictionaryTitle || !index.revision) { - throw new Error('Unrecognized dictionary format'); - } - - // Verify database is not already imported - if (await dictionaryDatabase.dictionaryExists(dictionaryTitle)) { - throw new Error('Dictionary is already imported'); - } - - // Data format converters - const convertTermBankEntry = (entry) => { - if (version === 1) { - const [expression, reading, definitionTags, rules, score, ...glossary] = entry; - return {expression, reading, definitionTags, rules, score, glossary}; - } else { - const [expression, reading, definitionTags, rules, score, glossary, sequence, termTags] = entry; - return {expression, reading, definitionTags, rules, score, glossary, sequence, termTags}; - } - }; - - const convertTermMetaBankEntry = (entry) => { - const [expression, mode, data] = entry; - return {expression, mode, data}; - }; - - const convertKanjiBankEntry = (entry) => { - if (version === 1) { - const [character, onyomi, kunyomi, tags, ...meanings] = entry; - return {character, onyomi, kunyomi, tags, meanings}; - } else { - const [character, onyomi, kunyomi, tags, meanings, stats] = entry; - return {character, onyomi, kunyomi, tags, meanings, stats}; - } - }; - - const convertKanjiMetaBankEntry = (entry) => { - const [character, mode, data] = entry; - return {character, mode, data}; - }; - - const convertTagBankEntry = (entry) => { - const [name, category, order, notes, score] = entry; - return {name, category, order, notes, score}; - }; - - // Archive file reading - const readFileSequence = async (fileNameFormat, convertEntry, schema) => { - const results = []; - for (let i = 1; true; ++i) { - const fileName = fileNameFormat.replace(/\?/, `${i}`); - const file = archive.files[fileName]; - if (!file) { break; } - - const entries = JSON.parse(await file.async('string')); - this._validateJsonSchema(entries, schema, fileName); - - for (let entry of entries) { - entry = convertEntry(entry); - entry.dictionary = dictionaryTitle; - results.push(entry); - } - } - return results; - }; - - // Load schemas - const dataBankSchemaPaths = this._getDataBankSchemaPaths(version); - const dataBankSchemas = await Promise.all(dataBankSchemaPaths.map((path) => this._getSchema(path))); - - // Load data - const termList = await readFileSequence('term_bank_?.json', convertTermBankEntry, dataBankSchemas[0]); - const termMetaList = await readFileSequence('term_meta_bank_?.json', convertTermMetaBankEntry, dataBankSchemas[1]); - const kanjiList = await readFileSequence('kanji_bank_?.json', convertKanjiBankEntry, dataBankSchemas[2]); - const kanjiMetaList = await readFileSequence('kanji_meta_bank_?.json', convertKanjiMetaBankEntry, dataBankSchemas[3]); - const tagList = await readFileSequence('tag_bank_?.json', convertTagBankEntry, dataBankSchemas[4]); - - // Old tags - const indexTagMeta = index.tagMeta; - if (typeof indexTagMeta === 'object' && indexTagMeta !== null) { - for (const name of Object.keys(indexTagMeta)) { - const {category, order, notes, score} = indexTagMeta[name]; - tagList.push({name, category, order, notes, score}); - } - } - - // Prefix wildcard support - const prefixWildcardsSupported = !!details.prefixWildcardsSupported; - if (prefixWildcardsSupported) { - for (const entry of termList) { - entry.expressionReverse = stringReverse(entry.expression); - entry.readingReverse = stringReverse(entry.reading); - } - } - - // Extended data support - const extendedDataContext = { - archive, - media: new Map() - }; - for (const entry of termList) { - const glossaryList = entry.glossary; - for (let i = 0, ii = glossaryList.length; i < ii; ++i) { - const glossary = glossaryList[i]; - if (typeof glossary !== 'object' || glossary === null) { continue; } - glossaryList[i] = await this._formatDictionaryTermGlossaryObject(glossary, extendedDataContext, entry); - } - } - - const media = [...extendedDataContext.media.values()]; - - // Add dictionary - const summary = this._createSummary(dictionaryTitle, version, index, {prefixWildcardsSupported}); - - dictionaryDatabase.bulkAdd('dictionaries', [summary], 0, 1); - - // Add data - const errors = []; - const total = ( - termList.length + - termMetaList.length + - kanjiList.length + - kanjiMetaList.length + - tagList.length - ); - let loadedCount = 0; - const maxTransactionLength = 1000; - - const bulkAdd = async (objectStoreName, entries) => { - const ii = entries.length; - for (let i = 0; i < ii; i += maxTransactionLength) { - const count = Math.min(maxTransactionLength, ii - i); - - try { - await dictionaryDatabase.bulkAdd(objectStoreName, entries, i, count); - } catch (e) { - errors.push(e); - } - - loadedCount += count; - if (hasOnProgress) { - onProgress(total, loadedCount); - } - } - }; - - await bulkAdd('terms', termList); - await bulkAdd('termMeta', termMetaList); - await bulkAdd('kanji', kanjiList); - await bulkAdd('kanjiMeta', kanjiMetaList); - await bulkAdd('tagMeta', tagList); - await bulkAdd('media', media); - - return {result: summary, errors}; - } - - _createSummary(dictionaryTitle, version, index, details) { - const summary = { - title: dictionaryTitle, - revision: index.revision, - sequenced: index.sequenced, - version - }; - - const {author, url, description, attribution} = index; - if (typeof author === 'string') { summary.author = author; } - if (typeof url === 'string') { summary.url = url; } - if (typeof description === 'string') { summary.description = description; } - if (typeof attribution === 'string') { summary.attribution = attribution; } - - Object.assign(summary, details); - - return summary; - } - - async _getSchema(fileName) { - let schemaPromise = this._schemas.get(fileName); - if (typeof schemaPromise !== 'undefined') { - return schemaPromise; - } - - schemaPromise = this._fetchJsonAsset(fileName); - this._schemas.set(fileName, schemaPromise); - return schemaPromise; - } - - _validateJsonSchema(value, schema, fileName) { - try { - this._jsonSchemaValidator.validate(value, schema); - } catch (e) { - throw this._formatSchemaError(e, fileName); - } - } - - _formatSchemaError(e, fileName) { - const valuePathString = this._getSchemaErrorPathString(e.info.valuePath, 'dictionary'); - const schemaPathString = this._getSchemaErrorPathString(e.info.schemaPath, 'schema'); - - const e2 = new Error(`Dictionary has invalid data in '${fileName}' for value '${valuePathString}', validated against '${schemaPathString}': ${e.message}`); - e2.data = e; - - return e2; - } - - _getSchemaErrorPathString(infoList, base='') { - let result = base; - for (const [part] of infoList) { - switch (typeof part) { - case 'string': - if (result.length > 0) { - result += '.'; - } - result += part; - break; - case 'number': - result += `[${part}]`; - break; - } - } - return result; - } - - _getDataBankSchemaPaths(version) { - const termBank = ( - version === 1 ? - '/data/schemas/dictionary-term-bank-v1-schema.json' : - '/data/schemas/dictionary-term-bank-v3-schema.json' - ); - const termMetaBank = '/data/schemas/dictionary-term-meta-bank-v3-schema.json'; - const kanjiBank = ( - version === 1 ? - '/data/schemas/dictionary-kanji-bank-v1-schema.json' : - '/data/schemas/dictionary-kanji-bank-v3-schema.json' - ); - const kanjiMetaBank = '/data/schemas/dictionary-kanji-meta-bank-v3-schema.json'; - const tagBank = '/data/schemas/dictionary-tag-bank-v3-schema.json'; - - return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank]; - } - - async _formatDictionaryTermGlossaryObject(data, context, entry) { - switch (data.type) { - case 'text': - return data.text; - case 'image': - return await this._formatDictionaryTermGlossaryImage(data, context, entry); - default: - throw new Error(`Unhandled data type: ${data.type}`); - } - } - - async _formatDictionaryTermGlossaryImage(data, context, entry) { - const dictionary = entry.dictionary; - const {path, width: preferredWidth, height: preferredHeight, title, description, pixelated} = data; - if (context.media.has(path)) { - // Already exists - return data; - } - - let errorSource = entry.expression; - if (entry.reading.length > 0) { - errorSource += ` (${entry.reading});`; - } - - const file = context.archive.file(path); - if (file === null) { - throw new Error(`Could not find image at path ${JSON.stringify(path)} for ${errorSource}`); - } - - const content = await file.async('base64'); - const mediaType = this._mediaUtility.getImageMediaTypeFromFileName(path); - if (mediaType === null) { - throw new Error(`Could not determine media type for image at path ${JSON.stringify(path)} for ${errorSource}`); - } - - let image; - try { - image = await this._loadImageBase64(mediaType, content); - } catch (e) { - throw new Error(`Could not load image at path ${JSON.stringify(path)} for ${errorSource}`); - } - - const width = image.naturalWidth; - const height = image.naturalHeight; - - // Create image data - const mediaData = { - dictionary, - path, - mediaType, - width, - height, - content - }; - context.media.set(path, mediaData); - - // Create new data - const newData = { - type: 'image', - path, - width, - height - }; - if (typeof preferredWidth === 'number') { newData.preferredWidth = preferredWidth; } - if (typeof preferredHeight === 'number') { newData.preferredHeight = preferredHeight; } - if (typeof title === 'string') { newData.title = title; } - if (typeof description === 'string') { newData.description = description; } - if (typeof pixelated === 'boolean') { newData.pixelated = pixelated; } - - return newData; - } - - async _fetchJsonAsset(url) { - const response = await fetch(chrome.runtime.getURL(url), { - method: 'GET', - mode: 'no-cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }); - if (!response.ok) { - throw new Error(`Failed to fetch ${url}: ${response.status}`); - } - return await response.json(); - } - - /** - * Attempts to load an image using a base64 encoded content and a media type. - * @param mediaType The media type for the image content. - * @param content The binary content for the image, encoded in base64. - * @returns A Promise which resolves with an HTMLImageElement instance on - * successful load, otherwise an error is thrown. - */ - _loadImageBase64(mediaType, content) { - return new Promise((resolve, reject) => { - const image = new Image(); - const eventListeners = new EventListenerCollection(); - eventListeners.addEventListener(image, 'load', () => { - eventListeners.removeAllEventListeners(); - resolve(image); - }, false); - eventListeners.addEventListener(image, 'error', () => { - eventListeners.removeAllEventListeners(); - reject(new Error('Image failed to load')); - }, false); - image.src = `data:${mediaType};base64,${content}`; - }); - } -} diff --git a/ext/bg/js/generic-page-main.js b/ext/bg/js/generic-page-main.js deleted file mode 100644 index db1a770a..00000000 --- a/ext/bg/js/generic-page-main.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * DocumentFocusController - */ - -function setupEnvironmentInfo() { - const {manifest_version: manifestVersion} = chrome.runtime.getManifest(); - document.documentElement.dataset.manifestVersion = `${manifestVersion}`; -} - -(() => { - const documentFocusController = new DocumentFocusController(); - documentFocusController.prepare(); - document.documentElement.dataset.loaded = 'true'; - setupEnvironmentInfo(); -})(); diff --git a/ext/bg/js/info-main.js b/ext/bg/js/info-main.js deleted file mode 100644 index 6cf82595..00000000 --- a/ext/bg/js/info-main.js +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * BackupController - * DocumentFocusController - * SettingsController - * api - */ - -function getBrowserDisplayName(browser) { - switch (browser) { - case 'chrome': return 'Chrome'; - case 'firefox': return 'Firefox'; - case 'firefox-mobile': return 'Firefox for Android'; - case 'edge': return 'Edge'; - case 'edge-legacy': return 'Edge Legacy'; - default: return `${browser}`; - } -} - -function getOperatingSystemDisplayName(os) { - switch (os) { - case 'mac': return 'Mac OS'; - case 'win': return 'Windows'; - case 'android': return 'Android'; - case 'cros': return 'Chrome OS'; - case 'linux': return 'Linux'; - case 'openbsd': return 'Open BSD'; - case 'unknown': return 'Unknown'; - default: return `${os}`; - } -} - -(async () => { - try { - const documentFocusController = new DocumentFocusController(); - documentFocusController.prepare(); - - const manifest = chrome.runtime.getManifest(); - const language = chrome.i18n.getUILanguage(); - - api.forwardLogsToBackend(); - await yomichan.prepare(); - - const {userAgent} = navigator; - const {name, version} = manifest; - const {browser, platform: {os}} = await api.getEnvironmentInfo(); - - const thisVersionLink = document.querySelector('#release-notes-this-version-link'); - thisVersionLink.href = thisVersionLink.dataset.hrefFormat.replace(/\{version\}/g, version); - - document.querySelector('#version').textContent = `${name} ${version}`; - document.querySelector('#browser').textContent = getBrowserDisplayName(browser); - document.querySelector('#platform').textContent = getOperatingSystemDisplayName(os); - document.querySelector('#language').textContent = `${language}`; - document.querySelector('#user-agent').textContent = userAgent; - - (async () => { - let ankiConnectVersion = null; - try { - ankiConnectVersion = await api.getAnkiConnectVersion(); - } catch (e) { - // NOP - } - - document.querySelector('#anki-connect-version').textContent = (ankiConnectVersion !== null ? `${ankiConnectVersion}` : 'Unknown'); - document.querySelector('#anki-connect-version-container').hasError = `${ankiConnectVersion === null}`; - document.querySelector('#anki-connect-version-unknown-message').hidden = (ankiConnectVersion !== null); - })(); - - (async () => { - let dictionaryInfos; - try { - dictionaryInfos = await api.getDictionaryInfo(); - } catch (e) { - return; - } - - const fragment = document.createDocumentFragment(); - let first = true; - for (const {title} of dictionaryInfos) { - if (first) { - first = false; - } else { - fragment.appendChild(document.createTextNode(', ')); - } - - const node = document.createElement('span'); - node.className = 'installed-dictionary'; - node.textContent = title; - fragment.appendChild(node); - } - - document.querySelector('#installed-dictionaries-none').hidden = (dictionaryInfos.length !== 0); - const container = document.querySelector('#installed-dictionaries'); - container.textContent = ''; - container.appendChild(fragment); - })(); - - const settingsController = new SettingsController(); - settingsController.prepare(); - - const backupController = new BackupController(settingsController, null); - await backupController.prepare(); - - await promiseTimeout(100); - - document.documentElement.dataset.loaded = 'true'; - } catch (e) { - yomichan.logError(e); - } -})(); diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js deleted file mode 100644 index 7b6b9c53..00000000 --- a/ext/bg/js/json-schema.js +++ /dev/null @@ -1,757 +0,0 @@ -/* - * Copyright (C) 2019-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * CacheMap - */ - -class JsonSchemaProxyHandler { - constructor(schema, jsonSchemaValidator) { - this._schema = schema; - this._jsonSchemaValidator = jsonSchemaValidator; - } - - getPrototypeOf(target) { - return Object.getPrototypeOf(target); - } - - setPrototypeOf() { - throw new Error('setPrototypeOf not supported'); - } - - isExtensible(target) { - return Object.isExtensible(target); - } - - preventExtensions(target) { - Object.preventExtensions(target); - return true; - } - - getOwnPropertyDescriptor(target, property) { - return Object.getOwnPropertyDescriptor(target, property); - } - - defineProperty() { - throw new Error('defineProperty not supported'); - } - - has(target, property) { - return property in target; - } - - get(target, property) { - if (typeof property === 'symbol') { - return target[property]; - } - - if (Array.isArray(target)) { - if (typeof property === 'string' && /^\d+$/.test(property)) { - property = parseInt(property, 10); - } else if (typeof property === 'string') { - return target[property]; - } - } - - const propertySchema = this._jsonSchemaValidator.getPropertySchema(this._schema, property, target); - if (propertySchema === null) { - return; - } - - const value = target[property]; - return value !== null && typeof value === 'object' ? this._jsonSchemaValidator.createProxy(value, propertySchema) : value; - } - - set(target, property, value) { - if (Array.isArray(target)) { - if (typeof property === 'string' && /^\d+$/.test(property)) { - property = parseInt(property, 10); - if (property > target.length) { - throw new Error('Array index out of range'); - } - } else if (typeof property === 'string') { - target[property] = value; - return true; - } - } - - const propertySchema = this._jsonSchemaValidator.getPropertySchema(this._schema, property, target); - if (propertySchema === null) { - throw new Error(`Property ${property} not supported`); - } - - value = clone(value); - - this._jsonSchemaValidator.validate(value, propertySchema); - - target[property] = value; - return true; - } - - deleteProperty(target, property) { - const required = this._schema.required; - if (Array.isArray(required) && required.includes(property)) { - throw new Error(`${property} cannot be deleted`); - } - return Reflect.deleteProperty(target, property); - } - - ownKeys(target) { - return Reflect.ownKeys(target); - } - - apply() { - throw new Error('apply not supported'); - } - - construct() { - throw new Error('construct not supported'); - } -} - -class JsonSchemaValidator { - constructor() { - this._regexCache = new CacheMap(100); - } - - createProxy(target, schema) { - return new Proxy(target, new JsonSchemaProxyHandler(schema, this)); - } - - isValid(value, schema) { - try { - this.validate(value, schema); - return true; - } catch (e) { - return false; - } - } - - validate(value, schema) { - const info = new JsonSchemaTraversalInfo(value, schema); - this._validate(value, schema, info); - } - - getValidValueOrDefault(schema, value) { - const info = new JsonSchemaTraversalInfo(value, schema); - return this._getValidValueOrDefault(schema, value, info); - } - - getPropertySchema(schema, property, value) { - return this._getPropertySchema(schema, property, value, null); - } - - clearCache() { - this._regexCache.clear(); - } - - // Private - - _getPropertySchema(schema, property, value, path) { - const type = this._getSchemaOrValueType(schema, value); - switch (type) { - case 'object': - { - const properties = schema.properties; - if (this._isObject(properties)) { - const propertySchema = properties[property]; - if (this._isObject(propertySchema)) { - if (path !== null) { path.push(['properties', properties], [property, propertySchema]); } - return propertySchema; - } - } - - const additionalProperties = schema.additionalProperties; - if (additionalProperties === false) { - return null; - } else if (this._isObject(additionalProperties)) { - if (path !== null) { path.push(['additionalProperties', additionalProperties]); } - return additionalProperties; - } else { - const result = JsonSchemaValidator.unconstrainedSchema; - if (path !== null) { path.push([null, result]); } - return result; - } - } - case 'array': - { - const items = schema.items; - if (this._isObject(items)) { - return items; - } - if (Array.isArray(items)) { - if (property >= 0 && property < items.length) { - const propertySchema = items[property]; - if (this._isObject(propertySchema)) { - if (path !== null) { path.push(['items', items], [property, propertySchema]); } - return propertySchema; - } - } - } - - const additionalItems = schema.additionalItems; - if (additionalItems === false) { - return null; - } else if (this._isObject(additionalItems)) { - if (path !== null) { path.push(['additionalItems', additionalItems]); } - return additionalItems; - } else { - const result = JsonSchemaValidator.unconstrainedSchema; - if (path !== null) { path.push([null, result]); } - return result; - } - } - default: - return null; - } - } - - _getSchemaOrValueType(schema, value) { - const type = schema.type; - - if (Array.isArray(type)) { - if (typeof value !== 'undefined') { - const valueType = this._getValueType(value); - if (type.indexOf(valueType) >= 0) { - return valueType; - } - } - return null; - } - - if (typeof type === 'undefined') { - if (typeof value !== 'undefined') { - return this._getValueType(value); - } - return null; - } - - return type; - } - - _validate(value, schema, info) { - this._validateSingleSchema(value, schema, info); - this._validateConditional(value, schema, info); - this._validateAllOf(value, schema, info); - this._validateAnyOf(value, schema, info); - this._validateOneOf(value, schema, info); - this._validateNoneOf(value, schema, info); - } - - _validateConditional(value, schema, info) { - const ifSchema = schema.if; - if (!this._isObject(ifSchema)) { return; } - - let okay = true; - info.schemaPush('if', ifSchema); - try { - this._validate(value, ifSchema, info); - } catch (e) { - okay = false; - } - info.schemaPop(); - - const nextSchema = okay ? schema.then : schema.else; - if (this._isObject(nextSchema)) { - info.schemaPush(okay ? 'then' : 'else', nextSchema); - this._validate(value, nextSchema, info); - info.schemaPop(); - } - } - - _validateAllOf(value, schema, info) { - const subSchemas = schema.allOf; - if (!Array.isArray(subSchemas)) { return; } - - info.schemaPush('allOf', subSchemas); - for (let i = 0; i < subSchemas.length; ++i) { - const subSchema = subSchemas[i]; - info.schemaPush(i, subSchema); - this._validate(value, subSchema, info); - info.schemaPop(); - } - info.schemaPop(); - } - - _validateAnyOf(value, schema, info) { - const subSchemas = schema.anyOf; - if (!Array.isArray(subSchemas)) { return; } - - info.schemaPush('anyOf', subSchemas); - for (let i = 0; i < subSchemas.length; ++i) { - const subSchema = subSchemas[i]; - info.schemaPush(i, subSchema); - try { - this._validate(value, subSchema, info); - return; - } catch (e) { - // NOP - } - info.schemaPop(); - } - - throw new JsonSchemaValidationError('0 anyOf schemas matched', value, schema, info); - // info.schemaPop(); // Unreachable - } - - _validateOneOf(value, schema, info) { - const subSchemas = schema.oneOf; - if (!Array.isArray(subSchemas)) { return; } - - info.schemaPush('oneOf', subSchemas); - let count = 0; - for (let i = 0; i < subSchemas.length; ++i) { - const subSchema = subSchemas[i]; - info.schemaPush(i, subSchema); - try { - this._validate(value, subSchema, info); - ++count; - } catch (e) { - // NOP - } - info.schemaPop(); - } - - if (count !== 1) { - throw new JsonSchemaValidationError(`${count} oneOf schemas matched`, value, schema, info); - } - - info.schemaPop(); - } - - _validateNoneOf(value, schema, info) { - const subSchemas = schema.not; - if (!Array.isArray(subSchemas)) { return; } - - info.schemaPush('not', subSchemas); - for (let i = 0; i < subSchemas.length; ++i) { - const subSchema = subSchemas[i]; - info.schemaPush(i, subSchema); - try { - this._validate(value, subSchema, info); - } catch (e) { - info.schemaPop(); - continue; - } - throw new JsonSchemaValidationError(`not[${i}] schema matched`, value, schema, info); - } - info.schemaPop(); - } - - _validateSingleSchema(value, schema, info) { - const type = this._getValueType(value); - const schemaType = schema.type; - if (!this._isValueTypeAny(value, type, schemaType)) { - throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema, info); - } - - const schemaConst = schema.const; - if (typeof schemaConst !== 'undefined' && !this._valuesAreEqual(value, schemaConst)) { - throw new JsonSchemaValidationError('Invalid constant value', value, schema, info); - } - - const schemaEnum = schema.enum; - if (Array.isArray(schemaEnum) && !this._valuesAreEqualAny(value, schemaEnum)) { - throw new JsonSchemaValidationError('Invalid enum value', value, schema, info); - } - - switch (type) { - case 'number': - this._validateNumber(value, schema, info); - break; - case 'string': - this._validateString(value, schema, info); - break; - case 'array': - this._validateArray(value, schema, info); - break; - case 'object': - this._validateObject(value, schema, info); - break; - } - } - - _validateNumber(value, schema, info) { - const multipleOf = schema.multipleOf; - if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) { - throw new JsonSchemaValidationError(`Number is not a multiple of ${multipleOf}`, value, schema, info); - } - - const minimum = schema.minimum; - if (typeof minimum === 'number' && value < minimum) { - throw new JsonSchemaValidationError(`Number is less than ${minimum}`, value, schema, info); - } - - const exclusiveMinimum = schema.exclusiveMinimum; - if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) { - throw new JsonSchemaValidationError(`Number is less than or equal to ${exclusiveMinimum}`, value, schema, info); - } - - const maximum = schema.maximum; - if (typeof maximum === 'number' && value > maximum) { - throw new JsonSchemaValidationError(`Number is greater than ${maximum}`, value, schema, info); - } - - const exclusiveMaximum = schema.exclusiveMaximum; - if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) { - throw new JsonSchemaValidationError(`Number is greater than or equal to ${exclusiveMaximum}`, value, schema, info); - } - } - - _validateString(value, schema, info) { - const minLength = schema.minLength; - if (typeof minLength === 'number' && value.length < minLength) { - throw new JsonSchemaValidationError('String length too short', value, schema, info); - } - - const maxLength = schema.maxLength; - if (typeof maxLength === 'number' && value.length > maxLength) { - throw new JsonSchemaValidationError('String length too long', value, schema, info); - } - - const pattern = schema.pattern; - if (typeof pattern === 'string') { - let patternFlags = schema.patternFlags; - if (typeof patternFlags !== 'string') { patternFlags = ''; } - - let regex; - try { - regex = this._getRegex(pattern, patternFlags); - } catch (e) { - throw new JsonSchemaValidationError(`Pattern is invalid (${e.message})`, value, schema, info); - } - - if (!regex.test(value)) { - throw new JsonSchemaValidationError('Pattern match failed', value, schema, info); - } - } - } - - _validateArray(value, schema, info) { - const minItems = schema.minItems; - if (typeof minItems === 'number' && value.length < minItems) { - throw new JsonSchemaValidationError('Array length too short', value, schema, info); - } - - const maxItems = schema.maxItems; - if (typeof maxItems === 'number' && value.length > maxItems) { - throw new JsonSchemaValidationError('Array length too long', value, schema, info); - } - - this._validateArrayContains(value, schema, info); - - for (let i = 0, ii = value.length; i < ii; ++i) { - const schemaPath = []; - const propertySchema = this._getPropertySchema(schema, i, value, schemaPath); - if (propertySchema === null) { - throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema, info); - } - - const propertyValue = value[i]; - - for (const [p, s] of schemaPath) { info.schemaPush(p, s); } - info.valuePush(i, propertyValue); - this._validate(propertyValue, propertySchema, info); - info.valuePop(); - for (let j = 0, jj = schemaPath.length; j < jj; ++j) { info.schemaPop(); } - } - } - - _validateArrayContains(value, schema, info) { - const containsSchema = schema.contains; - if (!this._isObject(containsSchema)) { return; } - - info.schemaPush('contains', containsSchema); - for (let i = 0, ii = value.length; i < ii; ++i) { - const propertyValue = value[i]; - info.valuePush(i, propertyValue); - try { - this._validate(propertyValue, containsSchema, info); - info.schemaPop(); - return; - } catch (e) { - // NOP - } - info.valuePop(); - } - throw new JsonSchemaValidationError('contains schema didn\'t match', value, schema, info); - } - - _validateObject(value, schema, info) { - const properties = new Set(Object.getOwnPropertyNames(value)); - - const required = schema.required; - if (Array.isArray(required)) { - for (const property of required) { - if (!properties.has(property)) { - throw new JsonSchemaValidationError(`Missing property ${property}`, value, schema, info); - } - } - } - - const minProperties = schema.minProperties; - if (typeof minProperties === 'number' && properties.length < minProperties) { - throw new JsonSchemaValidationError('Not enough object properties', value, schema, info); - } - - const maxProperties = schema.maxProperties; - if (typeof maxProperties === 'number' && properties.length > maxProperties) { - throw new JsonSchemaValidationError('Too many object properties', value, schema, info); - } - - for (const property of properties) { - const schemaPath = []; - const propertySchema = this._getPropertySchema(schema, property, value, schemaPath); - if (propertySchema === null) { - throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema, info); - } - - const propertyValue = value[property]; - - for (const [p, s] of schemaPath) { info.schemaPush(p, s); } - info.valuePush(property, propertyValue); - this._validate(propertyValue, propertySchema, info); - info.valuePop(); - for (let i = 0; i < schemaPath.length; ++i) { info.schemaPop(); } - } - } - - _isValueTypeAny(value, type, schemaTypes) { - if (typeof schemaTypes === 'string') { - return this._isValueType(value, type, schemaTypes); - } else if (Array.isArray(schemaTypes)) { - for (const schemaType of schemaTypes) { - if (this._isValueType(value, type, schemaType)) { - return true; - } - } - return false; - } - return true; - } - - _isValueType(value, type, schemaType) { - return ( - type === schemaType || - (schemaType === 'integer' && Math.floor(value) === value) - ); - } - - _getValueType(value) { - const type = typeof value; - if (type === 'object') { - if (value === null) { return 'null'; } - if (Array.isArray(value)) { return 'array'; } - } - return type; - } - - _valuesAreEqualAny(value1, valueList) { - for (const value2 of valueList) { - if (this._valuesAreEqual(value1, value2)) { - return true; - } - } - return false; - } - - _valuesAreEqual(value1, value2) { - return value1 === value2; - } - - _getDefaultTypeValue(type) { - if (typeof type === 'string') { - switch (type) { - case 'null': - return null; - case 'boolean': - return false; - case 'number': - case 'integer': - return 0; - case 'string': - return ''; - case 'array': - return []; - case 'object': - return {}; - } - } - return null; - } - - _getDefaultSchemaValue(schema) { - const schemaType = schema.type; - const schemaDefault = schema.default; - return ( - typeof schemaDefault !== 'undefined' && - this._isValueTypeAny(schemaDefault, this._getValueType(schemaDefault), schemaType) ? - clone(schemaDefault) : - this._getDefaultTypeValue(schemaType) - ); - } - - _getValidValueOrDefault(schema, value, info) { - let type = this._getValueType(value); - if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, schema.type)) { - value = this._getDefaultSchemaValue(schema); - type = this._getValueType(value); - } - - switch (type) { - case 'object': - value = this._populateObjectDefaults(value, schema, info); - break; - case 'array': - value = this._populateArrayDefaults(value, schema, info); - break; - default: - if (!this.isValid(value, schema)) { - const schemaDefault = this._getDefaultSchemaValue(schema); - if (this.isValid(schemaDefault, schema)) { - value = schemaDefault; - } - } - break; - } - - return value; - } - - _populateObjectDefaults(value, schema, info) { - const properties = new Set(Object.getOwnPropertyNames(value)); - - const required = schema.required; - if (Array.isArray(required)) { - for (const property of required) { - properties.delete(property); - - const propertySchema = this._getPropertySchema(schema, property, value, null); - if (propertySchema === null) { continue; } - info.valuePush(property, value); - info.schemaPush(property, propertySchema); - const hasValue = Object.prototype.hasOwnProperty.call(value, property); - value[property] = this._getValidValueOrDefault(propertySchema, hasValue ? value[property] : void 0, info); - info.schemaPop(); - info.valuePop(); - } - } - - for (const property of properties) { - const propertySchema = this._getPropertySchema(schema, property, value, null); - if (propertySchema === null) { - Reflect.deleteProperty(value, property); - } else { - info.valuePush(property, value); - info.schemaPush(property, propertySchema); - value[property] = this._getValidValueOrDefault(propertySchema, value[property], info); - info.schemaPop(); - info.valuePop(); - } - } - - return value; - } - - _populateArrayDefaults(value, schema, info) { - for (let i = 0, ii = value.length; i < ii; ++i) { - const propertySchema = this._getPropertySchema(schema, i, value, null); - if (propertySchema === null) { continue; } - info.valuePush(i, value); - info.schemaPush(i, propertySchema); - value[i] = this._getValidValueOrDefault(propertySchema, value[i], info); - info.schemaPop(); - info.valuePop(); - } - - const minItems = schema.minItems; - if (typeof minItems === 'number' && value.length < minItems) { - for (let i = value.length; i < minItems; ++i) { - const propertySchema = this._getPropertySchema(schema, i, value, null); - if (propertySchema === null) { break; } - info.valuePush(i, value); - info.schemaPush(i, propertySchema); - const item = this._getValidValueOrDefault(propertySchema, void 0, info); - info.schemaPop(); - info.valuePop(); - value.push(item); - } - } - - const maxItems = schema.maxItems; - if (typeof maxItems === 'number' && value.length > maxItems) { - value.splice(maxItems, value.length - maxItems); - } - - return value; - } - - _isObject(value) { - return typeof value === 'object' && value !== null && !Array.isArray(value); - } - - _getRegex(pattern, flags) { - const key = `${flags}:${pattern}`; - let regex = this._regexCache.get(key); - if (typeof regex === 'undefined') { - regex = new RegExp(pattern, flags); - this._regexCache.set(key, regex); - } - return regex; - } -} - -Object.defineProperty(JsonSchemaValidator, 'unconstrainedSchema', { - value: Object.freeze({}), - configurable: false, - enumerable: true, - writable: false -}); - -class JsonSchemaTraversalInfo { - constructor(value, schema) { - this.valuePath = []; - this.schemaPath = []; - this.valuePush(null, value); - this.schemaPush(null, schema); - } - - valuePush(path, value) { - this.valuePath.push([path, value]); - } - - valuePop() { - this.valuePath.pop(); - } - - schemaPush(path, schema) { - this.schemaPath.push([path, schema]); - } - - schemaPop() { - this.schemaPath.pop(); - } -} - -class JsonSchemaValidationError extends Error { - constructor(message, value, schema, info) { - super(message); - this.value = value; - this.schema = schema; - this.info = info; - } -} diff --git a/ext/bg/js/mecab.js b/ext/bg/js/mecab.js deleted file mode 100644 index 4eff2927..00000000 --- a/ext/bg/js/mecab.js +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2019-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/** - * This class is used to connect Yomichan to a native component that is - * used to parse text into individual terms. - */ -class Mecab { - /** - * Creates a new instance of the class. - */ - constructor() { - this._port = null; - this._sequence = 0; - this._invocations = new Map(); - this._eventListeners = new EventListenerCollection(); - this._timeout = 5000; - this._version = 1; - this._remoteVersion = null; - this._enabled = false; - this._setupPortPromise = null; - } - - /** - * Returns whether or not the component is enabled. - */ - isEnabled() { - return this._enabled; - } - - /** - * Changes whether or not the component connection is enabled. - * @param enabled A boolean indicating whether or not the component should be enabled. - */ - setEnabled(enabled) { - this._enabled = !!enabled; - if (!this._enabled && this._port !== null) { - this._clearPort(); - } - } - - /** - * Disconnects the current port, but does not disable future connections. - */ - disconnect() { - if (this._port !== null) { - this._clearPort(); - } - } - - /** - * Returns whether or not the connection to the native application is active. - * @returns `true` if the connection is active, `false` otherwise. - */ - isConnected() { - return (this._port !== null); - } - - /** - * Returns whether or not any invocation is currently active. - * @returns `true` if an invocation is active, `false` otherwise. - */ - isActive() { - return (this._invocations.size > 0); - } - - /** - * Gets the local API version being used. - * @returns An integer representing the API version that Yomichan uses. - */ - getLocalVersion() { - return this._version; - } - - /** - * Gets the version of the MeCab component. - * @returns The version of the MeCab component, or `null` if the component was not found. - */ - async getVersion() { - try { - await this._setupPort(); - } catch (e) { - // NOP - } - return this._remoteVersion; - } - - /** - * Parses a string of Japanese text into arrays of lines and terms. - * - * Return value format: - * ```js - * [ - * { - * name: (string), - * lines: [ - * {expression: (string), reading: (string), source: (string)}, - * ... - * ] - * }, - * ... - * ] - * ``` - * @param text The string to parse. - * @returns A collection of parsing results of the text. - */ - async parseText(text) { - await this._setupPort(); - const rawResults = await this._invoke('parse_text', {text}); - return this._convertParseTextResults(rawResults); - } - - // Private - - _onMessage({sequence, data}) { - const invocation = this._invocations.get(sequence); - if (typeof invocation === 'undefined') { return; } - - const {resolve, timer} = invocation; - clearTimeout(timer); - resolve(data); - this._invocations.delete(sequence); - } - - _onDisconnect() { - if (this._port === null) { return; } - const e = chrome.runtime.lastError; - const error = new Error(e ? e.message : 'MeCab disconnected'); - for (const {reject, timer} of this._invocations.values()) { - clearTimeout(timer); - reject(error); - } - this._clearPort(); - } - - _invoke(action, params) { - return new Promise((resolve, reject) => { - if (this._port === null) { - reject(new Error('Port disconnected')); - } - - const sequence = this._sequence++; - - const timer = setTimeout(() => { - this._invocations.delete(sequence); - reject(new Error(`MeCab invoke timed out after ${this._timeout}ms`)); - }, this._timeout); - - this._invocations.set(sequence, {resolve, reject, timer}, this._timeout); - - this._port.postMessage({action, params, sequence}); - }); - } - - _convertParseTextResults(rawResults) { - const results = []; - for (const [name, rawLines] of Object.entries(rawResults)) { - const lines = []; - for (const rawLine of rawLines) { - const line = []; - for (let {expression, reading, source} of rawLine) { - if (typeof expression !== 'string') { expression = ''; } - if (typeof reading !== 'string') { reading = ''; } - if (typeof source !== 'string') { source = ''; } - line.push({expression, reading, source}); - } - lines.push(line); - } - results.push({name, lines}); - } - return results; - } - - async _setupPort() { - if (!this._enabled) { - throw new Error('MeCab not enabled'); - } - if (this._setupPortPromise === null) { - this._setupPortPromise = this._setupPort2(); - } - try { - await this._setupPortPromise; - } catch (e) { - throw new Error(e.message); - } - } - - async _setupPort2() { - const port = chrome.runtime.connectNative('yomichan_mecab'); - this._eventListeners.addListener(port.onMessage, this._onMessage.bind(this)); - this._eventListeners.addListener(port.onDisconnect, this._onDisconnect.bind(this)); - this._port = port; - - try { - const {version} = await this._invoke('get_version', {}); - this._remoteVersion = version; - if (version !== this._version) { - throw new Error(`Unsupported MeCab native messenger version ${version}. Yomichan supports version ${this._version}.`); - } - } catch (e) { - if (this._port === port) { - this._clearPort(); - } - throw e; - } - } - - _clearPort() { - this._port.disconnect(); - this._port = null; - this._invocations.clear(); - this._eventListeners.removeAllEventListeners(); - this._sequence = 0; - this._setupPortPromise = null; - } -} diff --git a/ext/bg/js/media-utility.js b/ext/bg/js/media-utility.js deleted file mode 100644 index b4fbe04d..00000000 --- a/ext/bg/js/media-utility.js +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/** - * MediaUtility is a class containing helper methods related to media processing. - */ -class MediaUtility { - /** - * Gets the file extension of a file path. URL search queries and hash - * fragments are not handled. - * @param path The path to the file. - * @returns The file extension, including the '.', or an empty string - * if there is no file extension. - */ - getFileNameExtension(path) { - const match = /\.[^./\\]*$/.exec(path); - return match !== null ? match[0] : ''; - } - - /** - * Gets an image file's media type using a file path. - * @param path The path to the file. - * @returns The media type string if it can be determined from the file path, - * otherwise null. - */ - getImageMediaTypeFromFileName(path) { - switch (this.getFileNameExtension(path).toLowerCase()) { - case '.apng': - return 'image/apng'; - case '.bmp': - return 'image/bmp'; - case '.gif': - return 'image/gif'; - case '.ico': - case '.cur': - return 'image/x-icon'; - case '.jpg': - case '.jpeg': - case '.jfif': - case '.pjpeg': - case '.pjp': - return 'image/jpeg'; - case '.png': - return 'image/png'; - case '.svg': - return 'image/svg+xml'; - case '.tif': - case '.tiff': - return 'image/tiff'; - case '.webp': - return 'image/webp'; - default: - return null; - } - } - - /** - * Gets the file extension for a corresponding media type. - * @param mediaType The media type to use. - * @returns A file extension including the dot for the media type, - * otherwise null. - */ - getFileExtensionFromImageMediaType(mediaType) { - switch (mediaType) { - case 'image/apng': - return '.apng'; - case 'image/bmp': - return '.bmp'; - case 'image/gif': - return '.gif'; - case 'image/x-icon': - return '.ico'; - case 'image/jpeg': - return '.jpeg'; - case 'image/png': - return '.png'; - case 'image/svg+xml': - return '.svg'; - case 'image/tiff': - return '.tiff'; - case 'image/webp': - return '.webp'; - default: - return null; - } - } - - /** - * Gets the file extension for a corresponding media type. - * @param mediaType The media type to use. - * @returns A file extension including the dot for the media type, - * otherwise null. - */ - getFileExtensionFromAudioMediaType(mediaType) { - switch (mediaType) { - case 'audio/mpeg': - case 'audio/mp3': - return '.mp3'; - case 'audio/mp4': - return '.mp4'; - case 'audio/ogg': - case 'audio/vorbis': - return '.ogg'; - case 'audio/vnd.wav': - case 'audio/wave': - case 'audio/wav': - case 'audio/x-wav': - case 'audio/x-pn-wav': - return '.wav'; - case 'audio/flac': - return '.flac'; - case 'audio/webm': - return '.webm'; - default: - return null; - } - } -} diff --git a/ext/bg/js/native-simple-dom-parser.js b/ext/bg/js/native-simple-dom-parser.js deleted file mode 100644 index 27dadec0..00000000 --- a/ext/bg/js/native-simple-dom-parser.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class NativeSimpleDOMParser { - constructor(content) { - this._document = new DOMParser().parseFromString(content, 'text/html'); - } - - getElementById(id, root=null) { - return (root || this._document).querySelector(`[id='${id}']`); - } - - getElementByTagName(tagName, root=null) { - return (root || this._document).querySelector(tagName); - } - - getElementsByTagName(tagName, root=null) { - return [...(root || this._document).querySelectorAll(tagName)]; - } - - getElementsByClassName(className, root=null) { - return [...(root || this._document).querySelectorAll(`.${className}`)]; - } - - getAttribute(element, attribute) { - return element.hasAttribute(attribute) ? element.getAttribute(attribute) : null; - } - - getTextContent(element) { - return element.textContent; - } - - static isSupported() { - return typeof DOMParser !== 'undefined'; - } -} diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js deleted file mode 100644 index 1105dfed..00000000 --- a/ext/bg/js/options.js +++ /dev/null @@ -1,739 +0,0 @@ -/* - * Copyright (C) 2016-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * JsonSchemaValidator - * TemplatePatcher - */ - -class OptionsUtil { - constructor() { - this._schemaValidator = new JsonSchemaValidator(); - this._templatePatcher = null; - this._optionsSchema = null; - } - - async prepare() { - this._optionsSchema = await this._fetchAsset('/data/schemas/options-schema.json', true); - } - - async update(options) { - // Invalid options - if (!isObject(options)) { - options = {}; - } - - // Check for legacy options - let defaultProfileOptions = {}; - if (!Array.isArray(options.profiles)) { - defaultProfileOptions = options; - options = {}; - } - - // Ensure profiles is an array - if (!Array.isArray(options.profiles)) { - options.profiles = []; - } - - // Remove invalid profiles - const profiles = options.profiles; - for (let i = profiles.length - 1; i >= 0; --i) { - if (!isObject(profiles[i])) { - profiles.splice(i, 1); - } - } - - // Require at least one profile - if (profiles.length === 0) { - profiles.push({ - name: 'Default', - options: defaultProfileOptions, - conditionGroups: [] - }); - } - - // Ensure profileCurrent is valid - const profileCurrent = options.profileCurrent; - if (!( - typeof profileCurrent === 'number' && - Number.isFinite(profileCurrent) && - Math.floor(profileCurrent) === profileCurrent && - profileCurrent >= 0 && - profileCurrent < profiles.length - )) { - options.profileCurrent = 0; - } - - // Version - if (typeof options.version !== 'number') { - options.version = 0; - } - - // Generic updates - options = await this._applyUpdates(options, this._getVersionUpdates()); - - // Validation - options = this._schemaValidator.getValidValueOrDefault(this._optionsSchema, options); - - // Result - return options; - } - - async load() { - let options; - try { - const optionsStr = await new Promise((resolve, reject) => { - chrome.storage.local.get(['options'], (store) => { - const error = chrome.runtime.lastError; - if (error) { - reject(new Error(error.message)); - } else { - resolve(store.options); - } - }); - }); - options = JSON.parse(optionsStr); - } catch (e) { - // NOP - } - - if (typeof options !== 'undefined') { - options = await this.update(options); - } else { - options = this.getDefault(); - } - - return options; - } - - save(options) { - return new Promise((resolve, reject) => { - chrome.storage.local.set({options: JSON.stringify(options)}, () => { - const error = chrome.runtime.lastError; - if (error) { - reject(new Error(error.message)); - } else { - resolve(); - } - }); - }); - } - - getDefault() { - const optionsVersion = this._getVersionUpdates().length; - const options = this._schemaValidator.getValidValueOrDefault(this._optionsSchema); - options.version = optionsVersion; - return options; - } - - createValidatingProxy(options) { - return this._schemaValidator.createProxy(options, this._optionsSchema); - } - - validate(options) { - return this._schemaValidator.validate(options, this._optionsSchema); - } - - // Legacy profile updating - - _legacyProfileUpdateGetUpdates() { - return [ - null, - null, - null, - null, - (options) => { - options.general.audioSource = options.general.audioPlayback ? 'jpod101' : 'disabled'; - }, - (options) => { - options.general.showGuide = false; - }, - (options) => { - options.scanning.modifier = options.scanning.requireShift ? 'shift' : 'none'; - }, - (options) => { - options.general.resultOutputMode = options.general.groupResults ? 'group' : 'split'; - options.anki.fieldTemplates = null; - }, - (options) => { - if (this._getStringHashCode(options.anki.fieldTemplates) === 1285806040) { - options.anki.fieldTemplates = null; - } - }, - (options) => { - if (this._getStringHashCode(options.anki.fieldTemplates) === -250091611) { - options.anki.fieldTemplates = null; - } - }, - (options) => { - const oldAudioSource = options.general.audioSource; - const disabled = oldAudioSource === 'disabled'; - options.audio.enabled = !disabled; - options.audio.volume = options.general.audioVolume; - options.audio.autoPlay = options.general.autoPlayAudio; - options.audio.sources = [disabled ? 'jpod101' : oldAudioSource]; - - delete options.general.audioSource; - delete options.general.audioVolume; - delete options.general.autoPlayAudio; - }, - (options) => { - // Version 12 changes: - // The preferred default value of options.anki.fieldTemplates has been changed to null. - if (this._getStringHashCode(options.anki.fieldTemplates) === 1444379824) { - options.anki.fieldTemplates = null; - } - }, - (options) => { - // Version 13 changes: - // Default anki field tempaltes updated to include {document-title}. - let fieldTemplates = options.anki.fieldTemplates; - if (typeof fieldTemplates === 'string') { - fieldTemplates += '\n\n{{#*inline "document-title"}}\n {{~context.document.title~}}\n{{/inline}}'; - options.anki.fieldTemplates = fieldTemplates; - } - }, - (options) => { - // Version 14 changes: - // Changed template for Anki audio and tags. - let fieldTemplates = options.anki.fieldTemplates; - if (typeof fieldTemplates !== 'string') { return; } - - const replacements = [ - [ - '{{#*inline "audio"}}{{/inline}}', - '{{#*inline "audio"}}\n {{~#if definition.audioFileName~}}\n [sound:{{definition.audioFileName}}]\n {{~/if~}}\n{{/inline}}' - ], - [ - '{{#*inline "tags"}}\n {{~#each definition.definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each~}}\n{{/inline}}', - '{{#*inline "tags"}}\n {{~#mergeTags definition group merge}}{{this}}{{/mergeTags~}}\n{{/inline}}' - ] - ]; - - for (const [pattern, replacement] of replacements) { - let replaced = false; - fieldTemplates = fieldTemplates.replace(new RegExp(escapeRegExp(pattern), 'g'), () => { - replaced = true; - return replacement; - }); - - if (!replaced) { - fieldTemplates += '\n\n' + replacement; - } - } - - options.anki.fieldTemplates = fieldTemplates; - } - ]; - } - - _legacyProfileUpdateGetDefaults() { - return { - general: { - enable: true, - enableClipboardPopups: false, - resultOutputMode: 'group', - debugInfo: false, - maxResults: 32, - showAdvanced: false, - popupDisplayMode: 'default', - popupWidth: 400, - popupHeight: 250, - popupHorizontalOffset: 0, - popupVerticalOffset: 10, - popupHorizontalOffset2: 10, - popupVerticalOffset2: 0, - popupHorizontalTextPosition: 'below', - popupVerticalTextPosition: 'before', - popupScalingFactor: 1, - popupScaleRelativeToPageZoom: false, - popupScaleRelativeToVisualViewport: true, - showGuide: true, - compactTags: false, - compactGlossaries: false, - mainDictionary: '', - popupTheme: 'default', - popupOuterTheme: 'default', - customPopupCss: '', - customPopupOuterCss: '', - enableWanakana: true, - enableClipboardMonitor: false, - showPitchAccentDownstepNotation: true, - showPitchAccentPositionNotation: true, - showPitchAccentGraph: false, - showIframePopupsInRootFrame: false, - useSecurePopupFrameUrl: true, - usePopupShadowDom: true - }, - - audio: { - enabled: true, - sources: ['jpod101'], - volume: 100, - autoPlay: false, - customSourceUrl: '', - textToSpeechVoice: '' - }, - - scanning: { - middleMouse: true, - touchInputEnabled: true, - selectText: true, - alphanumeric: true, - autoHideResults: false, - delay: 20, - length: 10, - modifier: 'shift', - deepDomScan: false, - popupNestingMaxDepth: 0, - enablePopupSearch: false, - enableOnPopupExpressions: false, - enableOnSearchPage: true, - enableSearchTags: false, - layoutAwareScan: false - }, - - translation: { - convertHalfWidthCharacters: 'false', - convertNumericCharacters: 'false', - convertAlphabeticCharacters: 'false', - convertHiraganaToKatakana: 'false', - convertKatakanaToHiragana: 'variant', - collapseEmphaticSequences: 'false' - }, - - dictionaries: {}, - - parsing: { - enableScanningParser: true, - enableMecabParser: false, - selectedParser: null, - termSpacing: true, - readingMode: 'hiragana' - }, - - anki: { - enable: false, - server: 'http://127.0.0.1:8765', - tags: ['yomichan'], - sentenceExt: 200, - screenshot: {format: 'png', quality: 92}, - terms: {deck: '', model: '', fields: {}}, - kanji: {deck: '', model: '', fields: {}}, - duplicateScope: 'collection', - fieldTemplates: null - } - }; - } - - _legacyProfileUpdateAssignDefaults(options) { - const defaults = this._legacyProfileUpdateGetDefaults(); - - const combine = (target, source) => { - for (const key in source) { - if (!Object.prototype.hasOwnProperty.call(target, key)) { - target[key] = source[key]; - } - } - }; - - combine(options, defaults); - combine(options.general, defaults.general); - combine(options.scanning, defaults.scanning); - combine(options.anki, defaults.anki); - combine(options.anki.terms, defaults.anki.terms); - combine(options.anki.kanji, defaults.anki.kanji); - - return options; - } - - _legacyProfileUpdateUpdateVersion(options) { - const updates = this._legacyProfileUpdateGetUpdates(); - this._legacyProfileUpdateAssignDefaults(options); - - const targetVersion = updates.length; - const currentVersion = options.version; - - if (typeof currentVersion === 'number' && Number.isFinite(currentVersion)) { - for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) { - const update = updates[i]; - if (update !== null) { - update(options); - } - } - } - - options.version = targetVersion; - return options; - } - - // Private - - async _applyAnkiFieldTemplatesPatch(options, modificationsUrl) { - let patch = null; - for (const {options: profileOptions} of options.profiles) { - const fieldTemplates = profileOptions.anki.fieldTemplates; - if (fieldTemplates === null) { continue; } - - if (patch === null) { - const content = await this._fetchAsset(modificationsUrl); - if (this._templatePatcher === null) { - this._templatePatcher = new TemplatePatcher(); - } - patch = this._templatePatcher.parsePatch(content); - } - - profileOptions.anki.fieldTemplates = this._templatePatcher.applyPatch(fieldTemplates, patch); - } - } - - async _fetchAsset(url, json=false) { - url = chrome.runtime.getURL(url); - const response = await fetch(url, { - method: 'GET', - mode: 'no-cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }); - if (!response.ok) { - throw new Error(`Failed to fetch ${url}: ${response.status}`); - } - return await (json ? response.json() : response.text()); - } - - _getStringHashCode(string) { - let hashCode = 0; - - if (typeof string !== 'string') { return hashCode; } - - for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) { - hashCode = ((hashCode << 5) - hashCode) + charCode; - hashCode |= 0; - } - - return hashCode; - } - - async _applyUpdates(options, updates) { - const targetVersion = updates.length; - let currentVersion = options.version; - - if (typeof currentVersion !== 'number' || !Number.isFinite(currentVersion)) { - currentVersion = 0; - } - - for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) { - const {update, async} = updates[i]; - const result = update(options); - options = (async ? await result : result); - } - - options.version = targetVersion; - return options; - } - - _getVersionUpdates() { - return [ - {async: false, update: this._updateVersion1.bind(this)}, - {async: false, update: this._updateVersion2.bind(this)}, - {async: true, update: this._updateVersion3.bind(this)}, - {async: true, update: this._updateVersion4.bind(this)}, - {async: false, update: this._updateVersion5.bind(this)}, - {async: true, update: this._updateVersion6.bind(this)}, - {async: false, update: this._updateVersion7.bind(this)}, - {async: true, update: this._updateVersion8.bind(this)} - ]; - } - - _updateVersion1(options) { - // Version 1 changes: - // Added options.global.database.prefixWildcardsSupported = false. - options.global = { - database: { - prefixWildcardsSupported: false - } - }; - return options; - } - - _updateVersion2(options) { - // Version 2 changes: - // Legacy profile update process moved into this upgrade function. - for (const profile of options.profiles) { - if (!Array.isArray(profile.conditionGroups)) { - profile.conditionGroups = []; - } - profile.options = this._legacyProfileUpdateUpdateVersion(profile.options); - } - return options; - } - - async _updateVersion3(options) { - // Version 3 changes: - // Pitch accent Anki field templates added. - await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v2.handlebars'); - return options; - } - - async _updateVersion4(options) { - // Version 4 changes: - // Options conditions converted to string representations. - // Added usePopupWindow. - // Updated handlebars templates to include "clipboard-image" definition. - // Updated handlebars templates to include "clipboard-text" definition. - // Added hideDelay. - // Added inputs to profileOptions.scanning. - // Added pointerEventsEnabled to profileOptions.scanning. - // Added preventMiddleMouse to profileOptions.scanning. - for (const {conditionGroups} of options.profiles) { - for (const {conditions} of conditionGroups) { - for (const condition of conditions) { - const value = condition.value; - condition.value = ( - Array.isArray(value) ? - value.join(', ') : - `${value}` - ); - } - } - } - const createInputDefaultOptions = () => ({ - showAdvanced: false, - searchTerms: true, - searchKanji: true, - scanOnTouchMove: true, - scanOnPenHover: true, - scanOnPenPress: true, - scanOnPenRelease: false, - preventTouchScrolling: true - }); - for (const {options: profileOptions} of options.profiles) { - profileOptions.general.usePopupWindow = false; - profileOptions.scanning.hideDelay = 0; - profileOptions.scanning.pointerEventsEnabled = false; - profileOptions.scanning.preventMiddleMouse = { - onWebPages: false, - onPopupPages: false, - onSearchPages: false, - onSearchQuery: false - }; - - const {modifier, middleMouse} = profileOptions.scanning; - delete profileOptions.scanning.modifier; - delete profileOptions.scanning.middleMouse; - const scanningInputs = []; - let modifierInput = ''; - switch (modifier) { - case 'alt': - case 'ctrl': - case 'shift': - case 'meta': - modifierInput = modifier; - break; - case 'none': - modifierInput = ''; - break; - } - scanningInputs.push({ - include: modifierInput, - exclude: 'mouse0', - types: {mouse: true, touch: false, pen: false}, - options: createInputDefaultOptions() - }); - if (middleMouse) { - scanningInputs.push({ - include: 'mouse2', - exclude: '', - types: {mouse: true, touch: false, pen: false}, - options: createInputDefaultOptions() - }); - } - scanningInputs.push({ - include: '', - exclude: '', - types: {mouse: false, touch: true, pen: true}, - options: createInputDefaultOptions() - }); - profileOptions.scanning.inputs = scanningInputs; - } - await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v4.handlebars'); - return options; - } - - _updateVersion5(options) { - // Version 5 changes: - // Removed legacy version number from profile options. - for (const profile of options.profiles) { - delete profile.options.version; - } - return options; - } - - async _updateVersion6(options) { - // Version 6 changes: - // Updated handlebars templates to include "conjugation" definition. - // Added global option showPopupPreview. - // Added global option useSettingsV2. - // Added anki.checkForDuplicates. - // Added general.glossaryLayoutMode; removed general.compactGlossaries. - await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v6.handlebars'); - options.global.showPopupPreview = false; - options.global.useSettingsV2 = false; - for (const profile of options.profiles) { - profile.options.anki.checkForDuplicates = true; - profile.options.general.glossaryLayoutMode = (profile.options.general.compactGlossaries ? 'compact' : 'default'); - delete profile.options.general.compactGlossaries; - const fieldTemplates = profile.options.anki.fieldTemplates; - if (typeof fieldTemplates === 'string') { - profile.options.anki.fieldTemplates = this._updateVersion6AnkiTemplatesCompactTags(fieldTemplates); - } - } - return options; - } - - _updateVersion6AnkiTemplatesCompactTags(templates) { - const rawPattern1 = '{{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}'; - const pattern1 = new RegExp(`((\r?\n)?[ \t]*)${escapeRegExp(rawPattern1)}`, 'g'); - const replacement1 = ( - // eslint-disable-next-line indent -`{{~#scope~}} - {{~#set "any" false}}{{/set~}} - {{~#if definitionTags~}}{{#each definitionTags~}} - {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} - {{~#if (get "any")}}, {{else}}<i>({{/if~}} - {{name}} - {{~#set "any" true}}{{/set~}} - {{~/if~}} - {{~/each~}} - {{~#if (get "any")}})</i> {{/if~}} - {{~/if~}} -{{~/scope~}}` - ); - const simpleNewline = /\n/g; - templates = templates.replace(pattern1, (g0, space) => (space + replacement1.replace(simpleNewline, space))); - templates = templates.replace(/\bcompactGlossaries=((?:\.*\/)*)compactGlossaries\b/g, (g0, g1) => `${g0} data=${g1}.`); - return templates; - } - - _updateVersion7(options) { - // Version 7 changes: - // Added general.maximumClipboardSearchLength. - // Added general.popupCurrentIndicatorMode. - // Added general.popupActionBarVisibility. - // Added general.popupActionBarLocation. - // Removed global option showPopupPreview. - delete options.global.showPopupPreview; - for (const profile of options.profiles) { - profile.options.general.maximumClipboardSearchLength = 1000; - profile.options.general.popupCurrentIndicatorMode = 'triangle'; - profile.options.general.popupActionBarVisibility = 'auto'; - profile.options.general.popupActionBarLocation = 'right'; - } - return options; - } - - async _updateVersion8(options) { - // Version 8 changes: - // Added translation.textReplacements. - // Moved anki.sentenceExt to sentenceParsing.scanExtent. - // Added sentenceParsing.enableTerminationCharacters. - // Added sentenceParsing.terminationCharacters. - // Changed general.popupActionBarLocation. - // Added inputs.hotkeys. - // Added anki.suspendNewCards. - // Added popupWindow. - // Updated handlebars templates to include "stroke-count" definition. - // Updated global.useSettingsV2 to be true (opt-out). - // Added audio.customSourceType. - // Moved general.enableClipboardPopups => clipboard.enableBackgroundMonitor. - // Moved general.enableClipboardMonitor => clipboard.enableSearchPageMonitor. Forced value to false due to a bug which caused its value to not be read. - // Moved general.maximumClipboardSearchLength => clipboard.maximumSearchLength. - // Added clipboard.autoSearchContent. - await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v8.handlebars'); - options.global.useSettingsV2 = true; - for (const profile of options.profiles) { - profile.options.translation.textReplacements = { - searchOriginal: true, - groups: [] - }; - profile.options.sentenceParsing = { - scanExtent: profile.options.anki.sentenceExt, - enableTerminationCharacters: true, - terminationCharacters: [ - {enabled: true, character1: '「', character2: '」', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '『', character2: '』', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '"', character2: '"', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '\'', character2: '\'', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '。', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '…', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true} - ] - }; - delete profile.options.anki.sentenceExt; - profile.options.general.popupActionBarLocation = 'top'; - profile.options.inputs = { - hotkeys: [ - {action: 'close', key: 'Escape', modifiers: [], scopes: ['popup'], enabled: true}, - {action: 'focusSearchBox', key: 'Escape', modifiers: [], scopes: ['search'], enabled: true}, - {action: 'previousEntry3', key: 'PageUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'nextEntry3', key: 'PageDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'lastEntry', key: 'End', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'firstEntry', key: 'Home', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'previousEntry', key: 'ArrowUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'nextEntry', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'historyBackward', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'historyForward', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'addNoteKanji', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'addNoteTermKanji', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'addNoteTermKana', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'playAudio', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'viewNote', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'copyHostSelection', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup'], enabled: true} - ] - }; - profile.options.anki.suspendNewCards = false; - profile.options.popupWindow = { - width: profile.options.general.popupWidth, - height: profile.options.general.popupHeight, - left: 0, - top: 0, - useLeft: false, - useTop: false, - windowType: 'popup', - windowState: 'normal' - }; - profile.options.audio.customSourceType = 'audio'; - profile.options.clipboard = { - enableBackgroundMonitor: profile.options.general.enableClipboardPopups, - enableSearchPageMonitor: false, - autoSearchContent: true, - maximumSearchLength: profile.options.general.maximumClipboardSearchLength - }; - delete profile.options.general.enableClipboardPopups; - delete profile.options.general.enableClipboardMonitor; - delete profile.options.general.maximumClipboardSearchLength; - } - return options; - } -} diff --git a/ext/bg/js/permissions-main.js b/ext/bg/js/permissions-main.js deleted file mode 100644 index 5b17a5dd..00000000 --- a/ext/bg/js/permissions-main.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * DocumentFocusController - * PermissionsToggleController - * SettingsController - * api - */ - -async function setupEnvironmentInfo() { - const {manifest_version: manifestVersion} = chrome.runtime.getManifest(); - const {browser, platform} = await api.getEnvironmentInfo(); - document.documentElement.dataset.browser = browser; - document.documentElement.dataset.os = platform.os; - document.documentElement.dataset.manifestVersion = `${manifestVersion}`; -} - -async function isAllowedIncognitoAccess() { - return await new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve)); -} - -async function isAllowedFileSchemeAccess() { - return await new Promise((resolve) => chrome.extension.isAllowedFileSchemeAccess(resolve)); -} - -function setupPermissionsToggles() { - const manifest = chrome.runtime.getManifest(); - let optionalPermissions = manifest.optional_permissions; - if (!Array.isArray(optionalPermissions)) { optionalPermissions = []; } - optionalPermissions = new Set(optionalPermissions); - - const hasAllPermisions = (set, values) => { - for (const value of values) { - if (!set.has(value)) { return false; } - } - return true; - }; - - for (const toggle of document.querySelectorAll('.permissions-toggle')) { - let permissions = toggle.dataset.requiredPermissions; - permissions = (typeof permissions === 'string' && permissions.length > 0 ? permissions.split(' ') : []); - toggle.disabled = !hasAllPermisions(optionalPermissions, permissions); - } -} - -(async () => { - try { - const documentFocusController = new DocumentFocusController(); - documentFocusController.prepare(); - - setupPermissionsToggles(); - - for (const node of document.querySelectorAll('.extension-id-example')) { - node.textContent = chrome.runtime.getURL('/'); - } - - api.forwardLogsToBackend(); - await yomichan.prepare(); - - setupEnvironmentInfo(); - - const permissionsCheckboxes = [ - document.querySelector('#permission-checkbox-allow-in-private-windows'), - document.querySelector('#permission-checkbox-allow-file-url-access') - ]; - - const permissions = await Promise.all([ - isAllowedIncognitoAccess(), - isAllowedFileSchemeAccess() - ]); - - for (let i = 0, ii = permissions.length; i < ii; ++i) { - permissionsCheckboxes[i].checked = permissions[i]; - } - - const settingsController = new SettingsController(0); - settingsController.prepare(); - - const permissionsToggleController = new PermissionsToggleController(settingsController); - permissionsToggleController.prepare(); - - await promiseTimeout(100); - - document.documentElement.dataset.loaded = 'true'; - } catch (e) { - yomichan.logError(e); - } -})(); diff --git a/ext/bg/js/permissions-util.js b/ext/bg/js/permissions-util.js deleted file mode 100644 index bd3a18ce..00000000 --- a/ext/bg/js/permissions-util.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class PermissionsUtil { - constructor() { - this._ankiFieldMarkersRequiringClipboardPermission = new Set([ - 'clipboard-image', - 'clipboard-text' - ]); - this._ankiMarkerPattern = /\{([\w-]+)\}/g; - } - - hasPermissions(permissions) { - return new Promise((resolve, reject) => chrome.permissions.contains(permissions, (result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - })); - } - - setPermissionsGranted(permissions, shouldHave) { - return ( - shouldHave ? - new Promise((resolve, reject) => chrome.permissions.request(permissions, (result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - })) : - new Promise((resolve, reject) => chrome.permissions.remove(permissions, (result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(!result); - } - })) - ); - } - - getAllPermissions() { - return new Promise((resolve, reject) => chrome.permissions.getAll((result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - })); - } - - getRequiredPermissionsForAnkiFieldValue(fieldValue) { - const markers = this._getAnkiFieldMarkers(fieldValue); - const markerPermissions = this._ankiFieldMarkersRequiringClipboardPermission; - for (const marker of markers) { - if (markerPermissions.has(marker)) { - return ['clipboardRead']; - } - } - return []; - } - - hasRequiredPermissionsForOptions(permissions, options) { - const permissionsSet = new Set(permissions.permissions); - - if (!permissionsSet.has('nativeMessaging')) { - if (options.parsing.enableMecabParser) { - return false; - } - } - - if (!permissionsSet.has('clipboardRead')) { - if (options.clipboard.enableBackgroundMonitor || options.clipboard.enableSearchPageMonitor) { - return false; - } - const fieldMarkersRequiringClipboardPermission = this._ankiFieldMarkersRequiringClipboardPermission; - const fieldsList = [ - options.anki.terms.fields, - options.anki.kanji.fields - ]; - for (const fields of fieldsList) { - for (const fieldValue of Object.values(fields)) { - const markers = this._getAnkiFieldMarkers(fieldValue); - for (const marker of markers) { - if (fieldMarkersRequiringClipboardPermission.has(marker)) { - return false; - } - } - } - } - } - - return true; - } - - // Private - - _getAnkiFieldMarkers(fieldValue) { - const pattern = this._ankiMarkerPattern; - const markers = []; - let match; - while ((match = pattern.exec(fieldValue)) !== null) { - markers.push(match[1]); - } - return markers; - } -} diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js deleted file mode 100644 index 8e6c7163..00000000 --- a/ext/bg/js/profile-conditions.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/** - * Utility class to help processing profile conditions. - */ -class ProfileConditions { - /** - * Creates a new instance. - */ - constructor() { - this._splitPattern = /[,;\s]+/; - this._descriptors = new Map([ - [ - 'popupLevel', - { - operators: new Map([ - ['equal', this._createSchemaPopupLevelEqual.bind(this)], - ['notEqual', this._createSchemaPopupLevelNotEqual.bind(this)], - ['lessThan', this._createSchemaPopupLevelLessThan.bind(this)], - ['greaterThan', this._createSchemaPopupLevelGreaterThan.bind(this)], - ['lessThanOrEqual', this._createSchemaPopupLevelLessThanOrEqual.bind(this)], - ['greaterThanOrEqual', this._createSchemaPopupLevelGreaterThanOrEqual.bind(this)] - ]) - } - ], - [ - 'url', - { - operators: new Map([ - ['matchDomain', this._createSchemaUrlMatchDomain.bind(this)], - ['matchRegExp', this._createSchemaUrlMatchRegExp.bind(this)] - ]) - } - ], - [ - 'modifierKeys', - { - operators: new Map([ - ['are', this._createSchemaModifierKeysAre.bind(this)], - ['areNot', this._createSchemaModifierKeysAreNot.bind(this)], - ['include', this._createSchemaModifierKeysInclude.bind(this)], - ['notInclude', this._createSchemaModifierKeysNotInclude.bind(this)] - ]) - } - ] - ]); - } - - /** - * Creates a new JSON schema descriptor for the given set of condition groups. - * @param conditionGroups An array of condition groups in the following format: - * conditionGroups = [ - * { - * conditions: [ - * { - * type: (condition type: string), - * operator: (condition sub-type: string), - * value: (value to compare against: string) - * }, - * ... - * ] - * }, - * ... - * ] - */ - createSchema(conditionGroups) { - const anyOf = []; - for (const {conditions} of conditionGroups) { - const allOf = []; - for (const {type, operator, value} of conditions) { - const conditionDescriptor = this._descriptors.get(type); - if (typeof conditionDescriptor === 'undefined') { continue; } - - const createSchema = conditionDescriptor.operators.get(operator); - if (typeof createSchema === 'undefined') { continue; } - - const schema = createSchema(value); - allOf.push(schema); - } - switch (allOf.length) { - case 0: break; - case 1: anyOf.push(allOf[0]); break; - default: anyOf.push({allOf}); break; - } - } - switch (anyOf.length) { - case 0: return {}; - case 1: return anyOf[0]; - default: return {anyOf}; - } - } - - /** - * Creates a normalized version of the context object to test, - * assigning dependent fields as needed. - * @param context A context object which is used during schema validation. - * @returns A normalized context object. - */ - normalizeContext(context) { - const normalizedContext = Object.assign({}, context); - const {url} = normalizedContext; - if (typeof url === 'string') { - try { - normalizedContext.domain = new URL(url).hostname; - } catch (e) { - // NOP - } - } - return normalizedContext; - } - - // Private - - _split(value) { - return value.split(this._splitPattern); - } - - _stringToNumber(value) { - const number = Number.parseFloat(value); - return Number.isFinite(number) ? number : 0; - } - - // popupLevel schema creation functions - - _createSchemaPopupLevelEqual(value) { - value = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {const: value} - } - }; - } - - _createSchemaPopupLevelNotEqual(value) { - return { - not: [this._createSchemaPopupLevelEqual(value)] - }; - } - - _createSchemaPopupLevelLessThan(value) { - value = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {type: 'number', exclusiveMaximum: value} - } - }; - } - - _createSchemaPopupLevelGreaterThan(value) { - value = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {type: 'number', exclusiveMinimum: value} - } - }; - } - - _createSchemaPopupLevelLessThanOrEqual(value) { - value = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {type: 'number', maximum: value} - } - }; - } - - _createSchemaPopupLevelGreaterThanOrEqual(value) { - value = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {type: 'number', minimum: value} - } - }; - } - - // url schema creation functions - - _createSchemaUrlMatchDomain(value) { - const oneOf = []; - for (let domain of this._split(value)) { - if (domain.length === 0) { continue; } - domain = domain.toLowerCase(); - oneOf.push({const: domain}); - } - return { - required: ['domain'], - properties: { - domain: {oneOf} - } - }; - } - - _createSchemaUrlMatchRegExp(value) { - return { - required: ['url'], - properties: { - url: {type: 'string', pattern: value, patternFlags: 'i'} - } - }; - } - - // modifierKeys schema creation functions - - _createSchemaModifierKeysAre(value) { - return this._createSchemaModifierKeysGeneric(value, true, false); - } - - _createSchemaModifierKeysAreNot(value) { - return { - not: [this._createSchemaModifierKeysGeneric(value, true, false)] - }; - } - - _createSchemaModifierKeysInclude(value) { - return this._createSchemaModifierKeysGeneric(value, false, false); - } - - _createSchemaModifierKeysNotInclude(value) { - return this._createSchemaModifierKeysGeneric(value, false, true); - } - - _createSchemaModifierKeysGeneric(value, exact, none) { - const containsList = []; - for (const modifierKey of this._split(value)) { - if (modifierKey.length === 0) { continue; } - containsList.push({ - contains: { - const: modifierKey - } - }); - } - const containsListCount = containsList.length; - const modifierKeysSchema = { - type: 'array' - }; - if (exact) { - modifierKeysSchema.maxItems = containsListCount; - } - if (none) { - if (containsListCount > 0) { - modifierKeysSchema.not = containsList; - } - } else { - modifierKeysSchema.minItems = containsListCount; - if (containsListCount > 0) { - modifierKeysSchema.allOf = containsList; - } - } - return { - required: ['modifierKeys'], - properties: { - modifierKeys: modifierKeysSchema - } - }; - } -} diff --git a/ext/bg/js/query-parser.js b/ext/bg/js/query-parser.js deleted file mode 100644 index 05ebfa27..00000000 --- a/ext/bg/js/query-parser.js +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2019-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * TextScanner - * api - */ - -class QueryParser extends EventDispatcher { - constructor({getSearchContext, documentUtil}) { - super(); - this._getSearchContext = getSearchContext; - this._documentUtil = documentUtil; - this._text = ''; - this._setTextToken = null; - this._selectedParser = null; - this._parseResults = []; - this._queryParser = document.querySelector('#query-parser-content'); - this._queryParserModeContainer = document.querySelector('#query-parser-mode-container'); - this._queryParserModeSelect = document.querySelector('#query-parser-mode-select'); - this._textScanner = new TextScanner({ - node: this._queryParser, - getSearchContext, - documentUtil, - searchTerms: true, - searchKanji: false, - searchOnClick: true - }); - } - - get text() { - return this._text; - } - - prepare() { - this._textScanner.prepare(); - this._textScanner.on('searched', this._onSearched.bind(this)); - this._queryParserModeSelect.addEventListener('change', this._onParserChange.bind(this), false); - } - - setOptions({selectedParser, termSpacing, scanning}) { - let selectedParserChanged = false; - if (selectedParser === null || typeof selectedParser === 'string') { - selectedParserChanged = (this._selectedParser !== selectedParser); - this._selectedParser = selectedParser; - } - if (typeof termSpacing === 'boolean') { - this._queryParser.dataset.termSpacing = `${termSpacing}`; - } - if (scanning !== null && typeof scanning === 'object') { - this._textScanner.setOptions(scanning); - } - this._textScanner.setEnabled(true); - if (selectedParserChanged && this._parseResults.length > 0) { - this._renderParseResult(); - } - } - - async setText(text) { - this._text = text; - this._setPreview(text); - - const token = {}; - this._setTextToken = token; - this._parseResults = await api.textParse(text, this._getOptionsContext()); - if (this._setTextToken !== token) { return; } - - this._refreshSelectedParser(); - - this._renderParserSelect(); - this._renderParseResult(); - } - - // Private - - _onSearched(e) { - const {error} = e; - if (error !== null) { - yomichan.logError(error); - return; - } - if (e.type === null) { return; } - - this.trigger('searched', e); - } - - _onParserChange(e) { - const value = e.currentTarget.value; - this._setSelectedParser(value); - } - - _getOptionsContext() { - return this._getSearchContext().optionsContext; - } - - _refreshSelectedParser() { - if (this._parseResults.length > 0 && !this._getParseResult()) { - const value = this._parseResults[0].id; - this._setSelectedParser(value); - } - } - - _setSelectedParser(value) { - const optionsContext = this._getOptionsContext(); - api.modifySettings([{ - action: 'set', - path: 'parsing.selectedParser', - value, - scope: 'profile', - optionsContext - }], 'search'); - } - - _getParseResult() { - const selectedParser = this._selectedParser; - return this._parseResults.find((r) => r.id === selectedParser); - } - - _setPreview(text) { - const terms = [[{text, reading: ''}]]; - this._queryParser.textContent = ''; - this._queryParser.appendChild(this._createParseResult(terms, true)); - } - - _renderParserSelect() { - const visible = (this._parseResults.length > 1); - if (visible) { - this._updateParserModeSelect(this._queryParserModeSelect, this._parseResults, this._selectedParser); - } - this._queryParserModeContainer.hidden = !visible; - } - - _renderParseResult() { - const parseResult = this._getParseResult(); - this._queryParser.textContent = ''; - if (!parseResult) { return; } - this._queryParser.appendChild(this._createParseResult(parseResult.content, false)); - } - - _updateParserModeSelect(select, parseResults, selectedParser) { - const fragment = document.createDocumentFragment(); - - let index = 0; - let selectedIndex = -1; - for (const parseResult of parseResults) { - const option = document.createElement('option'); - option.value = parseResult.id; - switch (parseResult.source) { - case 'scanning-parser': - option.textContent = 'Scanning parser'; - break; - case 'mecab': - option.textContent = `MeCab: ${parseResult.dictionary}`; - break; - default: - option.textContent = `Unknown source: ${parseResult.source}`; - break; - } - fragment.appendChild(option); - - if (selectedParser === parseResult.id) { - selectedIndex = index; - } - ++index; - } - - select.textContent = ''; - select.appendChild(fragment); - select.selectedIndex = selectedIndex; - } - - _createParseResult(terms, preview) { - const type = preview ? 'preview' : 'normal'; - const fragment = document.createDocumentFragment(); - for (const term of terms) { - const termNode = document.createElement('span'); - termNode.className = 'query-parser-term'; - termNode.dataset.type = type; - for (const segment of term) { - if (segment.reading.trim().length === 0) { - this._addSegmentText(segment.text, termNode); - } else { - termNode.appendChild(this._createSegment(segment)); - } - } - fragment.appendChild(termNode); - } - return fragment; - } - - _createSegment(segment) { - const segmentNode = document.createElement('ruby'); - segmentNode.className = 'query-parser-segment'; - - const textNode = document.createElement('span'); - textNode.className = 'query-parser-segment-text'; - - const readingNode = document.createElement('rt'); - readingNode.className = 'query-parser-segment-reading'; - - segmentNode.appendChild(textNode); - segmentNode.appendChild(readingNode); - - this._addSegmentText(segment.text, textNode); - readingNode.textContent = segment.reading; - - return segmentNode; - } - - _addSegmentText(text, container) { - for (const character of text) { - const node = document.createElement('span'); - node.className = 'query-parser-char'; - node.textContent = character; - container.appendChild(node); - } - } -} diff --git a/ext/bg/js/request-builder.js b/ext/bg/js/request-builder.js deleted file mode 100644 index dda5825d..00000000 --- a/ext/bg/js/request-builder.js +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class RequestBuilder { - constructor() { - this._extraHeadersSupported = null; - this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders']; - this._textEncoder = new TextEncoder(); - this._ruleIds = new Set(); - } - - async prepare() { - try { - await this._clearDynamicRules(); - } catch (e) { - // NOP - } - } - - async fetchAnonymous(url, init) { - if (isObject(chrome.declarativeNetRequest)) { - return await this._fetchAnonymousDeclarative(url, init); - } - const originURL = this._getOriginURL(url); - const modifications = [ - ['cookie', null], - ['origin', {name: 'Origin', value: originURL}] - ]; - return await this._fetchModifyHeaders(url, init, modifications); - } - - // Private - - async _fetchModifyHeaders(url, init, modifications) { - const matchURL = this._getMatchURL(url); - - let done = false; - const callback = (details) => { - if (done || details.url !== url) { return {}; } - done = true; - - const requestHeaders = details.requestHeaders; - this._modifyHeaders(requestHeaders, modifications); - return {requestHeaders}; - }; - const filter = { - urls: [matchURL], - types: ['xmlhttprequest'] - }; - - let needsCleanup = false; - try { - this._onBeforeSendHeadersAddListener(callback, filter); - needsCleanup = true; - } catch (e) { - // NOP - } - - try { - return await fetch(url, init); - } finally { - if (needsCleanup) { - try { - chrome.webRequest.onBeforeSendHeaders.removeListener(callback); - } catch (e) { - // NOP - } - } - } - } - - _onBeforeSendHeadersAddListener(callback, filter) { - const extraInfoSpec = this._onBeforeSendHeadersExtraInfoSpec; - for (let i = 0; i < 2; ++i) { - try { - chrome.webRequest.onBeforeSendHeaders.addListener(callback, filter, extraInfoSpec); - if (this._extraHeadersSupported === null) { - this._extraHeadersSupported = true; - } - break; - } catch (e) { - // Firefox doesn't support the 'extraHeaders' option and will throw the following error: - // Type error for parameter extraInfoSpec (Error processing 2: Invalid enumeration value "extraHeaders") for webRequest.onBeforeSendHeaders. - if (this._extraHeadersSupported !== null || !`${e.message}`.includes('extraHeaders')) { - throw e; - } - } - - // addListener failed; remove 'extraHeaders' from extraInfoSpec. - this._extraHeadersSupported = false; - const index = extraInfoSpec.indexOf('extraHeaders'); - if (index >= 0) { extraInfoSpec.splice(index, 1); } - } - } - - _getMatchURL(url) { - const url2 = new URL(url); - return `${url2.protocol}//${url2.host}${url2.pathname}`; - } - - _getOriginURL(url) { - const url2 = new URL(url); - return `${url2.protocol}//${url2.host}`; - } - - _modifyHeaders(headers, modifications) { - modifications = new Map(modifications); - - for (let i = 0, ii = headers.length; i < ii; ++i) { - const header = headers[i]; - const name = header.name.toLowerCase(); - const modification = modifications.get(name); - if (typeof modification === 'undefined') { continue; } - - modifications.delete(name); - - if (modification === null) { - headers.splice(i, 1); - --i; - --ii; - } else { - headers[i] = modification; - } - } - - for (const header of modifications.values()) { - if (header !== null) { - headers.push(header); - } - } - } - - async _clearDynamicRules() { - if (!isObject(chrome.declarativeNetRequest)) { return; } - - const rules = this._getDynamicRules(); - - if (rules.length === 0) { return; } - - const removeRuleIds = []; - for (const {id} of rules) { - removeRuleIds.push(id); - } - - await this._updateDynamicRules({removeRuleIds}); - } - - async _fetchAnonymousDeclarative(url, init) { - const id = this._getNewRuleId(); - const originUrl = this._getOriginURL(url); - url = encodeURI(decodeURI(url)); - - this._ruleIds.add(id); - try { - const addRules = [{ - id, - priority: 1, - condition: { - urlFilter: `|${this._escapeDnrUrl(url)}|`, - resourceTypes: ['xmlhttprequest'] - }, - action: { - type: 'modifyHeaders', - requestHeaders: [ - { - operation: 'remove', - header: 'Cookie' - }, - { - operation: 'set', - header: 'Origin', - value: originUrl - } - ], - responseHeaders: [ - { - operation: 'remove', - header: 'Set-Cookie' - } - ] - } - }]; - - await this._updateDynamicRules({addRules}); - try { - return await fetch(url, init); - } finally { - await this._tryUpdateDynamicRules({removeRuleIds: [id]}); - } - } finally { - this._ruleIds.delete(id); - } - } - - _getDynamicRules() { - return new Promise((resolve, reject) => { - chrome.declarativeNetRequest.getDynamicRules((result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - }); - }); - } - - _updateDynamicRules(options) { - return new Promise((resolve, reject) => { - chrome.declarativeNetRequest.updateDynamicRules(options, () => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(); - } - }); - }); - } - - async _tryUpdateDynamicRules(options) { - try { - await this._updateDynamicRules(options); - return true; - } catch (e) { - return false; - } - } - - _getNewRuleId() { - let id = 1; - while (this._ruleIds.has(id)) { - const pre = id; - ++id; - if (id === pre) { throw new Error('Could not generate an id'); } - } - return id; - } - - _escapeDnrUrl(url) { - return url.replace(/[|*^]/g, (char) => this._urlEncodeUtf8(char)); - } - - _urlEncodeUtf8(text) { - const array = this._textEncoder.encode(text); - let result = ''; - for (const byte of array) { - result += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`; - } - return result; - } -} diff --git a/ext/bg/js/search-display-controller.js b/ext/bg/js/search-display-controller.js deleted file mode 100644 index a295346d..00000000 --- a/ext/bg/js/search-display-controller.js +++ /dev/null @@ -1,422 +0,0 @@ -/* - * Copyright (C) 2016-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * ClipboardMonitor - * api - * wanakana - */ - -class SearchDisplayController { - constructor(tabId, frameId, display, japaneseUtil) { - this._tabId = tabId; - this._frameId = frameId; - this._display = display; - this._searchButton = document.querySelector('#search-button'); - this._queryInput = document.querySelector('#search-textbox'); - this._introElement = document.querySelector('#intro'); - this._clipboardMonitorEnableCheckbox = document.querySelector('#clipboard-monitor-enable'); - this._wanakanaEnableCheckbox = document.querySelector('#wanakana-enable'); - this._queryInputEvents = new EventListenerCollection(); - this._queryInputEventsSetup = false; - this._wanakanaEnabled = false; - this._introVisible = true; - this._introAnimationTimer = null; - this._clipboardMonitorEnabled = false; - this._clipboardMonitor = new ClipboardMonitor({ - japaneseUtil, - clipboardReader: { - getText: async () => (await api.clipboardGet()) - } - }); - this._messageHandlers = new Map(); - this._mode = null; - } - - async prepare() { - this._updateMode(); - - await this._display.updateOptions(); - - chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); - yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this)); - - this._display.on('optionsUpdated', this._onDisplayOptionsUpdated.bind(this)); - this._display.on('contentUpdating', this._onContentUpdating.bind(this)); - - this._display.hotkeyHandler.registerActions([ - ['focusSearchBox', this._onActionFocusSearchBox.bind(this)] - ]); - this._registerMessageHandlers([ - ['getMode', {async: false, handler: this._onMessageGetMode.bind(this)}], - ['setMode', {async: false, handler: this._onMessageSetMode.bind(this)}], - ['updateSearchQuery', {async: false, handler: this._onExternalSearchUpdate.bind(this)}] - ]); - - this._display.autoPlayAudioDelay = 0; - this._display.queryParserVisible = true; - this._display.setHistorySettings({useBrowserHistory: true}); - this._display.setQueryPostProcessor(this._postProcessQuery.bind(this)); - - this._searchButton.addEventListener('click', this._onSearch.bind(this), false); - this._wanakanaEnableCheckbox.addEventListener('change', this._onWanakanaEnableChange.bind(this)); - window.addEventListener('copy', this._onCopy.bind(this)); - this._clipboardMonitor.on('change', this._onExternalSearchUpdate.bind(this)); - this._clipboardMonitorEnableCheckbox.addEventListener('change', this._onClipboardMonitorEnableChange.bind(this)); - this._display.hotkeyHandler.on('keydownNonHotkey', this._onKeyDown.bind(this)); - - this._onDisplayOptionsUpdated({options: this._display.getOptions()}); - } - - // Actions - - _onActionFocusSearchBox() { - if (this._queryInput === null) { return; } - this._queryInput.focus(); - this._queryInput.select(); - } - - // Messages - - _onMessageSetMode({mode}) { - this._setMode(mode, true); - } - - _onMessageGetMode() { - return this._mode; - } - - // Private - - _onMessage({action, params}, sender, callback) { - const messageHandler = this._messageHandlers.get(action); - if (typeof messageHandler === 'undefined') { return false; } - return yomichan.invokeMessageHandler(messageHandler, params, callback, sender); - } - - _onKeyDown(e) { - if ( - document.activeElement !== this._queryInput && - !e.ctrlKey && - !e.metaKey && - !e.altKey && - e.key.length === 1 - ) { - this._queryInput.focus({preventScroll: true}); - } - } - - async _onOptionsUpdated() { - await this._display.updateOptions(); - const query = this._queryInput.value; - if (query) { - this._display.searchLast(); - } - } - - _onDisplayOptionsUpdated({options}) { - this._clipboardMonitorEnabled = options.clipboard.enableSearchPageMonitor; - this._updateClipboardMonitorEnabled(); - - const enableWanakana = !!this._display.getOptions().general.enableWanakana; - this._wanakanaEnableCheckbox.checked = enableWanakana; - this._setWanakanaEnabled(enableWanakana); - } - - _onContentUpdating({type, content, source}) { - let animate = false; - let valid = false; - switch (type) { - case 'terms': - case 'kanji': - animate = !!content.animate; - valid = (typeof source === 'string' && source.length > 0); - this._display.blurElement(this._queryInput); - break; - case 'clear': - valid = false; - animate = true; - source = ''; - break; - } - - if (typeof source !== 'string') { source = ''; } - - if (this._queryInput.value !== source) { - this._queryInput.value = source; - this._updateSearchHeight(true); - } - this._setIntroVisible(!valid, animate); - } - - _onSearchInput() { - this._updateSearchHeight(false); - } - - _onSearchKeydown(e) { - const {code} = e; - if (!((code === 'Enter' || code === 'NumpadEnter') && !e.shiftKey)) { return; } - - // Search - e.preventDefault(); - e.stopImmediatePropagation(); - this._display.blurElement(e.currentTarget); - this._search(true, true, true); - } - - _onSearch(e) { - e.preventDefault(); - this._search(true, true, true); - } - - _onCopy() { - // ignore copy from search page - this._clipboardMonitor.setPreviousText(window.getSelection().toString().trim()); - } - - _onExternalSearchUpdate({text, animate=true}) { - const {clipboard: {autoSearchContent, maximumSearchLength}} = this._display.getOptions(); - if (text.length > maximumSearchLength) { - text = text.substring(0, maximumSearchLength); - } - this._queryInput.value = text; - this._updateSearchHeight(true); - this._search(animate, false, autoSearchContent); - } - - _onWanakanaEnableChange(e) { - const value = e.target.checked; - this._setWanakanaEnabled(value); - api.modifySettings([{ - action: 'set', - path: 'general.enableWanakana', - value, - scope: 'profile', - optionsContext: this._display.getOptionsContext() - }], 'search'); - } - - _onClipboardMonitorEnableChange(e) { - const enabled = e.target.checked; - this._setClipboardMonitorEnabled(enabled); - } - - _setWanakanaEnabled(enabled) { - if (this._queryInputEventsSetup && this._wanakanaEnabled === enabled) { return; } - - const input = this._queryInput; - this._queryInputEvents.removeAllEventListeners(); - this._queryInputEvents.addEventListener(input, 'keydown', this._onSearchKeydown.bind(this), false); - - this._wanakanaEnabled = enabled; - if (enabled) { - wanakana.bind(input); - } else { - wanakana.unbind(input); - } - - this._queryInputEvents.addEventListener(input, 'input', this._onSearchInput.bind(this), false); - this._queryInputEventsSetup = true; - } - - _setIntroVisible(visible, animate) { - if (this._introVisible === visible) { - return; - } - - this._introVisible = visible; - - if (this._introElement === null) { - return; - } - - if (this._introAnimationTimer !== null) { - clearTimeout(this._introAnimationTimer); - this._introAnimationTimer = null; - } - - if (visible) { - this._showIntro(animate); - } else { - this._hideIntro(animate); - } - } - - _showIntro(animate) { - if (animate) { - const duration = 0.4; - this._introElement.style.transition = ''; - this._introElement.style.height = ''; - const size = this._introElement.getBoundingClientRect(); - this._introElement.style.height = '0px'; - this._introElement.style.transition = `height ${duration}s ease-in-out 0s`; - window.getComputedStyle(this._introElement).getPropertyValue('height'); // Commits height so next line can start animation - this._introElement.style.height = `${size.height}px`; - this._introAnimationTimer = setTimeout(() => { - this._introElement.style.height = ''; - this._introAnimationTimer = null; - }, duration * 1000); - } else { - this._introElement.style.transition = ''; - this._introElement.style.height = ''; - } - } - - _hideIntro(animate) { - if (animate) { - const duration = 0.4; - const size = this._introElement.getBoundingClientRect(); - this._introElement.style.height = `${size.height}px`; - this._introElement.style.transition = `height ${duration}s ease-in-out 0s`; - window.getComputedStyle(this._introElement).getPropertyValue('height'); // Commits height so next line can start animation - } else { - this._introElement.style.transition = ''; - } - this._introElement.style.height = '0'; - } - - async _setClipboardMonitorEnabled(value) { - let modify = true; - if (value) { - value = await this._requestPermissions(['clipboardRead']); - modify = value; - } - - this._clipboardMonitorEnabled = value; - this._updateClipboardMonitorEnabled(); - - if (!modify) { return; } - - await api.modifySettings([{ - action: 'set', - path: 'clipboard.enableSearchPageMonitor', - value, - scope: 'profile', - optionsContext: this._display.getOptionsContext() - }], 'search'); - } - - _updateClipboardMonitorEnabled() { - const enabled = this._clipboardMonitorEnabled; - this._clipboardMonitorEnableCheckbox.checked = enabled; - if (enabled && this._mode !== 'popup') { - this._clipboardMonitor.start(); - } else { - this._clipboardMonitor.stop(); - } - } - - _requestPermissions(permissions) { - return new Promise((resolve) => { - chrome.permissions.request( - {permissions}, - (granted) => { - const e = chrome.runtime.lastError; - resolve(!e && granted); - } - ); - }); - } - - _search(animate, history, lookup) { - const query = this._queryInput.value; - const depth = this._display.depth; - const url = window.location.href; - const documentTitle = document.title; - const details = { - focus: false, - history, - params: { - query - }, - state: { - focusEntry: 0, - optionsContext: {depth, url}, - url, - sentence: {text: query, offset: 0}, - documentTitle - }, - content: { - definitions: null, - animate, - contentOrigin: { - tabId: this.tabId, - frameId: this.frameId - } - } - }; - if (!lookup) { details.params.lookup = 'false'; } - this._display.setContent(details); - } - - _updateSearchHeight(shrink) { - const node = this._queryInput; - if (shrink) { - node.style.height = '0'; - } - const {scrollHeight} = node; - const currentHeight = node.getBoundingClientRect().height; - if (shrink || scrollHeight >= currentHeight - 1) { - node.style.height = `${scrollHeight}px`; - } - } - - _postProcessQuery(query) { - if (this._wanakanaEnabled) { - try { - query = this._japaneseUtil.convertToKana(query); - } catch (e) { - // NOP - } - } - return query; - } - - _registerMessageHandlers(handlers) { - for (const [name, handlerInfo] of handlers) { - this._messageHandlers.set(name, handlerInfo); - } - } - - _updateMode() { - let mode = null; - try { - mode = sessionStorage.getItem('mode'); - } catch (e) { - // Browsers can throw a SecurityError when cookie blocking is enabled. - } - this._setMode(mode, false); - } - - _setMode(mode, save) { - if (mode === this._mode) { return; } - if (save) { - try { - if (mode === null) { - sessionStorage.removeItem('mode'); - } else { - sessionStorage.setItem('mode', mode); - } - } catch (e) { - // Browsers can throw a SecurityError when cookie blocking is enabled. - } - } - this._mode = mode; - document.documentElement.dataset.searchMode = (mode !== null ? mode : ''); - this._updateClipboardMonitorEnabled(); - } -} diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js deleted file mode 100644 index c7ec595a..00000000 --- a/ext/bg/js/search-main.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2019-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * Display - * DocumentFocusController - * HotkeyHandler - * JapaneseUtil - * SearchDisplayController - * api - * wanakana - */ - -(async () => { - try { - const documentFocusController = new DocumentFocusController(); - documentFocusController.prepare(); - - api.forwardLogsToBackend(); - await yomichan.backendReady(); - - const {tabId, frameId} = await api.frameInformationGet(); - - const japaneseUtil = new JapaneseUtil(wanakana); - - const hotkeyHandler = new HotkeyHandler(); - hotkeyHandler.prepare(); - - const display = new Display(tabId, frameId, 'search', japaneseUtil, documentFocusController, hotkeyHandler); - await display.prepare(); - - const searchDisplayController = new SearchDisplayController(tabId, frameId, display, japaneseUtil); - await searchDisplayController.prepare(); - - display.initializeState(); - - document.documentElement.dataset.loaded = 'true'; - - yomichan.ready(); - } catch (e) { - yomichan.logError(e); - } -})(); diff --git a/ext/bg/js/simple-dom-parser.js b/ext/bg/js/simple-dom-parser.js deleted file mode 100644 index 7c57ca98..00000000 --- a/ext/bg/js/simple-dom-parser.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* globals - * parse5 - */ - -class SimpleDOMParser { - constructor(content) { - this._document = parse5.parse(content); - } - - getElementById(id, root=null) { - for (const node of this._allNodes(root)) { - if (typeof node.tagName === 'string' && this.getAttribute(node, 'id') === id) { - return node; - } - } - return null; - } - - getElementByTagName(tagName, root=null) { - for (const node of this._allNodes(root)) { - if (node.tagName === tagName) { - return node; - } - } - return null; - } - - getElementsByTagName(tagName, root=null) { - const results = []; - for (const node of this._allNodes(root)) { - if (node.tagName === tagName) { - results.push(node); - } - } - return results; - } - - getElementsByClassName(className, root=null) { - const results = []; - const classNamePattern = new RegExp(`(^|\\s)${escapeRegExp(className)}(\\s|$)`); - for (const node of this._allNodes(root)) { - if (typeof node.tagName === 'string') { - const nodeClassName = this.getAttribute(node, 'class'); - if (nodeClassName !== null && classNamePattern.test(nodeClassName)) { - results.push(node); - } - } - } - return results; - } - - getAttribute(element, attribute) { - for (const attr of element.attrs) { - if ( - attr.name === attribute && - typeof attr.namespace === 'undefined' - ) { - return attr.value; - } - } - return null; - } - - getTextContent(element) { - let source = ''; - for (const node of this._allNodes(element)) { - if (node.nodeName === '#text') { - source += node.value; - } - } - return source; - } - - static isSupported() { - return typeof parse5 !== 'undefined'; - } - - // Private - - *_allNodes(root) { - if (root === null) { - root = this._document; - } - - // Depth-first pre-order traversal - const nodeQueue = [root]; - while (nodeQueue.length > 0) { - const node = nodeQueue.pop(); - - yield node; - - const childNodes = node.childNodes; - if (typeof childNodes !== 'undefined') { - for (let i = childNodes.length - 1; i >= 0; --i) { - nodeQueue.push(childNodes[i]); - } - } - } - } -} diff --git a/ext/bg/js/template-patcher.js b/ext/bg/js/template-patcher.js deleted file mode 100644 index 57178957..00000000 --- a/ext/bg/js/template-patcher.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class TemplatePatcher { - constructor() { - this._diffPattern1 = /\n?\{\{<<<<<<<\}\}\n/g; - this._diffPattern2 = /\n\{\{=======\}\}\n/g; - this._diffPattern3 = /\n\{\{>>>>>>>\}\}\n*/g; - this._lookupMarkerPattern = /[ \t]*\{\{~?>\s*\(\s*lookup\s*\.\s*"marker"\s*\)\s*~?\}\}/g; - } - - parsePatch(content) { - const diffPattern1 = this._diffPattern1; - const diffPattern2 = this._diffPattern2; - const diffPattern3 = this._diffPattern3; - const modifications = []; - let index = 0; - - while (true) { - // Find modification boundaries - diffPattern1.lastIndex = index; - const m1 = diffPattern1.exec(content); - if (m1 === null) { break; } - - diffPattern2.lastIndex = m1.index + m1[0].length; - const m2 = diffPattern2.exec(content); - if (m2 === null) { break; } - - diffPattern3.lastIndex = m2.index + m2[0].length; - const m3 = diffPattern3.exec(content); - if (m3 === null) { break; } - - // Construct - const current = content.substring(m1.index + m1[0].length, m2.index); - const replacement = content.substring(m2.index + m2[0].length, m3.index); - - if (current.length > 0) { - modifications.push({current, replacement}); - } - - // Update - content = content.substring(0, m1.index) + content.substring(m3.index + m3[0].length); - index = m1.index; - } - - return {addition: content, modifications}; - } - - applyPatch(template, patch) { - for (const {current, replacement} of patch.modifications) { - let fromIndex = 0; - while (true) { - const index = template.indexOf(current, fromIndex); - if (index < 0) { break; } - template = template.substring(0, index) + replacement + template.substring(index + current.length); - fromIndex = index + replacement.length; - } - } - template = this._addFieldTemplatesBeforeEnd(template, patch.addition); - return template; - } - - // Private - - _addFieldTemplatesBeforeEnd(template, addition) { - const newline = '\n'; - let replaced = false; - template = template.replace(this._lookupMarkerPattern, (g0) => { - replaced = true; - return `${addition}${newline}${g0}`; - }); - if (!replaced) { - template += newline; - template += addition; - } - return template; - } -} diff --git a/ext/bg/js/template-renderer-frame-api.js b/ext/bg/js/template-renderer-frame-api.js deleted file mode 100644 index 4936a2af..00000000 --- a/ext/bg/js/template-renderer-frame-api.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class TemplateRendererFrameApi { - constructor(templateRenderer) { - this._templateRenderer = templateRenderer; - this._windowMessageHandlers = new Map([ - ['render', {async: true, handler: this._onRender.bind(this)}] - ]); - } - - prepare() { - window.addEventListener('message', this._onWindowMessage.bind(this), false); - } - - // Private - - _onWindowMessage(e) { - const {source, data: {action, params, id}} = e; - const messageHandler = this._windowMessageHandlers.get(action); - if (typeof messageHandler === 'undefined') { return; } - - this._onWindowMessageInner(messageHandler, action, params, source, id); - } - - async _onWindowMessageInner({handler, async}, action, params, source, id) { - let response; - try { - let result = handler(params); - if (async) { - result = await result; - } - response = {result}; - } catch (error) { - response = {error: this._errorToJson(error)}; - } - - if (typeof id === 'undefined') { return; } - source.postMessage({action: `${action}.response`, params: response, id}, '*'); - } - - async _onRender({template, data, type}) { - return await this._templateRenderer.render(template, data, type); - } - - _errorToJson(error) { - try { - if (error !== null && typeof error === 'object') { - return { - name: error.name, - message: error.message, - stack: error.stack, - data: error.data - }; - } - } catch (e) { - // NOP - } - return { - value: error, - hasValue: true - }; - } -} diff --git a/ext/bg/js/template-renderer-frame-main.js b/ext/bg/js/template-renderer-frame-main.js deleted file mode 100644 index d25eb56d..00000000 --- a/ext/bg/js/template-renderer-frame-main.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* globals - * AnkiNoteData - * JapaneseUtil - * TemplateRenderer - * TemplateRendererFrameApi - */ - -(() => { - const japaneseUtil = new JapaneseUtil(null); - const templateRenderer = new TemplateRenderer(japaneseUtil); - templateRenderer.registerDataType('ankiNote', { - modifier: ({data, marker}) => new AnkiNoteData(data, marker).createPublic() - }); - const api = new TemplateRendererFrameApi(templateRenderer); - api.prepare(); -})(); diff --git a/ext/bg/js/template-renderer-proxy.js b/ext/bg/js/template-renderer-proxy.js deleted file mode 100644 index 6a49832b..00000000 --- a/ext/bg/js/template-renderer-proxy.js +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class TemplateRendererProxy { - constructor() { - this._frame = null; - this._frameNeedsLoad = true; - this._frameLoading = false; - this._frameLoadPromise = null; - this._frameUrl = chrome.runtime.getURL('/template-renderer.html'); - this._invocations = new Set(); - } - - async render(template, data, type) { - await this._prepareFrame(); - return await this._invoke('render', {template, data, type}); - } - - // Private - - async _prepareFrame() { - if (this._frame === null) { - this._frame = document.createElement('iframe'); - this._frame.addEventListener('load', this._onFrameLoad.bind(this), false); - const style = this._frame.style; - style.opacity = '0'; - style.width = '0'; - style.height = '0'; - style.position = 'absolute'; - style.border = '0'; - style.margin = '0'; - style.padding = '0'; - style.pointerEvents = 'none'; - } - if (this._frameNeedsLoad) { - this._frameNeedsLoad = false; - this._frameLoading = true; - this._frameLoadPromise = this._loadFrame(this._frame, this._frameUrl) - .finally(() => { this._frameLoading = false; }); - } - await this._frameLoadPromise; - } - - _loadFrame(frame, url, timeout=5000) { - return new Promise((resolve, reject) => { - let ready = false; - const cleanup = () => { - frame.removeEventListener('load', onLoad, false); - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - }; - const onLoad = () => { - if (!ready) { return; } - cleanup(); - resolve(); - }; - - let timer = setTimeout(() => { - cleanup(); - reject(new Error('Timeout')); - }, timeout); - - frame.removeAttribute('src'); - frame.removeAttribute('srcdoc'); - frame.addEventListener('load', onLoad, false); - try { - document.body.appendChild(frame); - ready = true; - frame.contentDocument.location.href = url; - } catch (e) { - cleanup(); - reject(e); - } - }); - } - - _invoke(action, params, timeout=null) { - return new Promise((resolve, reject) => { - const frameWindow = (this._frame !== null ? this._frame.contentWindow : null); - if (frameWindow === null) { - reject(new Error('Frame not set up')); - return; - } - - const id = generateId(16); - const invocation = { - cancel: () => { - cleanup(); - reject(new Error('Terminated')); - } - }; - - const cleanup = () => { - this._invocations.delete(invocation); - window.removeEventListener('message', onMessage, false); - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - }; - const onMessage = (e) => { - if ( - e.source !== frameWindow || - e.data.id !== id || - e.data.action !== `${action}.response` - ) { - return; - } - - const response = e.data.params; - cleanup(); - const {error} = response; - if (error) { - reject(deserializeError(error)); - } else { - resolve(response.result); - } - }; - - let timer = (typeof timeout === 'number' ? setTimeout(() => { - cleanup(); - reject(new Error('Timeout')); - }, timeout) : null); - - this._invocations.add(invocation); - - window.addEventListener('message', onMessage, false); - frameWindow.postMessage({action, params, id}, '*'); - }); - } - - _onFrameLoad() { - if (this._frameLoading) { return; } - this._frameNeedsLoad = true; - - for (const invocation of this._invocations) { - invocation.cancel(); - } - this._invocations.clear(); - } -} diff --git a/ext/bg/js/template-renderer.js b/ext/bg/js/template-renderer.js deleted file mode 100644 index ae39e478..00000000 --- a/ext/bg/js/template-renderer.js +++ /dev/null @@ -1,416 +0,0 @@ -/* - * Copyright (C) 2016-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * Handlebars - */ - -class TemplateRenderer { - constructor(japaneseUtil) { - this._japaneseUtil = japaneseUtil; - this._cache = new Map(); - this._cacheMaxSize = 5; - this._helpersRegistered = false; - this._stateStack = null; - this._dataTypes = new Map(); - } - - registerDataType(name, {modifier=null, modifierPost=null}) { - this._dataTypes.set(name, {modifier, modifierPost}); - } - - async render(template, data, type) { - if (!this._helpersRegistered) { - this._registerHelpers(); - this._helpersRegistered = true; - } - - const cache = this._cache; - let instance = cache.get(template); - if (typeof instance === 'undefined') { - this._updateCacheSize(this._cacheMaxSize - 1); - instance = Handlebars.compile(template); - cache.set(template, instance); - } - - let modifier = null; - let modifierPost = null; - if (typeof type === 'string') { - const typeInfo = this._dataTypes.get(type); - if (typeof typeInfo !== 'undefined') { - ({modifier, modifierPost} = typeInfo); - } - } - - try { - if (typeof modifier === 'function') { - data = modifier(data); - } - - this._stateStack = [new Map()]; - return instance(data).trim(); - } finally { - this._stateStack = null; - - if (typeof modifierPost === 'function') { - modifierPost(data); - } - } - } - - // Private - - _updateCacheSize(maxSize) { - const cache = this._cache; - let removeCount = cache.size - maxSize; - if (removeCount <= 0) { return; } - - for (const key of cache.keys()) { - cache.delete(key); - if (--removeCount <= 0) { break; } - } - } - - _registerHelpers() { - Handlebars.partials = Handlebars.templates; - - const helpers = [ - ['dumpObject', this._dumpObject.bind(this)], - ['furigana', this._furigana.bind(this)], - ['furiganaPlain', this._furiganaPlain.bind(this)], - ['kanjiLinks', this._kanjiLinks.bind(this)], - ['multiLine', this._multiLine.bind(this)], - ['sanitizeCssClass', this._sanitizeCssClass.bind(this)], - ['regexReplace', this._regexReplace.bind(this)], - ['regexMatch', this._regexMatch.bind(this)], - ['mergeTags', this._mergeTags.bind(this)], - ['eachUpTo', this._eachUpTo.bind(this)], - ['spread', this._spread.bind(this)], - ['op', this._op.bind(this)], - ['get', this._get.bind(this)], - ['set', this._set.bind(this)], - ['scope', this._scope.bind(this)], - ['property', this._property.bind(this)], - ['noop', this._noop.bind(this)], - ['isMoraPitchHigh', this._isMoraPitchHigh.bind(this)], - ['getKanaMorae', this._getKanaMorae.bind(this)], - ['typeof', this._getTypeof.bind(this)] - ]; - - for (const [name, helper] of helpers) { - this._registerHelper(name, helper); - } - } - - _registerHelper(name, helper) { - function wrapper(...args) { - return helper(this, ...args); - } - Handlebars.registerHelper(name, wrapper); - } - - _escape(text) { - return Handlebars.Utils.escapeExpression(text); - } - - _dumpObject(context, options) { - const dump = JSON.stringify(options.fn(context), null, 4); - return this._escape(dump); - } - - _furigana(context, ...args) { - const {expression, reading} = this._getFuriganaExpressionAndReading(context, ...args); - const segs = this._japaneseUtil.distributeFurigana(expression, reading); - - let result = ''; - for (const seg of segs) { - if (seg.furigana.length > 0) { - result += `<ruby>${seg.text}<rt>${seg.furigana}</rt></ruby>`; - } else { - result += seg.text; - } - } - - return result; - } - - _furiganaPlain(context, ...args) { - const {expression, reading} = this._getFuriganaExpressionAndReading(context, ...args); - const segs = this._japaneseUtil.distributeFurigana(expression, reading); - - let result = ''; - for (const seg of segs) { - if (seg.furigana.length > 0) { - if (result.length > 0) { result += ' '; } - result += `${seg.text}[${seg.furigana}]`; - } else { - result += seg.text; - } - } - - return result; - } - - _getFuriganaExpressionAndReading(context, ...args) { - const options = args[args.length - 1]; - if (args.length >= 3) { - return {expression: args[0], reading: args[1]}; - } else { - const {expression, reading} = options.fn(context); - return {expression, reading}; - } - } - - _kanjiLinks(context, options) { - const jp = this._japaneseUtil; - let result = ''; - for (const c of options.fn(context)) { - if (jp.isCodePointKanji(c.codePointAt(0))) { - result += `<a href="#" class="kanji-link">${c}</a>`; - } else { - result += c; - } - } - - return result; - } - - _multiLine(context, options) { - return options.fn(context).split('\n').join('<br>'); - } - - _sanitizeCssClass(context, options) { - return options.fn(context).replace(/[^_a-z0-9\u00a0-\uffff]/ig, '_'); - } - - _regexReplace(context, ...args) { - // Usage: - // {{#regexReplace regex string [flags]}}content{{/regexReplace}} - // regex: regular expression string - // string: string to replace - // flags: optional flags for regular expression - // e.g. "i" for case-insensitive, "g" for replace all - let value = args[args.length - 1].fn(context); - if (args.length >= 3) { - try { - const flags = args.length > 3 ? args[2] : 'g'; - const regex = new RegExp(args[0], flags); - value = value.replace(regex, args[1]); - } catch (e) { - return `${e}`; - } - } - return value; - } - - _regexMatch(context, ...args) { - // Usage: - // {{#regexMatch regex [flags]}}content{{/regexMatch}} - // regex: regular expression string - // flags: optional flags for regular expression - // e.g. "i" for case-insensitive, "g" for match all - let value = args[args.length - 1].fn(context); - if (args.length >= 2) { - try { - const flags = args.length > 2 ? args[1] : ''; - const regex = new RegExp(args[0], flags); - const parts = []; - value.replace(regex, (g0) => parts.push(g0)); - value = parts.join(''); - } catch (e) { - return `${e}`; - } - } - return value; - } - - _mergeTags(context, object, isGroupMode, isMergeMode) { - const tagSources = []; - if (isGroupMode || isMergeMode) { - for (const definition of object.definitions) { - tagSources.push(definition.definitionTags); - } - } else { - tagSources.push(object.definitionTags); - } - - const tags = new Set(); - for (const tagSource of tagSources) { - for (const tag of tagSource) { - tags.add(tag.name); - } - } - - return [...tags].join(', '); - } - - _eachUpTo(context, iterable, maxCount, options) { - if (iterable) { - const results = []; - let any = false; - for (const entry of iterable) { - any = true; - if (results.length >= maxCount) { break; } - const processedEntry = options.fn(entry); - results.push(processedEntry); - } - if (any) { - return results.join(''); - } - } - return options.inverse(context); - } - - _spread(context, ...args) { - const result = []; - for (let i = 0, ii = args.length - 1; i < ii; ++i) { - try { - result.push(...args[i]); - } catch (e) { - // NOP - } - } - return result; - } - - _op(context, ...args) { - switch (args.length) { - case 3: return this._evaluateUnaryExpression(args[0], args[1]); - case 4: return this._evaluateBinaryExpression(args[0], args[1], args[2]); - case 5: return this._evaluateTernaryExpression(args[0], args[1], args[2], args[3]); - default: return void 0; - } - } - - _evaluateUnaryExpression(operator, operand1) { - switch (operator) { - case '+': return +operand1; - case '-': return -operand1; - case '~': return ~operand1; - case '!': return !operand1; - default: return void 0; - } - } - - _evaluateBinaryExpression(operator, operand1, operand2) { - switch (operator) { - case '+': return operand1 + operand2; - case '-': return operand1 - operand2; - case '/': return operand1 / operand2; - case '*': return operand1 * operand2; - case '%': return operand1 % operand2; - case '**': return operand1 ** operand2; - case '==': return operand1 == operand2; // eslint-disable-line eqeqeq - case '!=': return operand1 != operand2; // eslint-disable-line eqeqeq - case '===': return operand1 === operand2; - case '!==': return operand1 !== operand2; - case '<': return operand1 < operand2; - case '<=': return operand1 <= operand2; - case '>': return operand1 > operand2; - case '>=': return operand1 >= operand2; - case '<<': return operand1 << operand2; - case '>>': return operand1 >> operand2; - case '>>>': return operand1 >>> operand2; - case '&': return operand1 & operand2; - case '|': return operand1 | operand2; - case '^': return operand1 ^ operand2; - case '&&': return operand1 && operand2; - case '||': return operand1 || operand2; - default: return void 0; - } - } - - _evaluateTernaryExpression(operator, operand1, operand2, operand3) { - switch (operator) { - case '?:': return operand1 ? operand2 : operand3; - default: return void 0; - } - } - - _get(context, key) { - for (let i = this._stateStack.length; --i >= 0;) { - const map = this._stateStack[i]; - if (map.has(key)) { - return map.get(key); - } - } - return void 0; - } - - _set(context, ...args) { - switch (args.length) { - case 2: - { - const [key, options] = args; - const value = options.fn(context); - this._stateStack[this._stateStack.length - 1].set(key, value); - } - break; - case 3: - { - const [key, value] = args; - this._stateStack[this._stateStack.length - 1].set(key, value); - } - break; - } - return ''; - } - - _scope(context, options) { - try { - this._stateStack.push(new Map()); - return options.fn(context); - } finally { - if (this._stateStack.length > 1) { - this._stateStack.pop(); - } - } - } - - _property(context, ...args) { - const ii = args.length - 1; - if (ii <= 0) { return void 0; } - - try { - let value = args[0]; - for (let i = 1; i < ii; ++i) { - value = value[args[i]]; - } - return value; - } catch (e) { - return void 0; - } - } - - _noop(context, options) { - return options.fn(context); - } - - _isMoraPitchHigh(context, index, position) { - return this._japaneseUtil.isMoraPitchHigh(index, position); - } - - _getKanaMorae(context, text) { - return this._japaneseUtil.getKanaMorae(`${text}`); - } - - _getTypeof(context, ...args) { - const ii = args.length - 1; - const value = (ii > 0 ? args[0] : args[ii].fn(context)); - return typeof value; - } -} diff --git a/ext/bg/js/text-source-map.js b/ext/bg/js/text-source-map.js deleted file mode 100644 index 49b6d99f..00000000 --- a/ext/bg/js/text-source-map.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2020-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -class TextSourceMap { - constructor(source, mapping=null) { - this._source = source; - this._mapping = (mapping !== null ? TextSourceMap.normalizeMapping(mapping) : null); - } - - get source() { - return this._source; - } - - equals(other) { - if (this === other) { - return true; - } - - const source = this._source; - if (!(other instanceof TextSourceMap && source === other.source)) { - return false; - } - - let mapping = this._mapping; - let otherMapping = other.getMappingCopy(); - if (mapping === null) { - if (otherMapping === null) { - return true; - } - mapping = TextSourceMap.createMapping(source); - } else if (otherMapping === null) { - otherMapping = TextSourceMap.createMapping(source); - } - - const mappingLength = mapping.length; - if (mappingLength !== otherMapping.length) { - return false; - } - - for (let i = 0; i < mappingLength; ++i) { - if (mapping[i] !== otherMapping[i]) { - return false; - } - } - - return true; - } - - getSourceLength(finalLength) { - const mapping = this._mapping; - if (mapping === null) { - return finalLength; - } - - let sourceLength = 0; - for (let i = 0; i < finalLength; ++i) { - sourceLength += mapping[i]; - } - return sourceLength; - } - - combine(index, count) { - if (count <= 0) { return; } - - if (this._mapping === null) { - this._mapping = TextSourceMap.createMapping(this._source); - } - - let sum = this._mapping[index]; - const parts = this._mapping.splice(index + 1, count); - for (const part of parts) { - sum += part; - } - this._mapping[index] = sum; - } - - insert(index, ...items) { - if (this._mapping === null) { - this._mapping = TextSourceMap.createMapping(this._source); - } - - this._mapping.splice(index, 0, ...items); - } - - getMappingCopy() { - return this._mapping !== null ? [...this._mapping] : null; - } - - static createMapping(text) { - return new Array(text.length).fill(1); - } - - static normalizeMapping(mapping) { - const result = []; - for (const value of mapping) { - result.push( - (typeof value === 'number' && Number.isFinite(value)) ? - Math.floor(value) : - 0 - ); - } - return result; - } -} diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js deleted file mode 100644 index 729c8294..00000000 --- a/ext/bg/js/translator.js +++ /dev/null @@ -1,1397 +0,0 @@ -/* - * Copyright (C) 2016-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * Deinflector - * TextSourceMap - */ - -/** - * Class which finds term and kanji definitions for text. - */ -class Translator { - /** - * Creates a new Translator instance. - * @param database An instance of DictionaryDatabase. - */ - constructor({japaneseUtil, database}) { - this._japaneseUtil = japaneseUtil; - this._database = database; - this._deinflector = null; - this._tagCache = new Map(); - this._stringComparer = new Intl.Collator('en-US'); // Invariant locale - } - - /** - * Initializes the instance for use. The public API should not be used until - * this function has been called. - * @param deinflectionReasons The raw deinflections reasons data that the Deinflector uses. - */ - prepare(deinflectionReasons) { - this._deinflector = new Deinflector(deinflectionReasons); - } - - /** - * Clears the database tag cache. This should be executed if the database is changed. - */ - clearDatabaseCaches() { - this._tagCache.clear(); - } - - /** - * Finds term definitions for the given text. - * @param mode The mode to use for finding terms, which determines the format of the resulting array. - * One of: 'group', 'merge', 'split', 'simple' - * @param text The text to find terms for. - * @param options An object using the following structure: - * { - * wildcard: (enum: null, 'prefix', 'suffix'), - * mainDictionary: (string), - * alphanumeric: (boolean), - * convertHalfWidthCharacters: (enum: 'false', 'true', 'variant'), - * convertNumericCharacters: (enum: 'false', 'true', 'variant'), - * convertAlphabeticCharacters: (enum: 'false', 'true', 'variant'), - * convertHiraganaToKatakana: (enum: 'false', 'true', 'variant'), - * convertKatakanaToHiragana: (enum: 'false', 'true', 'variant'), - * collapseEmphaticSequences: (enum: 'false', 'true', 'full'), - * textReplacements: [ - * (null or [ - * {pattern: (RegExp), replacement: (string)} - * ... - * ]) - * ... - * ], - * enabledDictionaryMap: (Map of [ - * (string), - * { - * priority: (number), - * allowSecondarySearches: (boolean) - * } - * ]) - * } - * @returns An array of [definitions, textLength]. The structure of each definition depends on the - * mode parameter, see the _create?TermDefinition?() functions for structure details. - */ - async findTerms(mode, text, options) { - switch (mode) { - case 'group': - return await this._findTermsGrouped(text, options); - case 'merge': - return await this._findTermsMerged(text, options); - case 'split': - return await this._findTermsSplit(text, options); - case 'simple': - return await this._findTermsSimple(text, options); - default: - return [[], 0]; - } - } - - /** - * Finds kanji definitions for the given text. - * @param text The text to find kanji definitions for. This string can be of any length, - * but is typically just one character, which is a single kanji. If the string is multiple - * characters long, each character will be searched in the database. - * @param options An object using the following structure: - * { - * enabledDictionaryMap: (Map of [ - * (string), - * { - * priority: (number) - * } - * ]) - * } - * @returns An array of definitions. See the _createKanjiDefinition() function for structure details. - */ - async findKanji(text, options) { - const {enabledDictionaryMap} = options; - const kanjiUnique = new Set(); - for (const c of text) { - kanjiUnique.add(c); - } - - const databaseDefinitions = await this._database.findKanjiBulk([...kanjiUnique], enabledDictionaryMap); - if (databaseDefinitions.length === 0) { return []; } - - this._sortDatabaseDefinitionsByIndex(databaseDefinitions); - - const definitions = []; - for (const {character, onyomi, kunyomi, tags, glossary, stats, dictionary} of databaseDefinitions) { - const expandedStats = await this._expandStats(stats, dictionary); - const expandedTags = await this._expandTags(tags, dictionary); - this._sortTags(expandedTags); - - const definition = this._createKanjiDefinition(character, dictionary, onyomi, kunyomi, glossary, expandedTags, expandedStats); - definitions.push(definition); - } - - await this._buildKanjiMeta(definitions, enabledDictionaryMap); - - return definitions; - } - - // Find terms core functions - - async _findTermsSimple(text, options) { - const {enabledDictionaryMap} = options; - const [definitions, length] = await this._findTermsInternal(text, enabledDictionaryMap, options); - this._sortDefinitions(definitions, false); - return [definitions, length]; - } - - async _findTermsSplit(text, options) { - const {enabledDictionaryMap} = options; - const [definitions, length] = await this._findTermsInternal(text, enabledDictionaryMap, options); - await this._buildTermMeta(definitions, enabledDictionaryMap); - this._sortDefinitions(definitions, true); - return [definitions, length]; - } - - async _findTermsGrouped(text, options) { - const {enabledDictionaryMap} = options; - const [definitions, length] = await this._findTermsInternal(text, enabledDictionaryMap, options); - - const groupedDefinitions = this._groupTerms(definitions, enabledDictionaryMap); - await this._buildTermMeta(groupedDefinitions, enabledDictionaryMap); - this._sortDefinitions(groupedDefinitions, false); - - for (const definition of groupedDefinitions) { - this._flagRedundantDefinitionTags(definition.definitions); - } - - return [groupedDefinitions, length]; - } - - async _findTermsMerged(text, options) { - const {mainDictionary, enabledDictionaryMap} = options; - const secondarySearchDictionaryMap = this._getSecondarySearchDictionaryMap(enabledDictionaryMap); - - const [definitions, length] = await this._findTermsInternal(text, enabledDictionaryMap, options); - const {sequencedDefinitions, unsequencedDefinitions} = await this._getSequencedDefinitions(definitions, mainDictionary, enabledDictionaryMap); - const definitionsMerged = []; - const usedDefinitions = new Set(); - - for (const {sourceDefinitions, relatedDefinitions} of sequencedDefinitions) { - const result = await this._getMergedDefinition( - sourceDefinitions, - relatedDefinitions, - unsequencedDefinitions, - secondarySearchDictionaryMap, - usedDefinitions - ); - definitionsMerged.push(result); - } - - const unusedDefinitions = unsequencedDefinitions.filter((definition) => !usedDefinitions.has(definition)); - for (const groupedDefinition of this._groupTerms(unusedDefinitions, enabledDictionaryMap)) { - const {reasons, score, expression, reading, source, rawSource, sourceTerm, furiganaSegments, termTags, definitions: definitions2} = groupedDefinition; - const termDetailsList = [this._createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTags)]; - const compatibilityDefinition = this._createMergedTermDefinition( - source, - rawSource, - this._convertTermDefinitionsToMergedGlossaryTermDefinitions(definitions2), - [expression], - [reading], - termDetailsList, - reasons, - score - ); - definitionsMerged.push(compatibilityDefinition); - } - - await this._buildTermMeta(definitionsMerged, enabledDictionaryMap); - this._sortDefinitions(definitionsMerged, false); - - for (const definition of definitionsMerged) { - this._flagRedundantDefinitionTags(definition.definitions); - } - - return [definitionsMerged, length]; - } - - // Find terms internal implementation - - async _findTermsInternal(text, enabledDictionaryMap, options) { - const {alphanumeric, wildcard} = options; - text = this._getSearchableText(text, alphanumeric); - if (text.length === 0) { - return [[], 0]; - } - - const deinflections = ( - wildcard ? - await this._findTermWildcard(text, enabledDictionaryMap, wildcard) : - await this._findTermDeinflections(text, enabledDictionaryMap, options) - ); - - let maxLength = 0; - const definitions = []; - for (const {databaseDefinitions, source, rawSource, term, reasons} of deinflections) { - if (databaseDefinitions.length === 0) { continue; } - maxLength = Math.max(maxLength, rawSource.length); - for (const databaseDefinition of databaseDefinitions) { - const definition = await this._createTermDefinitionFromDatabaseDefinition(databaseDefinition, source, rawSource, term, reasons, enabledDictionaryMap); - definitions.push(definition); - } - } - - this._removeDuplicateDefinitions(definitions); - return [definitions, maxLength]; - } - - async _findTermWildcard(text, enabledDictionaryMap, wildcard) { - const databaseDefinitions = await this._database.findTermsBulk([text], enabledDictionaryMap, wildcard); - if (databaseDefinitions.length === 0) { - return []; - } - - return [{ - source: text, - rawSource: text, - term: text, - rules: 0, - reasons: [], - databaseDefinitions - }]; - } - - async _findTermDeinflections(text, enabledDictionaryMap, options) { - const deinflections = this._getAllDeinflections(text, options); - - if (deinflections.length === 0) { - return []; - } - - const uniqueDeinflectionTerms = []; - const uniqueDeinflectionArrays = []; - const uniqueDeinflectionsMap = new Map(); - for (const deinflection of deinflections) { - const term = deinflection.term; - let deinflectionArray = uniqueDeinflectionsMap.get(term); - if (typeof deinflectionArray === 'undefined') { - deinflectionArray = []; - uniqueDeinflectionTerms.push(term); - uniqueDeinflectionArrays.push(deinflectionArray); - uniqueDeinflectionsMap.set(term, deinflectionArray); - } - deinflectionArray.push(deinflection); - } - - const databaseDefinitions = await this._database.findTermsBulk(uniqueDeinflectionTerms, enabledDictionaryMap, null); - - for (const databaseDefinition of databaseDefinitions) { - const definitionRules = Deinflector.rulesToRuleFlags(databaseDefinition.rules); - for (const deinflection of uniqueDeinflectionArrays[databaseDefinition.index]) { - const deinflectionRules = deinflection.rules; - if (deinflectionRules === 0 || (definitionRules & deinflectionRules) !== 0) { - deinflection.databaseDefinitions.push(databaseDefinition); - } - } - } - - return deinflections; - } - - _getAllDeinflections(text, options) { - const textOptionVariantArray = [ - this._getTextReplacementsVariants(options), - this._getTextOptionEntryVariants(options.convertHalfWidthCharacters), - this._getTextOptionEntryVariants(options.convertNumericCharacters), - this._getTextOptionEntryVariants(options.convertAlphabeticCharacters), - this._getTextOptionEntryVariants(options.convertHiraganaToKatakana), - this._getTextOptionEntryVariants(options.convertKatakanaToHiragana), - this._getCollapseEmphaticOptions(options) - ]; - - const jp = this._japaneseUtil; - const deinflections = []; - const used = new Set(); - for (const [textReplacements, halfWidth, numeric, alphabetic, katakana, hiragana, [collapseEmphatic, collapseEmphaticFull]] of this._getArrayVariants(textOptionVariantArray)) { - let text2 = text; - const sourceMap = new TextSourceMap(text2); - if (textReplacements !== null) { - text2 = this._applyTextReplacements(text2, sourceMap, textReplacements); - } - if (halfWidth) { - text2 = jp.convertHalfWidthKanaToFullWidth(text2, sourceMap); - } - if (numeric) { - text2 = jp.convertNumericToFullWidth(text2); - } - if (alphabetic) { - text2 = jp.convertAlphabeticToKana(text2, sourceMap); - } - if (katakana) { - text2 = jp.convertHiraganaToKatakana(text2); - } - if (hiragana) { - text2 = jp.convertKatakanaToHiragana(text2); - } - if (collapseEmphatic) { - text2 = jp.collapseEmphaticSequences(text2, collapseEmphaticFull, sourceMap); - } - - for (let i = text2.length; i > 0; --i) { - const text2Substring = text2.substring(0, i); - if (used.has(text2Substring)) { break; } - used.add(text2Substring); - const rawSource = sourceMap.source.substring(0, sourceMap.getSourceLength(i)); - for (const deinflection of this._deinflector.deinflect(text2Substring, rawSource)) { - deinflections.push(deinflection); - } - } - } - return deinflections; - } - - async _getSequencedDefinitions(definitions, mainDictionary, enabledDictionaryMap) { - const sequenceList = []; - const sequencedDefinitionMap = new Map(); - const sequencedDefinitions = []; - const unsequencedDefinitions = []; - for (const definition of definitions) { - const {sequence, dictionary} = definition; - if (mainDictionary === dictionary && sequence >= 0) { - let sequencedDefinition = sequencedDefinitionMap.get(sequence); - if (typeof sequencedDefinition === 'undefined') { - sequencedDefinition = { - sourceDefinitions: [], - relatedDefinitions: [], - relatedDefinitionIds: new Set() - }; - sequencedDefinitionMap.set(sequence, sequencedDefinition); - sequencedDefinitions.push(sequencedDefinition); - sequenceList.push(sequence); - } - sequencedDefinition.sourceDefinitions.push(definition); - sequencedDefinition.relatedDefinitions.push(definition); - sequencedDefinition.relatedDefinitionIds.add(definition.id); - } else { - unsequencedDefinitions.push(definition); - } - } - - if (sequenceList.length > 0) { - const databaseDefinitions = await this._database.findTermsBySequenceBulk(sequenceList, mainDictionary); - for (const databaseDefinition of databaseDefinitions) { - const {relatedDefinitions, relatedDefinitionIds} = sequencedDefinitions[databaseDefinition.index]; - const {id} = databaseDefinition; - if (relatedDefinitionIds.has(id)) { continue; } - - const {source, rawSource, sourceTerm} = relatedDefinitions[0]; - const definition = await this._createTermDefinitionFromDatabaseDefinition(databaseDefinition, source, rawSource, sourceTerm, [], enabledDictionaryMap); - relatedDefinitions.push(definition); - } - } - - for (const {relatedDefinitions} of sequencedDefinitions) { - this._sortDefinitionsById(relatedDefinitions); - } - - return {sequencedDefinitions, unsequencedDefinitions}; - } - - async _getMergedSecondarySearchResults(expressionsMap, secondarySearchDictionaryMap) { - if (secondarySearchDictionaryMap.size === 0) { - return []; - } - - const expressionList = []; - const readingList = []; - for (const [expression, readingMap] of expressionsMap.entries()) { - for (const reading of readingMap.keys()) { - expressionList.push(expression); - readingList.push(reading); - } - } - - const databaseDefinitions = await this._database.findTermsExactBulk(expressionList, readingList, secondarySearchDictionaryMap); - this._sortDatabaseDefinitionsByIndex(databaseDefinitions); - - const definitions = []; - for (const databaseDefinition of databaseDefinitions) { - const source = expressionList[databaseDefinition.index]; - const definition = await this._createTermDefinitionFromDatabaseDefinition(databaseDefinition, source, source, source, [], secondarySearchDictionaryMap); - definitions.push(definition); - } - - return definitions; - } - - async _getMergedDefinition(sourceDefinitions, relatedDefinitions, unsequencedDefinitions, secondarySearchDictionaryMap, usedDefinitions) { - const {reasons, source, rawSource} = sourceDefinitions[0]; - const score = this._getMaxDefinitionScore(sourceDefinitions); - const termInfoMap = new Map(); - const glossaryDefinitions = []; - const glossaryDefinitionGroupMap = new Map(); - - this._mergeByGlossary(relatedDefinitions, glossaryDefinitionGroupMap); - this._addUniqueTermInfos(relatedDefinitions, termInfoMap); - - let secondaryDefinitions = await this._getMergedSecondarySearchResults(termInfoMap, secondarySearchDictionaryMap); - secondaryDefinitions = [...unsequencedDefinitions, ...secondaryDefinitions]; - - this._removeUsedDefinitions(secondaryDefinitions, termInfoMap, usedDefinitions); - this._removeDuplicateDefinitions(secondaryDefinitions); - - this._mergeByGlossary(secondaryDefinitions, glossaryDefinitionGroupMap); - - const allExpressions = new Set(); - const allReadings = new Set(); - for (const {expressions, readings} of glossaryDefinitionGroupMap.values()) { - for (const expression of expressions) { allExpressions.add(expression); } - for (const reading of readings) { allReadings.add(reading); } - } - - for (const {expressions, readings, definitions} of glossaryDefinitionGroupMap.values()) { - const glossaryDefinition = this._createMergedGlossaryTermDefinition( - source, - rawSource, - definitions, - expressions, - readings, - allExpressions, - allReadings - ); - glossaryDefinitions.push(glossaryDefinition); - } - - this._sortDefinitions(glossaryDefinitions, true); - - const termDetailsList = this._createTermDetailsListFromTermInfoMap(termInfoMap); - - return this._createMergedTermDefinition( - source, - rawSource, - glossaryDefinitions, - [...allExpressions], - [...allReadings], - termDetailsList, - reasons, - score - ); - } - - _removeUsedDefinitions(definitions, termInfoMap, usedDefinitions) { - for (let i = 0, ii = definitions.length; i < ii; ++i) { - const definition = definitions[i]; - const {expression, reading} = definition; - const expressionMap = termInfoMap.get(expression); - if ( - typeof expressionMap !== 'undefined' && - typeof expressionMap.get(reading) !== 'undefined' - ) { - usedDefinitions.add(definition); - } else { - definitions.splice(i, 1); - --i; - --ii; - } - } - } - - _getUniqueDefinitionTags(definitions) { - const definitionTagsMap = new Map(); - for (const {definitionTags} of definitions) { - for (const tag of definitionTags) { - const {name} = tag; - if (definitionTagsMap.has(name)) { continue; } - definitionTagsMap.set(name, this._cloneTag(tag)); - } - } - return [...definitionTagsMap.values()]; - } - - _removeDuplicateDefinitions(definitions) { - const definitionGroups = new Map(); - for (let i = 0, ii = definitions.length; i < ii; ++i) { - const definition = definitions[i]; - const {id} = definition; - const existing = definitionGroups.get(id); - if (typeof existing === 'undefined') { - definitionGroups.set(id, [i, definition]); - continue; - } - - let removeIndex = i; - if (definition.source.length > existing[1].source.length) { - definitionGroups.set(id, [i, definition]); - removeIndex = existing[0]; - } - - definitions.splice(removeIndex, 1); - --i; - --ii; - } - } - - _flagRedundantDefinitionTags(definitions) { - let lastDictionary = null; - let lastPartOfSpeech = ''; - const removeCategoriesSet = new Set(); - - for (const {dictionary, definitionTags} of definitions) { - const partOfSpeech = this._createMapKey(this._getTagNamesWithCategory(definitionTags, 'partOfSpeech')); - - if (lastDictionary !== dictionary) { - lastDictionary = dictionary; - lastPartOfSpeech = ''; - } - - if (lastPartOfSpeech === partOfSpeech) { - removeCategoriesSet.add('partOfSpeech'); - } else { - lastPartOfSpeech = partOfSpeech; - } - - if (removeCategoriesSet.size > 0) { - this._flagTagsWithCategoryAsRedundant(definitionTags, removeCategoriesSet); - removeCategoriesSet.clear(); - } - } - } - - _groupTerms(definitions) { - const groups = new Map(); - for (const definition of definitions) { - const key = this._createMapKey([definition.source, definition.expression, definition.reading, ...definition.reasons]); - let groupDefinitions = groups.get(key); - if (typeof groupDefinitions === 'undefined') { - groupDefinitions = []; - groups.set(key, groupDefinitions); - } - - groupDefinitions.push(definition); - } - - const results = []; - for (const groupDefinitions of groups.values()) { - this._sortDefinitions(groupDefinitions, true); - const definition = this._createGroupedTermDefinition(groupDefinitions); - results.push(definition); - } - - return results; - } - - _mergeByGlossary(definitions, glossaryDefinitionGroupMap) { - for (const definition of definitions) { - const {expression, reading, dictionary, glossary, id} = definition; - - const key = this._createMapKey([dictionary, ...glossary]); - let group = glossaryDefinitionGroupMap.get(key); - if (typeof group === 'undefined') { - group = { - expressions: new Set(), - readings: new Set(), - definitions: [], - definitionIds: new Set() - }; - glossaryDefinitionGroupMap.set(key, group); - } - - const {definitionIds} = group; - if (definitionIds.has(id)) { continue; } - definitionIds.add(id); - group.expressions.add(expression); - group.readings.add(reading); - group.definitions.push(definition); - } - } - - _addUniqueTermInfos(definitions, termInfoMap) { - for (const {expression, reading, sourceTerm, furiganaSegments, termTags} of definitions) { - let readingMap = termInfoMap.get(expression); - if (typeof readingMap === 'undefined') { - readingMap = new Map(); - termInfoMap.set(expression, readingMap); - } - - let termInfo = readingMap.get(reading); - if (typeof termInfo === 'undefined') { - termInfo = { - sourceTerm, - furiganaSegments, - termTagsMap: new Map() - }; - readingMap.set(reading, termInfo); - } - - const {termTagsMap} = termInfo; - for (const tag of termTags) { - const {name} = tag; - if (termTagsMap.has(name)) { continue; } - termTagsMap.set(name, this._cloneTag(tag)); - } - } - } - - _convertTermDefinitionsToMergedGlossaryTermDefinitions(definitions) { - const convertedDefinitions = []; - for (const definition of definitions) { - const {source, rawSource, expression, reading} = definition; - const expressions = new Set([expression]); - const readings = new Set([reading]); - const convertedDefinition = this._createMergedGlossaryTermDefinition(source, rawSource, [definition], expressions, readings, expressions, readings); - convertedDefinitions.push(convertedDefinition); - } - return convertedDefinitions; - } - - // Metadata building - - async _buildTermMeta(definitions, enabledDictionaryMap) { - const addMetadataTargetInfo = (targetMap1, target, parents) => { - let {expression, reading} = target; - if (!reading) { reading = expression; } - - let targetMap2 = targetMap1.get(expression); - if (typeof targetMap2 === 'undefined') { - targetMap2 = new Map(); - targetMap1.set(expression, targetMap2); - } - - let targets = targetMap2.get(reading); - if (typeof targets === 'undefined') { - targets = new Set([target, ...parents]); - targetMap2.set(reading, targets); - } else { - targets.add(target); - for (const parent of parents) { - targets.add(parent); - } - } - }; - - const targetMap = new Map(); - const definitionsQueue = definitions.map((definition) => ({definition, parents: []})); - while (definitionsQueue.length > 0) { - const {definition, parents} = definitionsQueue.shift(); - const childDefinitions = definition.definitions; - if (Array.isArray(childDefinitions)) { - for (const definition2 of childDefinitions) { - definitionsQueue.push({definition: definition2, parents: [...parents, definition]}); - } - } else { - addMetadataTargetInfo(targetMap, definition, parents); - } - - for (const target of definition.expressions) { - addMetadataTargetInfo(targetMap, target, []); - } - } - const targetMapEntries = [...targetMap.entries()]; - const uniqueExpressions = targetMapEntries.map(([expression]) => expression); - - const metas = await this._database.findTermMetaBulk(uniqueExpressions, enabledDictionaryMap); - for (const {expression, mode, data, dictionary, index} of metas) { - const targetMap2 = targetMapEntries[index][1]; - for (const [reading, targets] of targetMap2) { - switch (mode) { - case 'freq': - { - const frequencyData = this._getTermFrequencyData(expression, reading, dictionary, data); - if (frequencyData === null) { continue; } - for (const {frequencies} of targets) { frequencies.push(frequencyData); } - } - break; - case 'pitch': - { - const pitchData = await this._getPitchData(expression, reading, dictionary, data); - if (pitchData === null) { continue; } - for (const {pitches} of targets) { pitches.push(pitchData); } - } - break; - } - } - } - } - - async _buildKanjiMeta(definitions, enabledDictionaryMap) { - const kanjiList = []; - for (const {character} of definitions) { - kanjiList.push(character); - } - - const metas = await this._database.findKanjiMetaBulk(kanjiList, enabledDictionaryMap); - for (const {character, mode, data, dictionary, index} of metas) { - switch (mode) { - case 'freq': - { - const frequencyData = this._getKanjiFrequencyData(character, dictionary, data); - definitions[index].frequencies.push(frequencyData); - } - break; - } - } - } - - async _expandTags(names, dictionary) { - const tagMetaList = await this._getTagMetaList(names, dictionary); - const results = []; - for (let i = 0, ii = tagMetaList.length; i < ii; ++i) { - const meta = tagMetaList[i]; - const name = names[i]; - const {category, notes, order, score} = (meta !== null ? meta : {}); - const tag = this._createTag(name, category, notes, order, score, dictionary, false); - results.push(tag); - } - return results; - } - - async _expandStats(items, dictionary) { - const names = Object.keys(items); - const tagMetaList = await this._getTagMetaList(names, dictionary); - - const statsGroups = new Map(); - for (let i = 0; i < names.length; ++i) { - const name = names[i]; - const meta = tagMetaList[i]; - if (meta === null) { continue; } - - const {category, notes, order, score} = meta; - let group = statsGroups.get(category); - if (typeof group === 'undefined') { - group = []; - statsGroups.set(category, group); - } - - const value = items[name]; - const stat = this._createKanjiStat(name, category, notes, order, score, dictionary, value); - group.push(stat); - } - - const stats = {}; - for (const [category, group] of statsGroups.entries()) { - this._sortKanjiStats(group); - stats[category] = group; - } - return stats; - } - - async _getTagMetaList(names, dictionary) { - const tagMetaList = []; - let cache = this._tagCache.get(dictionary); - if (typeof cache === 'undefined') { - cache = new Map(); - this._tagCache.set(dictionary, cache); - } - - for (const name of names) { - const base = this._getNameBase(name); - - let tagMeta = cache.get(base); - if (typeof tagMeta === 'undefined') { - tagMeta = await this._database.findTagForTitle(base, dictionary); - cache.set(base, tagMeta); - } - - tagMetaList.push(tagMeta); - } - - return tagMetaList; - } - - _getTermFrequencyData(expression, reading, dictionary, data) { - let frequency = data; - const hasReading = (data !== null && typeof data === 'object'); - if (hasReading) { - if (data.reading !== reading) { return null; } - frequency = data.frequency; - } - return {dictionary, expression, reading, hasReading, frequency}; - } - - _getKanjiFrequencyData(character, dictionary, data) { - return {dictionary, character, frequency: data}; - } - - async _getPitchData(expression, reading, dictionary, data) { - if (data.reading !== reading) { return null; } - - const pitches = []; - for (let {position, tags} of data.pitches) { - tags = Array.isArray(tags) ? await this._expandTags(tags, dictionary) : []; - pitches.push({position, tags}); - } - - return {expression, reading, dictionary, pitches}; - } - - // Simple helpers - - _scoreToTermFrequency(score) { - if (score > 0) { - return 'popular'; - } else if (score < 0) { - return 'rare'; - } else { - return 'normal'; - } - } - - _getNameBase(name) { - const pos = name.indexOf(':'); - return (pos >= 0 ? name.substring(0, pos) : name); - } - - _getSearchableText(text, allowAlphanumericCharacters) { - if (allowAlphanumericCharacters) { - return text; - } - - const jp = this._japaneseUtil; - let newText = ''; - for (const c of text) { - if (!jp.isCodePointJapanese(c.codePointAt(0))) { - break; - } - newText += c; - } - return newText; - } - - _getTextOptionEntryVariants(value) { - switch (value) { - case 'true': return [true]; - case 'variant': return [false, true]; - default: return [false]; - } - } - - _getCollapseEmphaticOptions(options) { - const collapseEmphaticOptions = [[false, false]]; - switch (options.collapseEmphaticSequences) { - case 'true': - collapseEmphaticOptions.push([true, false]); - break; - case 'full': - collapseEmphaticOptions.push([true, false], [true, true]); - break; - } - return collapseEmphaticOptions; - } - - _getTextReplacementsVariants(options) { - return options.textReplacements; - } - - _getSecondarySearchDictionaryMap(enabledDictionaryMap) { - const secondarySearchDictionaryMap = new Map(); - for (const [dictionary, details] of enabledDictionaryMap.entries()) { - if (!details.allowSecondarySearches) { continue; } - secondarySearchDictionaryMap.set(dictionary, details); - } - return secondarySearchDictionaryMap; - } - - _getDictionaryPriority(dictionary, enabledDictionaryMap) { - const info = enabledDictionaryMap.get(dictionary); - return typeof info !== 'undefined' ? info.priority : 0; - } - - _getTagNamesWithCategory(tags, category) { - const results = []; - for (const tag of tags) { - if (tag.category !== category) { continue; } - results.push(tag.name); - } - results.sort(); - return results; - } - - _flagTagsWithCategoryAsRedundant(tags, removeCategoriesSet) { - for (const tag of tags) { - if (removeCategoriesSet.has(tag.category)) { - tag.redundant = true; - } - } - } - - _getUniqueDictionaryNames(definitions) { - const uniqueDictionaryNames = new Set(); - for (const {dictionaryNames} of definitions) { - for (const dictionaryName of dictionaryNames) { - uniqueDictionaryNames.add(dictionaryName); - } - } - return [...uniqueDictionaryNames]; - } - - _getUniqueTermTags(definitions) { - const newTermTags = []; - if (definitions.length <= 1) { - for (const {termTags} of definitions) { - for (const tag of termTags) { - newTermTags.push(this._cloneTag(tag)); - } - } - } else { - const tagsSet = new Set(); - let checkTagsMap = false; - for (const {termTags} of definitions) { - for (const tag of termTags) { - const key = this._getTagMapKey(tag); - if (checkTagsMap && tagsSet.has(key)) { continue; } - tagsSet.add(key); - newTermTags.push(this._cloneTag(tag)); - } - checkTagsMap = true; - } - } - return newTermTags; - } - - *_getArrayVariants(arrayVariants) { - const ii = arrayVariants.length; - - let total = 1; - for (let i = 0; i < ii; ++i) { - total *= arrayVariants[i].length; - } - - for (let a = 0; a < total; ++a) { - const variant = []; - let index = a; - for (let i = 0; i < ii; ++i) { - const entryVariants = arrayVariants[i]; - variant.push(entryVariants[index % entryVariants.length]); - index = Math.floor(index / entryVariants.length); - } - yield variant; - } - } - - _areSetsEqual(set1, set2) { - if (set1.size !== set2.size) { - return false; - } - - for (const value of set1) { - if (!set2.has(value)) { - return false; - } - } - - return true; - } - - _getSetIntersection(set1, set2) { - const result = []; - for (const value of set1) { - if (set2.has(value)) { - result.push(value); - } - } - return result; - } - - // Reduction functions - - _getTermTagsScoreSum(termTags) { - let result = 0; - for (const {score} of termTags) { - result += score; - } - return result; - } - - _getSourceTermMatchCountSum(definitions) { - let result = 0; - for (const {sourceTermExactMatchCount} of definitions) { - result += sourceTermExactMatchCount; - } - return result; - } - - _getMaxDefinitionScore(definitions) { - let result = Number.MIN_SAFE_INTEGER; - for (const {score} of definitions) { - if (score > result) { result = score; } - } - return result; - } - - _getMaxDictionaryPriority(definitions) { - let result = Number.MIN_SAFE_INTEGER; - for (const {dictionaryPriority} of definitions) { - if (dictionaryPriority > result) { result = dictionaryPriority; } - } - return result; - } - - // Common data creation and cloning functions - - _cloneTag(tag) { - const {name, category, notes, order, score, dictionary, redundant} = tag; - return this._createTag(name, category, notes, order, score, dictionary, redundant); - } - - _getTagMapKey(tag) { - const {name, category, notes} = tag; - return this._createMapKey([name, category, notes]); - } - - _createMapKey(array) { - return JSON.stringify(array); - } - - _createTag(name, category, notes, order, score, dictionary, redundant) { - return { - name, - category: (typeof category === 'string' && category.length > 0 ? category : 'default'), - notes: (typeof notes === 'string' ? notes : ''), - order: (typeof order === 'number' ? order : 0), - score: (typeof score === 'number' ? score : 0), - dictionary: (typeof dictionary === 'string' ? dictionary : null), - redundant - }; - } - - _createKanjiStat(name, category, notes, order, score, dictionary, value) { - return { - name, - category: (typeof category === 'string' && category.length > 0 ? category : 'default'), - notes: (typeof notes === 'string' ? notes : ''), - order: (typeof order === 'number' ? order : 0), - score: (typeof score === 'number' ? score : 0), - dictionary: (typeof dictionary === 'string' ? dictionary : null), - value - }; - } - - _createKanjiDefinition(character, dictionary, onyomi, kunyomi, glossary, tags, stats) { - return { - type: 'kanji', - character, - dictionary, - onyomi, - kunyomi, - glossary, - tags, - stats, - frequencies: [] - }; - } - - async _createTermDefinitionFromDatabaseDefinition(databaseDefinition, source, rawSource, sourceTerm, reasons, enabledDictionaryMap) { - const {expression, reading, definitionTags, termTags, glossary, score, dictionary, id, sequence} = databaseDefinition; - const dictionaryPriority = this._getDictionaryPriority(dictionary, enabledDictionaryMap); - const termTagsExpanded = await this._expandTags(termTags, dictionary); - const definitionTagsExpanded = await this._expandTags(definitionTags, dictionary); - - this._sortTags(definitionTagsExpanded); - this._sortTags(termTagsExpanded); - - const furiganaSegments = this._japaneseUtil.distributeFurigana(expression, reading); - const termDetailsList = [this._createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTagsExpanded)]; - const sourceTermExactMatchCount = (sourceTerm === expression ? 1 : 0); - - return { - type: 'term', - id, - source, - rawSource, - sourceTerm, - reasons, - score, - sequence, - dictionary, - dictionaryPriority, - dictionaryNames: [dictionary], - expression, - reading, - expressions: termDetailsList, - furiganaSegments, - glossary, - definitionTags: definitionTagsExpanded, - termTags: termTagsExpanded, - // definitions - frequencies: [], - pitches: [], - // only - sourceTermExactMatchCount - }; - } - - _createGroupedTermDefinition(definitions) { - const {expression, reading, furiganaSegments, reasons, source, rawSource, sourceTerm} = definitions[0]; - const score = this._getMaxDefinitionScore(definitions); - const dictionaryPriority = this._getMaxDictionaryPriority(definitions); - const dictionaryNames = this._getUniqueDictionaryNames(definitions); - const termTags = this._getUniqueTermTags(definitions); - const termDetailsList = [this._createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTags)]; - const sourceTermExactMatchCount = (sourceTerm === expression ? 1 : 0); - return { - type: 'termGrouped', - // id - source, - rawSource, - sourceTerm, - reasons: [...reasons], - score, - // sequence - dictionary: dictionaryNames[0], - dictionaryPriority, - dictionaryNames, - expression, - reading, - expressions: termDetailsList, - furiganaSegments, // Contains duplicate data - // glossary - // definitionTags - termTags, - definitions, // type: 'term' - frequencies: [], - pitches: [], - // only - sourceTermExactMatchCount - }; - } - - _createMergedTermDefinition(source, rawSource, definitions, expressions, readings, termDetailsList, reasons, score) { - const dictionaryPriority = this._getMaxDictionaryPriority(definitions); - const sourceTermExactMatchCount = this._getSourceTermMatchCountSum(definitions); - const dictionaryNames = this._getUniqueDictionaryNames(definitions); - return { - type: 'termMerged', - // id - source, - rawSource, - // sourceTerm - reasons, - score, - // sequence - dictionary: dictionaryNames[0], - dictionaryPriority, - dictionaryNames, - expression: expressions, - reading: readings, - expressions: termDetailsList, - // furiganaSegments - // glossary - // definitionTags - // termTags - definitions, // type: 'termMergedByGlossary' - frequencies: [], - pitches: [], - // only - sourceTermExactMatchCount - }; - } - - _createMergedGlossaryTermDefinition(source, rawSource, definitions, expressions, readings, allExpressions, allReadings) { - const only = []; - if (!this._areSetsEqual(expressions, allExpressions)) { - only.push(...this._getSetIntersection(expressions, allExpressions)); - } - if (!this._areSetsEqual(readings, allReadings)) { - only.push(...this._getSetIntersection(readings, allReadings)); - } - - const sourceTermExactMatchCount = this._getSourceTermMatchCountSum(definitions); - const dictionaryNames = this._getUniqueDictionaryNames(definitions); - - const termInfoMap = new Map(); - this._addUniqueTermInfos(definitions, termInfoMap); - const termDetailsList = this._createTermDetailsListFromTermInfoMap(termInfoMap); - - const definitionTags = this._getUniqueDefinitionTags(definitions); - this._sortTags(definitionTags); - - const {glossary} = definitions[0]; - const score = this._getMaxDefinitionScore(definitions); - const dictionaryPriority = this._getMaxDictionaryPriority(definitions); - return { - type: 'termMergedByGlossary', - // id - source, - rawSource, - // sourceTerm - reasons: [], - score, - // sequence - dictionary: dictionaryNames[0], - dictionaryPriority, - dictionaryNames, - expression: [...expressions], - reading: [...readings], - expressions: termDetailsList, - // furiganaSegments - glossary: [...glossary], - definitionTags, - // termTags - definitions, // type: 'term'; contains duplicate data - frequencies: [], - pitches: [], - only, - sourceTermExactMatchCount - }; - } - - _createTermDetailsListFromTermInfoMap(termInfoMap) { - const termDetailsList = []; - for (const [expression, readingMap] of termInfoMap.entries()) { - for (const [reading, {termTagsMap, sourceTerm, furiganaSegments}] of readingMap.entries()) { - const termTags = [...termTagsMap.values()]; - this._sortTags(termTags); - termDetailsList.push(this._createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTags)); - } - } - return termDetailsList; - } - - _createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTags) { - const termFrequency = this._scoreToTermFrequency(this._getTermTagsScoreSum(termTags)); - return { - sourceTerm, - expression, - reading, - furiganaSegments, // Contains duplicate data - termTags, - termFrequency, - frequencies: [], - pitches: [] - }; - } - - // Sorting functions - - _sortTags(tags) { - if (tags.length <= 1) { return; } - const stringComparer = this._stringComparer; - tags.sort((v1, v2) => { - const i = v1.order - v2.order; - if (i !== 0) { return i; } - - return stringComparer.compare(v1.name, v2.name); - }); - } - - _sortDefinitions(definitions, useDictionaryPriority) { - if (definitions.length <= 1) { return; } - const stringComparer = this._stringComparer; - const compareFunction1 = (v1, v2) => { - let i = v2.source.length - v1.source.length; - if (i !== 0) { return i; } - - i = v1.reasons.length - v2.reasons.length; - if (i !== 0) { return i; } - - i = v2.sourceTermExactMatchCount - v1.sourceTermExactMatchCount; - if (i !== 0) { return i; } - - i = v2.score - v1.score; - if (i !== 0) { return i; } - - const expression1 = v1.expression; - const expression2 = v2.expression; - if (typeof expression1 !== 'string' || typeof expression2 !== 'string') { return 0; } // Skip if either is not a string (array) - - i = expression2.length - expression1.length; - if (i !== 0) { return i; } - - return stringComparer.compare(expression1, expression2); - }; - const compareFunction2 = (v1, v2) => { - const i = v2.dictionaryPriority - v1.dictionaryPriority; - return (i !== 0) ? i : compareFunction1(v1, v2); - }; - definitions.sort(useDictionaryPriority ? compareFunction2 : compareFunction1); - } - - _sortDatabaseDefinitionsByIndex(definitions) { - if (definitions.length <= 1) { return; } - definitions.sort((a, b) => a.index - b.index); - } - - _sortDefinitionsById(definitions) { - if (definitions.length <= 1) { return; } - definitions.sort((a, b) => a.id - b.id); - } - - _sortKanjiStats(stats) { - if (stats.length <= 1) { return; } - const stringComparer = this._stringComparer; - stats.sort((v1, v2) => { - const i = v1.order - v2.order; - if (i !== 0) { return i; } - - return stringComparer.compare(v1.notes, v2.notes); - }); - } - - // Regex functions - - _applyTextReplacements(text, sourceMap, replacements) { - for (const {pattern, replacement} of replacements) { - text = this._applyTextReplacement(text, sourceMap, pattern, replacement); - } - return text; - } - - _applyTextReplacement(text, sourceMap, pattern, replacement) { - const isGlobal = pattern.global; - if (isGlobal) { pattern.lastIndex = 0; } - for (let loop = true; loop; loop = isGlobal) { - const match = pattern.exec(text); - if (match === null) { break; } - - const matchText = match[0]; - const index = match.index; - const actualReplacement = this._applyMatchReplacement(replacement, match); - const actualReplacementLength = actualReplacement.length; - const delta = actualReplacementLength - (matchText.length > 0 ? matchText.length : -1); - - text = `${text.substring(0, index)}${actualReplacement}${text.substring(index + matchText.length)}`; - pattern.lastIndex += delta; - - if (actualReplacementLength > 0) { - sourceMap.combine(Math.max(0, index - 1), matchText.length); - sourceMap.insert(index, ...(new Array(actualReplacementLength).fill(0))); - } else { - sourceMap.combine(index, matchText.length); - } - } - return text; - } - - _applyMatchReplacement(replacement, match) { - const pattern = /\$(?:\$|&|`|'|(\d\d?)|<([^>]*)>)/g; - return replacement.replace(pattern, (g0, g1, g2) => { - if (typeof g1 !== 'undefined') { - const matchIndex = Number.parseInt(g1, 10); - if (matchIndex >= 1 && matchIndex <= match.length) { - return match[matchIndex]; - } - } else if (typeof g2 !== 'undefined') { - const {groups} = match; - if (typeof groups === 'object' && groups !== null && Object.prototype.hasOwnProperty.call(groups, g2)) { - return groups[g2]; - } - } else { - switch (g0) { - case '$': return '$'; - case '&': return match[0]; - case '`': return replacement.substring(0, match.index); - case '\'': return replacement.substring(match.index + g0.length); - } - } - return g0; - }); - } -} diff --git a/ext/bg/js/welcome-main.js b/ext/bg/js/welcome-main.js deleted file mode 100644 index 57b265dc..00000000 --- a/ext/bg/js/welcome-main.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2019-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * DictionaryController - * DictionaryImportController - * DocumentFocusController - * GenericSettingController - * ModalController - * ScanInputsSimpleController - * SettingsController - * SettingsDisplayController - * StatusFooter - * api - */ - -async function setupEnvironmentInfo() { - const {manifest_version: manifestVersion} = chrome.runtime.getManifest(); - const {browser, platform} = await api.getEnvironmentInfo(); - document.documentElement.dataset.browser = browser; - document.documentElement.dataset.os = platform.os; - document.documentElement.dataset.manifestVersion = `${manifestVersion}`; -} - -async function setupGenericSettingsController(genericSettingController) { - await genericSettingController.prepare(); - await genericSettingController.refresh(); -} - -(async () => { - try { - const documentFocusController = new DocumentFocusController(); - documentFocusController.prepare(); - - const statusFooter = new StatusFooter(document.querySelector('.status-footer-container')); - statusFooter.prepare(); - - api.forwardLogsToBackend(); - await yomichan.prepare(); - - setupEnvironmentInfo(); - - const optionsFull = await api.optionsGetFull(); - - const preparePromises = []; - - const modalController = new ModalController(); - modalController.prepare(); - - const settingsController = new SettingsController(optionsFull.profileCurrent); - settingsController.prepare(); - - const dictionaryController = new DictionaryController(settingsController, modalController, null, statusFooter); - dictionaryController.prepare(); - - const dictionaryImportController = new DictionaryImportController(settingsController, modalController, null, statusFooter); - dictionaryImportController.prepare(); - - const genericSettingController = new GenericSettingController(settingsController); - preparePromises.push(setupGenericSettingsController(genericSettingController)); - - const simpleScanningInputController = new ScanInputsSimpleController(settingsController); - simpleScanningInputController.prepare(); - - await Promise.all(preparePromises); - - document.documentElement.dataset.loaded = 'true'; - - const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); - settingsDisplayController.prepare(); - } catch (e) { - yomichan.logError(e); - } -})(); |