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
*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
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
@ -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
submitting issues on GitHub, try the *testing* version; otherwise, the *stable* version will be your best bet.
* **Google Chrome**
([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.
*Note: [Yomichan is no longer available for download](/posts/passing-the-torch-to-yomitan).*
## Dictionaries

View File

@ -2,7 +2,7 @@
"manifest": {
"manifest_version": 2,
"name": "Yomichan",
"version": "22.9.9.2",
"version": "22.10.23.0",
"description": "Japanese dictionary with Anki integration",
"author": "Alex Yatskov",
"icons": {
@ -259,7 +259,7 @@
{
"action": "set",
"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": [

View File

@ -41,9 +41,10 @@ class JsonSchemaAjv {
validate(data) {
if (this._validate(data)) { return; }
const {errors} = this._validate(data);
const message = errors.map((e) => e.toString()).join('\n');
throw new Error(message);
const {errors} = this._validate;
const error = new Error('Schema validation failed');
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/icon64.png" sizes="64x64">
<link rel="icon" type="image/png" href="/images/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/css/background.css">
</head>
<body>
@ -61,7 +62,7 @@
https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
-->
<!-- [html-validate-disable close-order] -->
<div id="clipboard-image-paste-target" contenteditable="true">
<div id="clipboard-rich-content-paste-target" contenteditable="true">
</body>
</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;
white-space: pre-wrap;
z-index: 1;
margin: 0;
}
.search-button {
flex: 0 0 auto;

View File

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

View File

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

View File

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

View File

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

View File

@ -15,13 +15,29 @@
* 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 {
/**
* 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() {
this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders'];
this._textEncoder = new TextEncoder();
this._ruleIds = new Set();
}
/**
* Initializes the instance.
*/
async prepare() {
try {
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) {
fetch(1, 2);
if (isObject(chrome.declarativeNetRequest)) {
return await this._fetchAnonymousDeclarative(url, init);
}
@ -42,6 +65,12 @@ class RequestBuilder {
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) {
let reader;
try {

View File

@ -19,7 +19,13 @@
* AnkiUtil
*/
/**
* This class controls communication with Anki via the AnkiConnect plugin.
*/
class AnkiConnect {
/**
* Creates a new instance.
*/
constructor() {
this._enabled = false;
this._server = null;
@ -29,30 +35,59 @@ class AnkiConnect {
this._apiKey = null;
}
/**
* Gets the URL of the AnkiConnect server.
* @type {string}
*/
get server() {
return this._server;
}
/**
* Assigns the URL of the AnkiConnect server.
* @param {string} value The new server URL to assign.
*/
set server(value) {
this._server = value;
}
/**
* Gets whether or not server communication is enabled.
* @type {boolean}
*/
get enabled() {
return this._enabled;
}
/**
* Sets whether or not server communication is enabled.
* @param {boolean} value The enabled state.
*/
set 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() {
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) {
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() {
try {
await this._invoke('version');
@ -62,6 +97,10 @@ class AnkiConnect {
}
}
/**
* Gets the AnkiConnect API version number.
* @returns {Promise<number>} The version number
*/
async getVersion() {
if (!this._enabled) { return null; }
await this._checkVersion();

View File

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

View File

@ -28,15 +28,15 @@ class ClipboardReader {
* @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 {?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._browser = null;
this._pasteTarget = null;
this._pasteTargetSelector = pasteTargetSelector;
this._imagePasteTarget = null;
this._imagePasteTargetSelector = imagePasteTargetSelector;
this._richContentPasteTarget = null;
this._richContentPasteTargetSelector = richContentPasteTargetSelector;
}
/**
@ -56,13 +56,14 @@ class ClipboardReader {
/**
* 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.
* @throws {Error} Error if not supported.
*/
async getText() {
async getText(useRichText) {
/*
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
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
non-extension permission for clipboard access.
*/
if (this._isFirefox()) {
if (this._isFirefox() && !useRichText) {
try {
return await navigator.clipboard.readText();
} catch (e) {
@ -86,15 +87,15 @@ class ClipboardReader {
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;
}
if (useRichText) {
const target = this._getRichContentPasteTarget();
target.focus();
document.execCommand('paste');
const result = target.textContent;
this._clearRichContent(target);
return result;
} else {
const target = this._getPasteTarget();
target.value = '';
target.focus();
document.execCommand('paste');
@ -102,6 +103,7 @@ class ClipboardReader {
target.value = '';
return (typeof result === 'string' ? result : '');
}
}
/**
* Gets the first image in the clipboard.
@ -143,23 +145,12 @@ class ClipboardReader {
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;
}
const target = this._getRichContentPasteTarget();
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 = '';
this._clearRichContent(target);
return result;
}
@ -177,4 +168,28 @@ class ClipboardReader {
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,
"name": "Yomichan",
"version": "22.9.9.2",
"version": "22.10.23.0",
"description": "Japanese dictionary with Anki integration",
"author": "Alex Yatskov",
"icons": {