Compare commits

..

10 Commits

Author SHA1 Message Date
528a63fdac Remove downloads for Yomichan 2023-12-10 12:02:05 -08:00
2f13cf5d2a Update maintanence info 2023-02-25 12:43:18 -08:00
a528c72107 Remove updates.json from master, it will be accessed from the metadata branch instead. 2022-10-30 12:48:50 -07:00
991fa8189a Change URL Firefox testing builds use to locate updates.json 2022-10-30 12:42:26 -07:00
toasted-nutbread
752d73418b Update version 2022-10-23 17:24:56 -04:00
toasted-nutbread
4abbc707a5
Use "const" instead of "enum" with one value (#2259) 2022-10-16 22:08:08 -04:00
toasted-nutbread
c93682f677
Ajv error fix (#2258)
* Fix ajv validation errors

* Update error format
2022-10-16 22:08:03 -04:00
toasted-nutbread
096bde44ee
Documentation updates (#2257)
* Document Backend

* Document RequestBuilder

* Document some of AnkiConnect
2022-10-16 22:07:57 -04:00
toasted-nutbread
a47cbc39e2
Fix unnecessary margin on search box in Firefox (#2255) 2022-10-15 22:43:21 -04:00
toasted-nutbread
9ef7f9d383
Clipboard updates (#2254)
* Rename

* Rename vars

* Refactor paste target

* Prevent most CSS url() properties from loading

* Add helper function to clear rich function

* Add useRichText argument

* Update condition for using readText

* Fix indent

* Update CSS
2022-10-15 22:43:12 -04:00
16 changed files with 182 additions and 98 deletions

View File

@ -1,5 +1,8 @@
# Yomichan # Yomichan
*Note: this project is no longer maintained. Please see [this
post](https://foosoft.net/posts/sunsetting-the-yomichan-project/) for more information.*
Yomichan turns your web browser into a tool for building Japanese language literacy by helping you to decipher texts Yomichan turns your web browser into a tool for building Japanese language literacy by helping you to decipher texts
which would be otherwise too difficult tackle. This extension is similar to which would be otherwise too difficult tackle. This extension is similar to
[Rikaichamp](https://addons.mozilla.org/en-US/firefox/addon/rikaichamp/) for Firefox and [Rikaichamp](https://addons.mozilla.org/en-US/firefox/addon/rikaichamp/) for Firefox and
@ -43,16 +46,7 @@ New changes are initially introduced into the *testing* version, and after some
relatively bug free, they will be promoted to the *stable* version. If you are technically savvy and don't mind relatively bug free, they will be promoted to the *stable* version. If you are technically savvy and don't mind
submitting issues on GitHub, try the *testing* version; otherwise, the *stable* version will be your best bet. submitting issues on GitHub, try the *testing* version; otherwise, the *stable* version will be your best bet.
* **Google Chrome** *Note: [Yomichan is no longer available for download](/posts/passing-the-torch-to-yomitan).*
([stable](https://chrome.google.com/webstore/detail/yomichan/ogmnaimimemjmbakcfefmnahgdfhfami) or [testing](https://chrome.google.com/webstore/detail/yomichan-testing/bcknnfebhefllbjhagijobjklocakpdm)) \
[![](img/chrome-web-store.png)](https://chrome.google.com/webstore/detail/yomichan/ogmnaimimemjmbakcfefmnahgdfhfami)
* **Mozilla Firefox**
([stable](https://addons.mozilla.org/en-US/firefox/addon/yomichan/) or [testing](https://github.com/FooSoft/yomichan/releases)<sup>*</sup>) \
[![](img/firefox-marketplace.png)](https://addons.mozilla.org/en-US/firefox/addon/yomichan/) \
<sup>*</sup>Unlike Chrome, Firefox does not allow extensions meant for testing to be hosted in the marketplace.
You will have to download a desired version and side-load it yourself. You only need to do this once and will get
updates automatically.
## Dictionaries ## Dictionaries

View File

@ -2,7 +2,7 @@
"manifest": { "manifest": {
"manifest_version": 2, "manifest_version": 2,
"name": "Yomichan", "name": "Yomichan",
"version": "22.9.9.2", "version": "22.10.23.0",
"description": "Japanese dictionary with Anki integration", "description": "Japanese dictionary with Anki integration",
"author": "Alex Yatskov", "author": "Alex Yatskov",
"icons": { "icons": {
@ -259,7 +259,7 @@
{ {
"action": "set", "action": "set",
"path": ["browser_specific_settings", "gecko", "update_url"], "path": ["browser_specific_settings", "gecko", "update_url"],
"value": "https://foosoft.net/projects/yomichan/dl/updates.json" "value": "https://raw.githubusercontent.com/FooSoft/yomichan/metadata/updates.json"
} }
], ],
"excludeFiles": [ "excludeFiles": [

View File

@ -41,9 +41,10 @@ class JsonSchemaAjv {
validate(data) { validate(data) {
if (this._validate(data)) { return; } if (this._validate(data)) { return; }
const {errors} = this._validate(data); const {errors} = this._validate;
const message = errors.map((e) => e.toString()).join('\n'); const error = new Error('Schema validation failed');
throw new Error(message); error.data = JSON.parse(JSON.stringify(errors));
throw error;
} }
} }

View File

@ -1,36 +0,0 @@
{
"addons": {
"alex.testing@foosoft.net": {
"updates": [
{
"version": "21.10.31.1",
"update_link": "https://github.com/FooSoft/yomichan/releases/download/21.10.31.1/yomichan_testing-21.10.31.1-an+fx.xpi"
},
{
"version": "22.2.2.0",
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.2.2.0/yomichan_testing-22.2.2.0-an+fx.xpi"
},
{
"version": "22.4.4.0",
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.4.4.0/yomichan_testing-22.4.4.0-an+fx.xpi"
},
{
"version": "22.6.6.0",
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.6.6.0/a708116f79104891acbd-22.6.6.0.xpi"
},
{
"version": "22.9.9.0",
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.9.9.0/a708116f79104891acbd-22.9.9.0.xpi"
},
{
"version": "22.9.9.1",
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.9.9.1/a708116f79104891acbd-22.9.9.1.xpi"
},
{
"version": "22.9.9.2",
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.9.9.2/a708116f79104891acbd-22.9.9.2.xpi"
}
]
}
}
}

View File

@ -11,6 +11,7 @@
<link rel="icon" type="image/png" href="/images/icon48.png" sizes="48x48"> <link rel="icon" type="image/png" href="/images/icon48.png" sizes="48x48">
<link rel="icon" type="image/png" href="/images/icon64.png" sizes="64x64"> <link rel="icon" type="image/png" href="/images/icon64.png" sizes="64x64">
<link rel="icon" type="image/png" href="/images/icon128.png" sizes="128x128"> <link rel="icon" type="image/png" href="/images/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/css/background.css">
</head> </head>
<body> <body>
@ -61,7 +62,7 @@
https://bugzilla.mozilla.org/show_bug.cgi?id=1603985 https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
--> -->
<!-- [html-validate-disable close-order] --> <!-- [html-validate-disable close-order] -->
<div id="clipboard-image-paste-target" contenteditable="true"> <div id="clipboard-rich-content-paste-target" contenteditable="true">
</body> </body>
</html> </html>

29
ext/css/background.css Normal file
View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2022 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/>.
*/
/* stylelint-disable declaration-no-important */
#clipboard-rich-content-paste-target * {
background-image: none !important;
list-style-image: none !important;
content: none !important;
cursor: auto !important;
border-image-source: none !important;
offset-path: none !important;
-webkit-mask-image: none !important;
mask-image: none !important;
}
/* stylelint-enable declaration-no-important */

View File

@ -86,6 +86,7 @@ h1 {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
white-space: pre-wrap; white-space: pre-wrap;
z-index: 1; z-index: 1;
margin: 0;
} }
.search-button { .search-button {
flex: 0 0 auto; flex: 0 0 auto;

View File

@ -38,7 +38,7 @@
}, },
{ {
"type": "string", "type": "string",
"enum": ["freq"], "const": "freq",
"description": "Type of data. \"freq\" corresponds to frequency information." "description": "Type of data. \"freq\" corresponds to frequency information."
}, },
{ {

View File

@ -27,7 +27,7 @@
"properties": { "properties": {
"tag": { "tag": {
"type": "string", "type": "string",
"enum": ["br"] "const": "br"
}, },
"data": { "data": {
"$ref": "#/definitions/structuredContentData" "$ref": "#/definitions/structuredContentData"
@ -364,7 +364,7 @@
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"enum": ["text"] "const": "text"
}, },
"text": { "text": {
"type": "string", "type": "string",
@ -381,7 +381,7 @@
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"enum": ["structured-content"] "const": "structured-content"
}, },
"content": { "content": {
"$ref": "#/definitions/structuredContent", "$ref": "#/definitions/structuredContent",
@ -398,7 +398,7 @@
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"enum": ["image"] "const": "image"
}, },
"path": { "path": {
"type": "string", "type": "string",

View File

@ -49,7 +49,7 @@
{ {
"items": [ "items": [
{}, {},
{"enum": ["freq"]}, {"const": "freq"},
{ {
"oneOf": [ "oneOf": [
{ {
@ -81,7 +81,7 @@
{ {
"items": [ "items": [
{}, {},
{"enum": ["pitch"]}, {"const": "pitch"},
{ {
"type": ["object"], "type": ["object"],
"description": "Pitch accent information for the term.", "description": "Pitch accent information for the term.",

View File

@ -38,7 +38,14 @@
* wanakana * wanakana
*/ */
/**
* This class controls the core logic of the extension, including API calls
* and various forms of communication between browser tabs and external applications.
*/
class Backend { class Backend {
/**
* Creates a new instance.
*/
constructor() { constructor() {
this._japaneseUtil = new JapaneseUtil(wanakana); this._japaneseUtil = new JapaneseUtil(wanakana);
this._environment = new Environment(); this._environment = new Environment();
@ -53,7 +60,7 @@ class Backend {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
document: (typeof document === 'object' && document !== null ? document : null), document: (typeof document === 'object' && document !== null ? document : null),
pasteTargetSelector: '#clipboard-paste-target', pasteTargetSelector: '#clipboard-paste-target',
imagePasteTargetSelector: '#clipboard-image-paste-target' richContentPasteTargetSelector: '#clipboard-rich-content-paste-target'
}); });
this._clipboardMonitor = new ClipboardMonitor({ this._clipboardMonitor = new ClipboardMonitor({
japaneseUtil: this._japaneseUtil, japaneseUtil: this._japaneseUtil,
@ -145,6 +152,10 @@ class Backend {
]); ]);
} }
/**
* Initializes the instance.
* @returns {Promise<void>} A promise which is resolved when initialization completes.
*/
prepare() { prepare() {
if (this._preparePromise === null) { if (this._preparePromise === null) {
const promise = this._prepareInternal(); const promise = this._prepareInternal();
@ -596,7 +607,7 @@ class Backend {
} }
async _onApiClipboardGet() { async _onApiClipboardGet() {
return this._clipboardReader.getText(); return this._clipboardReader.getText(false);
} }
async _onApiGetDisplayTemplatesHtml() { async _onApiGetDisplayTemplatesHtml() {
@ -1773,7 +1784,7 @@ class Backend {
try { try {
if (clipboardDetails !== null && clipboardDetails.text) { if (clipboardDetails !== null && clipboardDetails.text) {
clipboardText = await this._clipboardReader.getText(); clipboardText = await this._clipboardReader.getText(false);
} }
} catch (e) { } catch (e) {
errors.push(serializeError(e)); errors.push(serializeError(e));

View File

@ -15,13 +15,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/**
* This class is used to generate `fetch()` requests on the background page
* with additional controls over anonymity and error handling.
*/
class RequestBuilder { class RequestBuilder {
/**
* A progress callback for a fetch read.
* @callback ProgressCallback
* @param {boolean} complete Whether or not the data has been completely read.
*/
/**
* Creates a new instance.
*/
constructor() { constructor() {
this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders']; this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders'];
this._textEncoder = new TextEncoder(); this._textEncoder = new TextEncoder();
this._ruleIds = new Set(); this._ruleIds = new Set();
} }
/**
* Initializes the instance.
*/
async prepare() { async prepare() {
try { try {
await this._clearDynamicRules(); await this._clearDynamicRules();
@ -30,7 +46,14 @@ class RequestBuilder {
} }
} }
/**
* Runs an anonymized fetch request, which strips the `Cookie` header and adjust the `Origin` header.
* @param {string} url The URL to fetch.
* @param {RequestInit} init The initialization parameters passed to the `fetch` function.
* @returns {Promise<Response>} The response of the `fetch` call.
*/
async fetchAnonymous(url, init) { async fetchAnonymous(url, init) {
fetch(1, 2);
if (isObject(chrome.declarativeNetRequest)) { if (isObject(chrome.declarativeNetRequest)) {
return await this._fetchAnonymousDeclarative(url, init); return await this._fetchAnonymousDeclarative(url, init);
} }
@ -42,6 +65,12 @@ class RequestBuilder {
return await this._fetchInternal(url, init, headerModifications); return await this._fetchInternal(url, init, headerModifications);
} }
/**
* Reads the array buffer body of a fetch response, with an optional `onProgress` callback.
* @param {Response} response The response of a `fetch` call.
* @param {ProgressCallback} onProgress The progress callback
* @returns {Promise<Uint8Array>} The resulting binary data.
*/
static async readFetchResponseArrayBuffer(response, onProgress) { static async readFetchResponseArrayBuffer(response, onProgress) {
let reader; let reader;
try { try {

View File

@ -19,7 +19,13 @@
* AnkiUtil * AnkiUtil
*/ */
/**
* This class controls communication with Anki via the AnkiConnect plugin.
*/
class AnkiConnect { class AnkiConnect {
/**
* Creates a new instance.
*/
constructor() { constructor() {
this._enabled = false; this._enabled = false;
this._server = null; this._server = null;
@ -29,30 +35,59 @@ class AnkiConnect {
this._apiKey = null; this._apiKey = null;
} }
/**
* Gets the URL of the AnkiConnect server.
* @type {string}
*/
get server() { get server() {
return this._server; return this._server;
} }
/**
* Assigns the URL of the AnkiConnect server.
* @param {string} value The new server URL to assign.
*/
set server(value) { set server(value) {
this._server = value; this._server = value;
} }
/**
* Gets whether or not server communication is enabled.
* @type {boolean}
*/
get enabled() { get enabled() {
return this._enabled; return this._enabled;
} }
/**
* Sets whether or not server communication is enabled.
* @param {boolean} value The enabled state.
*/
set enabled(value) { set enabled(value) {
this._enabled = value; this._enabled = value;
} }
/**
* Gets the API key used when connecting to AnkiConnect.
* The value will be `null` if no API key is used.
* @type {?string}
*/
get apiKey() { get apiKey() {
return this._apiKey; return this._apiKey;
} }
/**
* Sets the API key used when connecting to AnkiConnect.
* @param {?string} value The API key to use, or `null` if no API key should be used.
*/
set apiKey(value) { set apiKey(value) {
this._apiKey = value; this._apiKey = value;
} }
/**
* Checks whether a connection to AnkiConnect can be established.
* @returns {Promise<boolean>} `true` if the connection was made, `false` otherwise.
*/
async isConnected() { async isConnected() {
try { try {
await this._invoke('version'); await this._invoke('version');
@ -62,6 +97,10 @@ class AnkiConnect {
} }
} }
/**
* Gets the AnkiConnect API version number.
* @returns {Promise<number>} The version number
*/
async getVersion() { async getVersion() {
if (!this._enabled) { return null; } if (!this._enabled) { return null; }
await this._checkVersion(); await this._checkVersion();

View File

@ -39,7 +39,7 @@ class ClipboardMonitor extends EventDispatcher {
let text = null; let text = null;
try { try {
text = await this._clipboardReader.getText(); text = await this._clipboardReader.getText(false);
} catch (e) { } catch (e) {
// NOP // NOP
} }

View File

@ -28,15 +28,15 @@ class ClipboardReader {
* @param {object} details Details about how to set up the instance. * @param {object} details Details about how to set up the instance.
* @param {?Document} details.document The Document object to be used, or null for no support. * @param {?Document} details.document The Document object to be used, or null for no support.
* @param {?string} details.pasteTargetSelector The selector for the paste target element. * @param {?string} details.pasteTargetSelector The selector for the paste target element.
* @param {?string} details.imagePasteTargetSelector The selector for the image paste target element. * @param {?string} details.richContentPasteTargetSelector The selector for the rich content paste target element.
*/ */
constructor({document=null, pasteTargetSelector=null, imagePasteTargetSelector=null}) { constructor({document=null, pasteTargetSelector=null, richContentPasteTargetSelector=null}) {
this._document = document; this._document = document;
this._browser = null; this._browser = null;
this._pasteTarget = null; this._pasteTarget = null;
this._pasteTargetSelector = pasteTargetSelector; this._pasteTargetSelector = pasteTargetSelector;
this._imagePasteTarget = null; this._richContentPasteTarget = null;
this._imagePasteTargetSelector = imagePasteTargetSelector; this._richContentPasteTargetSelector = richContentPasteTargetSelector;
} }
/** /**
@ -56,13 +56,14 @@ class ClipboardReader {
/** /**
* Gets the text in the clipboard. * Gets the text in the clipboard.
* @param {boolean} useRichText Whether or not to use rich text for pasting, when possible.
* @returns {string} A string containing the clipboard text. * @returns {string} A string containing the clipboard text.
* @throws {Error} Error if not supported. * @throws {Error} Error if not supported.
*/ */
async getText() { async getText(useRichText) {
/* /*
Notes: Notes:
document.execCommand('paste') doesn't work on Firefox. document.execCommand('paste') sometimes doesn't work on Firefox.
See: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985 See: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
Therefore, navigator.clipboard.readText() is used on Firefox. Therefore, navigator.clipboard.readText() is used on Firefox.
@ -72,7 +73,7 @@ class ClipboardReader {
being an extension with clipboard permissions. It effectively asks for the being an extension with clipboard permissions. It effectively asks for the
non-extension permission for clipboard access. non-extension permission for clipboard access.
*/ */
if (this._isFirefox()) { if (this._isFirefox() && !useRichText) {
try { try {
return await navigator.clipboard.readText(); return await navigator.clipboard.readText();
} catch (e) { } catch (e) {
@ -86,15 +87,15 @@ class ClipboardReader {
throw new Error('Clipboard reading not supported in this context'); throw new Error('Clipboard reading not supported in this context');
} }
let target = this._pasteTarget; if (useRichText) {
if (target === null) { const target = this._getRichContentPasteTarget();
target = document.querySelector(this._pasteTargetSelector); target.focus();
if (target === null) { document.execCommand('paste');
throw new Error('Clipboard paste target does not exist'); const result = target.textContent;
} this._clearRichContent(target);
this._pasteTarget = target; return result;
} } else {
const target = this._getPasteTarget();
target.value = ''; target.value = '';
target.focus(); target.focus();
document.execCommand('paste'); document.execCommand('paste');
@ -102,6 +103,7 @@ class ClipboardReader {
target.value = ''; target.value = '';
return (typeof result === 'string' ? result : ''); return (typeof result === 'string' ? result : '');
} }
}
/** /**
* Gets the first image in the clipboard. * Gets the first image in the clipboard.
@ -143,23 +145,12 @@ class ClipboardReader {
throw new Error('Clipboard reading not supported in this context'); throw new Error('Clipboard reading not supported in this context');
} }
let target = this._imagePasteTarget; const target = this._getRichContentPasteTarget();
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(); target.focus();
document.execCommand('paste'); document.execCommand('paste');
const image = target.querySelector('img[src^="data:"]'); const image = target.querySelector('img[src^="data:"]');
const result = (image !== null ? image.getAttribute('src') : null); const result = (image !== null ? image.getAttribute('src') : null);
for (const image2 of target.querySelectorAll('img')) { this._clearRichContent(target);
image2.removeAttribute('src');
}
target.textContent = '';
return result; return result;
} }
@ -177,4 +168,28 @@ class ClipboardReader {
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
} }
_getPasteTarget() {
if (this._pasteTarget === null) { this._pasteTarget = this._findPasteTarget(this._pasteTargetSelector); }
return this._pasteTarget;
}
_getRichContentPasteTarget() {
if (this._richContentPasteTarget === null) { this._richContentPasteTarget = this._findPasteTarget(this._richContentPasteTargetSelector); }
return this._richContentPasteTarget;
}
_findPasteTarget(selector) {
const target = this._document.querySelector(selector);
if (target === null) { throw new Error('Clipboard paste target does not exist'); }
return target;
}
_clearRichContent(element) {
for (const image of element.querySelectorAll('img')) {
image.removeAttribute('src');
image.removeAttribute('srcset');
}
element.textContent = '';
}
} }

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Yomichan", "name": "Yomichan",
"version": "22.9.9.2", "version": "22.10.23.0",
"description": "Japanese dictionary with Anki integration", "description": "Japanese dictionary with Anki integration",
"author": "Alex Yatskov", "author": "Alex Yatskov",
"icons": { "icons": {