diff --git a/.eslintrc.json b/.eslintrc.json index 3723d97d..486d7bd1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -156,6 +156,46 @@ "node": true, "webextensions": false } + }, + { + "files": [ + "ext/mixed/js/core.js", + "ext/mixed/js/yomichan.js", + "ext/mixed/js/environment.js", + "ext/mixed/js/japanese.js", + "ext/mixed/js/cache-map.js", + "ext/mixed/js/dictionary-data-util.js", + "ext/mixed/js/object-property-accessor.js", + "ext/bg/js/anki.js", + "ext/bg/js/audio-downloader.js", + "ext/bg/js/clipboard-monitor.js", + "ext/bg/js/clipboard-reader.js", + "ext/bg/js/database.js", + "ext/bg/js/deinflector.js", + "ext/bg/js/dictionary-database.js", + "ext/bg/js/json-schema.js", + "ext/bg/js/mecab.js", + "ext/bg/js/media-utility.js", + "ext/bg/js/options.js", + "ext/bg/js/profile-conditions.js", + "ext/bg/js/request-builder.js", + "ext/bg/js/native-simple-dom-parser.js", + "ext/bg/js/text-source-map.js", + "ext/bg/js/translator.js", + "ext/bg/js/backend.js", + "ext/bg/js/background-main.js" + ], + "env": { + "browser": false, + "serviceworker": true, + "es2017": true, + "webextensions": true + }, + "globals": { + "FileReader": "readonly", + "Intl": "readonly", + "crypto": "readonly" + } } ] } diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json index 3f65d86f..0532ab9b 100644 --- a/dev/data/manifest-variants.json +++ b/dev/data/manifest-variants.json @@ -130,6 +130,27 @@ } ] }, + { + "name": "chrome-mv3", + "fileName": "yomichan-chrome-mv3.zip", + "modifications": [ + {"action": "set", "path": ["manifest_version"], "value": 3}, + {"action": "move", "path": ["browser_action"], "newPath": ["action"]}, + {"action": "delete", "path": ["background", "page"]}, + {"action": "delete", "path": ["background", "persistent"]}, + {"action": "set", "path": ["background", "service_worker"], "value": "sw.js"}, + {"action": "move", "path": ["content_security_policy"], "newPath": ["content_security_policy_old"]}, + {"action": "set", "path": ["content_security_policy"], "value": {}}, + {"action": "move", "path": ["content_security_policy_old"], "newPath": ["content_security_policy", "extension_pages"]}, + {"action": "move", "path": ["sandbox", "content_security_policy"], "newPath": ["content_security_policy", "sandbox"]}, + {"action": "remove", "path": ["permissions"], "item": ""}, + {"action": "set", "path": ["host_permissions"], "value": [""], "after": "optional_permissions"}, + {"action": "add", "path": ["permissions"], "items": ["scripting"]}, + {"action": "move", "path": ["web_accessible_resources"], "newPath": ["web_accessible_resources_old"]}, + {"action": "set", "path": ["web_accessible_resources"], "value": [{"resources": [], "matches": [""]}], "after": "web_accessible_resources_old"}, + {"action": "move", "path": ["web_accessible_resources_old"], "newPath": ["web_accessible_resources", 0, "resources"]} + ] + }, { "name": "firefox", "fileName": "yomichan-firefox.xpi", diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index fb11ba2e..322d9400 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -47,6 +47,7 @@ class Backend { 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', @@ -551,43 +552,7 @@ class Backend { } _onApiInjectStylesheet({type, value}, sender) { - if (!sender.tab) { - return Promise.reject(new Error('Invalid tab')); - } - - const tabId = sender.tab.id; - const frameId = sender.frameId; - const details = ( - 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; - } - - return new Promise((resolve, reject) => { - chrome.tabs.insertCSS(tabId, details, () => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(); - } - }); - }); + return this._injectStylesheet(type, value, sender); } async _onApiGetStylesheetContent({url}) { @@ -1772,4 +1737,88 @@ class Backend { }); }); } + + _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(); + } + }); + }); + } } diff --git a/ext/bg/js/native-simple-dom-parser.js b/ext/bg/js/native-simple-dom-parser.js index 4e0d89ea..c1752bc4 100644 --- a/ext/bg/js/native-simple-dom-parser.js +++ b/ext/bg/js/native-simple-dom-parser.js @@ -17,6 +17,8 @@ class NativeSimpleDOMParser { constructor(content) { + // TODO : Remove + // eslint-disable-next-line no-undef this._document = new DOMParser().parseFromString(content, 'text/html'); } diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 72ab7474..aa894e01 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -304,7 +304,12 @@ function promiseTimeout(delay, resolveValue) { } function promiseAnimationFrame(timeout=null) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { + if (typeof cancelAnimationFrame !== 'function' || typeof requestAnimationFrame !== 'function') { + reject(new Error('Animation not supported in this context')); + return; + } + let timer = null; let frameRequest = null; const onFrame = (time) => { @@ -318,12 +323,14 @@ function promiseAnimationFrame(timeout=null) { const onTimeout = () => { timer = null; if (frameRequest !== null) { + // eslint-disable-next-line no-undef cancelAnimationFrame(frameRequest); frameRequest = null; } resolve({time: timeout, timeout: true}); }; + // eslint-disable-next-line no-undef frameRequest = requestAnimationFrame(onFrame); if (typeof timeout === 'number') { timer = setTimeout(onTimeout, timeout); diff --git a/ext/sw.js b/ext/sw.js new file mode 100644 index 00000000..1b377ae0 --- /dev/null +++ b/ext/sw.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 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 . + */ + +self.importScripts( + '/mixed/lib/wanakana.min.js', + '/mixed/js/core.js', + '/mixed/js/yomichan.js', + '/mixed/js/environment.js', + '/mixed/js/japanese.js', + '/mixed/js/cache-map.js', + '/mixed/js/dictionary-data-util.js', + '/mixed/js/object-property-accessor.js', + '/bg/js/anki.js', + '/bg/js/audio-downloader.js', + '/bg/js/clipboard-monitor.js', + '/bg/js/clipboard-reader.js', + '/bg/js/database.js', + '/bg/js/deinflector.js', + '/bg/js/dictionary-database.js', + '/bg/js/json-schema.js', + '/bg/js/mecab.js', + '/bg/js/media-utility.js', + '/bg/js/options.js', + '/bg/js/profile-conditions.js', + '/bg/js/request-builder.js', + '/bg/js/native-simple-dom-parser.js', + '/bg/js/text-source-map.js', + '/bg/js/translator.js', + '/bg/js/backend.js', + '/bg/js/background-main.js' +); diff --git a/test/test-sw.js b/test/test-sw.js new file mode 100644 index 00000000..a2cb9df4 --- /dev/null +++ b/test/test-sw.js @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * Author: 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 . + */ + +const fs = require('fs'); +const path = require('path'); +const {JSDOM} = require('jsdom'); +const {VM} = require('../dev/vm'); +const assert = require('assert'); + + +function getAllHtmlScriptPaths(fileName) { + const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); + const dom = new JSDOM(domSource); + const {window} = dom; + const {document} = window; + try { + const scripts = document.querySelectorAll('script'); + return [...scripts].map(({src}) => src); + } finally { + window.close(); + } +} + + +function main() { + try { + // Verify that sw.js scripts match background.html scripts + const rootDir = path.join(__dirname, '..'); + const extDirName = 'ext'; + const extDir = path.join(rootDir, extDirName); + + const scripts = getAllHtmlScriptPaths(path.join(extDir, 'bg', 'background.html')); + const importedScripts = []; + + const importScripts = (...scripts2) => { + importedScripts.push(...scripts2); + }; + + const vm = new VM({importScripts}); + vm.context.self = vm.context; + vm.execute(['sw.js']); + + vm.assert.deepStrictEqual(scripts, importedScripts); + + // Verify that eslint config lists files correctly + const expectedSwRulesFiles = scripts.filter((src) => !src.startsWith('/mixed/lib/')).map((src) => `${extDirName}${src}`); + const eslintConfig = JSON.parse(fs.readFileSync(path.join(rootDir, '.eslintrc.json'), {encoding: 'utf8'})); + const swRules = eslintConfig.overrides.find((item) => ( + typeof item.env === 'object' && + item.env !== null && + item.env.serviceworker === true + )); + assert.ok(typeof swRules !== 'undefined'); + assert.ok(Array.isArray(swRules.files)); + assert.deepStrictEqual(swRules.files, expectedSwRulesFiles); + } catch (e) { + console.error(e); + process.exit(-1); + return; + } + process.exit(0); +} + + +if (require.main === module) { main(); }