diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 572c354a4e..0000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,2 +0,0 @@
-node_modules/
-builds/
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 5b6ebd05ac..0000000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,470 +0,0 @@
-{
- "root": true,
- "extends": [
- "eslint:recommended",
- "plugin:jsonc/recommended-with-json",
- "plugin:eslint-comments/recommended",
- "plugin:@typescript-eslint/recommended"
- ],
- "parser": "@typescript-eslint/parser",
- "parserOptions": {
- "ecmaVersion": 2022,
- "sourceType": "module",
- "ecmaFeatures": {
- "globalReturn": false,
- "impliedStrict": true
- },
- "project": [
- "./jsconfig.json",
- "./dev/jsconfig.json",
- "./test/jsconfig.json",
- "./benches/jsconfig.json"
- ]
- },
- "env": {
- "browser": true,
- "es2022": true,
- "webextensions": true
- },
- "plugins": [
- "no-unsanitized",
- "header",
- "jsdoc",
- "jsonc",
- "unused-imports",
- "@typescript-eslint",
- "@stylistic"
- ],
- "ignorePatterns": [
- "/ext/lib/",
- "/dev/lib/handlebars/"
- ],
- "rules": {
- "curly": ["error", "all"],
- "dot-notation": "error",
- "eqeqeq": "error",
- "func-names": ["error", "always"],
- "guard-for-in": "error",
- "no-case-declarations": "error",
- "no-const-assign": "error",
- "no-constant-condition": "off",
- "no-global-assign": "error",
- "no-implicit-globals": "error",
- "no-new": "error",
- "no-param-reassign": "off",
- "no-prototype-builtins": "error",
- "no-restricted-syntax": [
- "error",
- {
- "message": "Avoid using JSON.parse(), prefer parseJson.",
- "selector": "MemberExpression[object.name=JSON][property.name=parse]"
- },
- {
- "message": "Avoid using Response.json(), prefer readResponseJson.",
- "selector": "MemberExpression[property.name=json]"
- }
- ],
- "no-shadow": ["off", {"builtinGlobals": false}],
- "no-undef": "error",
- "no-undefined": "error",
- "no-underscore-dangle": ["error", {"allowAfterThis": true, "allowAfterSuper": false, "allowAfterThisConstructor": false}],
- "no-unexpected-multiline": "error",
- "no-unneeded-ternary": "error",
- "no-unused-vars": ["error", {"vars": "local", "args": "after-used", "argsIgnorePattern": "^_", "caughtErrors": "none"}],
- "no-unused-expressions": "error",
- "no-var": "error",
- "prefer-const": ["error", {"destructuring": "all"}],
- "require-atomic-updates": "off",
-
- "@stylistic/array-bracket-spacing": ["error", "never"],
- "@stylistic/arrow-parens": ["error", "always"],
- "@stylistic/arrow-spacing": ["error", {"before": true, "after": true}],
- "@stylistic/block-spacing": ["error", "always"],
- "@stylistic/brace-style": ["error", "1tbs", {"allowSingleLine": true}],
- "@stylistic/comma-dangle": ["error", "never"],
- "@stylistic/comma-spacing": ["error", {"before": false, "after": true}],
- "@stylistic/computed-property-spacing": ["error", "never"],
- "@stylistic/eol-last": ["error", "always"],
- "@stylistic/func-call-spacing": ["error", "never"],
- "@stylistic/function-paren-newline": ["error", "multiline-arguments"],
- "@stylistic/generator-star-spacing": ["error", "before"],
- "@stylistic/indent": ["error", 4, {"SwitchCase": 1, "MemberExpression": 1, "flatTernaryExpressions": true, "ignoredNodes": ["ConditionalExpression"]}],
- "@stylistic/key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}],
- "@stylistic/keyword-spacing": ["error", {"before": true, "after": true}],
- "@stylistic/new-parens": "error",
- "@stylistic/no-multi-spaces": "error",
- "@stylistic/no-multiple-empty-lines": ["error", {"max": 2}],
- "@stylistic/no-trailing-spaces": "error",
- "@stylistic/no-whitespace-before-property": "error",
- "@stylistic/object-curly-newline": "error",
- "@stylistic/object-curly-spacing": ["error", "never"],
- "@stylistic/padded-blocks": ["error", "never"],
- "@stylistic/quote-props": ["error", "consistent"],
- "@stylistic/quotes": ["error", "single", "avoid-escape"],
- "@stylistic/rest-spread-spacing": ["error", "never"],
- "@stylistic/semi": "error",
- "@stylistic/semi-spacing": ["error", {"before": false, "after": true}],
- "@stylistic/space-before-blocks": ["error", "always"],
- "@stylistic/space-before-function-paren": ["error", {"anonymous": "never", "named": "never", "asyncArrow": "always"}],
- "@stylistic/space-in-parens": ["error", "never"],
- "@stylistic/space-infix-ops": ["error", {"int32Hint": false}],
- "@stylistic/space-unary-ops": "error",
- "@stylistic/spaced-comment": ["error", "always"],
- "@stylistic/switch-colon-spacing": ["error", {"after": true, "before": false}],
- "@stylistic/template-curly-spacing": ["error", "never"],
- "@stylistic/template-tag-spacing": ["error", "never"],
- "@stylistic/wrap-iife": ["error", "inside"],
-
- "no-unsanitized/method": "error",
- "no-unsanitized/property": "error",
-
- "jsdoc/check-access": "error",
- "jsdoc/check-alignment": "error",
- "jsdoc/check-line-alignment": ["error", "never", {"wrapIndent": " "}],
- "jsdoc/check-param-names": "error",
- "jsdoc/check-property-names": "error",
- "jsdoc/check-tag-names": "error",
- "jsdoc/empty-tags": "error",
- "jsdoc/check-types": "error",
- "jsdoc/check-values": "error",
- "jsdoc/implements-on-classes": "error",
- "jsdoc/multiline-blocks": "error",
- "jsdoc/no-bad-blocks": "error",
- "jsdoc/no-multi-asterisks": "error",
- "jsdoc/no-undefined-types": "error",
- "jsdoc/require-asterisk-prefix": "error",
- "jsdoc/require-description": "off",
- "jsdoc/require-hyphen-before-param-description": ["error", "never"],
- "jsdoc/require-jsdoc": [
- "error",
- {
- "require": {
- "ClassDeclaration": false,
- "FunctionDeclaration": true,
- "MethodDefinition": false
- },
- "contexts": [
- "MethodDefinition[kind=constructor]>FunctionExpression>BlockStatement>ExpressionStatement>AssignmentExpression[left.object.type=ThisExpression]",
- "ClassDeclaration>Classbody>PropertyDefinition",
- "MethodDefinition[kind!=constructor][kind!=set]",
- "MethodDefinition[kind=constructor][value.params.length>0]"
- ],
- "checkGetters": "no-setter",
- "checkSetters": "no-getter"
- }
- ],
- "jsdoc/require-param": "error",
- "jsdoc/require-param-description": "off",
- "jsdoc/require-param-name": "error",
- "jsdoc/require-param-type": "error",
- "jsdoc/require-property": "error",
- "jsdoc/require-property-description": "off",
- "jsdoc/require-property-name": "error",
- "jsdoc/require-property-type": "error",
- "jsdoc/require-returns": "error",
- "jsdoc/require-returns-check": "error",
- "jsdoc/require-returns-description": "off",
- "jsdoc/require-returns-type": "error",
- "jsdoc/require-throws": "error",
- "jsdoc/require-yields": "error",
- "jsdoc/require-yields-check": "error",
- "jsdoc/tag-lines": ["error", "never", {"startLines": 0}],
- "jsdoc/valid-types": "error",
-
- "jsonc/indent": ["error", 4],
- "jsonc/array-bracket-newline": ["error", "consistent"],
- "jsonc/array-bracket-spacing": ["error", "never"],
- "jsonc/array-element-newline": ["error", "consistent"],
- "jsonc/comma-style": ["error", "last"],
- "jsonc/key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}],
- "jsonc/no-octal-escape": "error",
- "jsonc/object-curly-newline": ["error", {"consistent": true}],
- "jsonc/object-curly-spacing": ["error", "never"],
- "jsonc/object-property-newline": ["error", {"allowAllPropertiesOnSameLine": true}],
-
- "eslint-comments/no-unused-disable": "error",
-
- "unused-imports/no-unused-imports": "error",
-
- "@typescript-eslint/ban-ts-comment": ["error", {"ts-expect-error": {"descriptionFormat": "^ - .+$"}}],
- "@typescript-eslint/ban-types": ["error", {"types": {"object": true}, "extendDefaults": true}],
- "@typescript-eslint/consistent-type-exports": "off",
- "@typescript-eslint/no-explicit-any": "error",
- "@typescript-eslint/no-shadow": ["error", {"builtinGlobals": false}],
- "@typescript-eslint/no-this-alias": "error",
- "@typescript-eslint/no-unused-vars": "off",
- "@typescript-eslint/no-var-requires": "off"
- },
- "overrides": [
- {
- "files": [
- "*.ts"
- ],
- "rules": {
- "no-undef": "off",
-
- "@typescript-eslint/no-unused-vars": ["error", {"vars": "local", "args": "after-used", "argsIgnorePattern": "^_", "caughtErrors": "none"}],
-
- "@stylistic/block-spacing": "off",
- "@stylistic/brace-style": ["error", "1tbs", {"allowSingleLine": true}],
- "@stylistic/comma-dangle": [
- "error",
- {
- "arrays": "always-multiline",
- "objects": "always-multiline",
- "imports": "always-multiline",
- "exports": "always-multiline",
- "functions": "always-multiline",
- "enums": "always-multiline",
- "generics": "always-multiline",
- "tuples": "always-multiline"
- }
- ],
- "@stylistic/comma-spacing": ["error", {"before": false, "after": true}],
- "@stylistic/function-call-spacing": ["error", "never"],
- "@stylistic/indent": ["error", 4],
- "@stylistic/key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}],
- "@stylistic/keyword-spacing": ["error", {"before": true, "after": true}],
- "@stylistic/lines-around-comment": "off",
- "@stylistic/lines-between-class-members": ["error", "always"],
- "@stylistic/member-delimiter-style": [
- "error",
- {
- "multiline": {"delimiter": "semi", "requireLast": true},
- "singleline": {"delimiter": "comma", "requireLast": false},
- "multilineDetection": "brackets"
- }
- ],
- "@stylistic/no-multiple-empty-lines": ["error", {"max": 1, "maxEOF": 0}],
- "@stylistic/no-extra-parens": ["error", "all"],
- "@stylistic/no-extra-semi": "error",
- "@stylistic/object-curly-spacing": ["error", "never"],
- "@stylistic/padding-line-between-statements": "off",
- "@stylistic/quotes": ["error", "single", "avoid-escape"],
- "@stylistic/semi": "error",
- "@stylistic/space-before-blocks": ["error", "always"],
- "@stylistic/space-before-function-paren": ["error", {"anonymous": "never", "named": "never", "asyncArrow": "always"}],
- "@stylistic/space-infix-ops": "error",
- "@stylistic/type-annotation-spacing": "error"
- }
- },
- {
- "files": [
- "*.json"
- ],
- "parser": "jsonc-eslint-parser"
- },
- {
- "files": [
- "ext/data/schemas/options-schema.json"
- ],
- "rules": {
- "@stylistic/no-multi-spaces": "off"
- }
- },
- {
- "files": [
- "test/data/anki-note-builder-test-results.json",
- "test/data/database-test-cases.json",
- "test/data/translator-test-results-note-data1.json",
- "test/data/translator-test-results.json"
- ],
- "rules": {
- "jsonc/indent": ["error", 2]
- }
- },
- {
- "files": [
- "test/data/dictionaries/valid-dictionary1/term_bank_1.json",
- "test/data/dictionaries/valid-dictionary1/term_bank_2.json"
- ],
- "rules": {
- "jsonc/array-element-newline": "off",
- "jsonc/object-property-newline": "off"
- }
- },
- {
- "files": [
- "*.js",
- "*.ts"
- ],
- "rules": {
- "header/header": [
- "error",
- "block",
- {
- "pattern": " \\* Copyright \\(C\\) (2023-)?2024 Yomitan Authors(\n \\* Copyright \\(C\\) (20(16|17|18|19|20|21)-)?2022 Yomichan Authors)?\n \\*\n \\* This program is free software: you can redistribute it and/or modify\n \\* it under the terms of the GNU General Public License as published by\n \\* the Free Software Foundation, either version 3 of the License, or\n \\* \\(at your option\\) any later version\\.\n \\*\n \\* This program is distributed in the hope that it will be useful,\n \\* but WITHOUT ANY WARRANTY; without even the implied warranty of\n \\* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE\\. See the\n \\* GNU General Public License for more details\\.\n \\*\n \\* You should have received a copy of the GNU General Public License\n \\* along with this program\\. If not, see \\.\n "
- }
- ]
- }
- },
- {
- "files": [
- "ext/**/*.js"
- ],
- "rules": {
- "no-console": "error"
- }
- },
- {
- "files": [
- "test/**/*.js",
- "dev/**/*.js",
- "integration.spec.js",
- "playwright.config.js",
- "playwright-util.js",
- "visual.spec.js"
- ],
- "env": {
- "browser": false,
- "node": true,
- "webextensions": false
- }
- },
- {
- "files": [
- "test/data/html/**/*.js"
- ],
- "parserOptions": {
- "sourceType": "script"
- },
- "env": {
- "browser": true,
- "node": false,
- "webextensions": false
- }
- },
- {
- "files": [
- "test/data/html/**/*.js"
- ],
- "excludedFiles": [
- "test/data/html/js/html-test-utilities.js"
- ],
- "globals": {
- "HtmlTestUtilities": "readonly"
- }
- },
- {
- "files": [
- "test/**/*.test.js"
- ],
- "plugins": [
- "vitest"
- ],
- "extends": [
- "plugin:vitest/recommended"
- ],
- "rules": {
- "vitest/prefer-to-be": "off"
- }
- },
- {
- "files": [
- "dev/lib/**/*.js"
- ],
- "extends": [
- "plugin:@typescript-eslint/disable-type-checked"
- ]
- },
- {
- "files": [
- "ext/js/core/api-map.js",
- "ext/js/core/extension-error.js",
- "ext/js/core/json.js",
- "ext/js/data/sandbox/anki-note-data-creator.js",
- "ext/js/dictionary/dictionary-data-util.js",
- "ext/js/display/sandbox/pronunciation-generator.js",
- "ext/js/display/sandbox/structured-content-generator.js",
- "ext/js/dom/sandbox/css-style-applier.js",
- "ext/js/language/ja/japanese.js",
- "ext/js/templates/sandbox/anki-template-renderer-content-manager.js",
- "ext/js/templates/sandbox/anki-template-renderer.js",
- "ext/js/templates/sandbox/template-renderer-frame-api.js",
- "ext/js/templates/sandbox/template-renderer-frame-main.js",
- "ext/js/templates/sandbox/template-renderer-media-provider.js",
- "ext/js/templates/sandbox/template-renderer.js"
- ],
- "env": {
- "webextensions": false
- }
- },
- {
- "files": [
- "ext/js/core/event-dispatcher.js",
- "ext/js/core/extension-error.js",
- "ext/js/core/json.js",
- "ext/js/core/logger.js",
- "ext/js/core/to-error.js",
- "ext/js/core/utilities.js",
- "ext/js/data/database.js",
- "ext/js/dictionary/dictionary-database.js",
- "ext/js/dictionary/dictionary-importer.js",
- "ext/js/dictionary/dictionary-worker-handler.js",
- "ext/js/dictionary/dictionary-worker-main.js",
- "ext/js/dictionary/dictionary-worker-media-loader.js",
- "ext/js/media/media-util.js"
- ],
- "env": {
- "browser": false,
- "worker": true
- }
- },
- {
- "files": [
- "ext/js/accessibility/accessibility-controller.js",
- "ext/js/background/backend.js",
- "ext/js/background/background-main.js",
- "ext/js/background/offscreen-proxy.js",
- "ext/js/background/profile-conditions-util.js",
- "ext/js/background/request-builder.js",
- "ext/js/background/script-manager.js",
- "ext/js/comm/anki-connect.js",
- "ext/js/comm/clipboard-monitor.js",
- "ext/js/comm/clipboard-reader.js",
- "ext/js/comm/mecab.js",
- "ext/js/core/api-map.js",
- "ext/js/core/event-dispatcher.js",
- "ext/js/core/event-listener-collection.js",
- "ext/js/core/extension-error.js",
- "ext/js/core/fetch-utilities.js",
- "ext/js/core/json.js",
- "ext/js/core/logger.js",
- "ext/js/core/to-error.js",
- "ext/js/core/utilities.js",
- "ext/js/data/anki-util.js",
- "ext/js/data/database.js",
- "ext/js/data/json-schema.js",
- "ext/js/data/options-util.js",
- "ext/js/data/permissions-util.js",
- "ext/js/data/sandbox/array-buffer-util.js",
- "ext/js/dictionary/dictionary-database.js",
- "ext/js/dom/native-simple-dom-parser.js",
- "ext/js/dom/simple-dom-parser.js",
- "ext/js/extension/environment.js",
- "ext/js/extension/web-extension.js",
- "ext/js/general/cache-map.js",
- "ext/js/general/object-property-accessor.js",
- "ext/js/general/regex-util.js",
- "ext/js/general/text-source-map.js",
- "ext/js/language/ja/japanese-wanakana.js",
- "ext/js/language/ja/japanese.js",
- "ext/js/language/language-transformer.js",
- "ext/js/language/translator.js",
- "ext/js/media/audio-downloader.js",
- "ext/js/media/media-util.js",
- "ext/js/templates/template-patcher.js"
- ],
- "env": {
- "browser": false,
- "serviceworker": true
- },
- "globals": {
- "FileReader": "readonly",
- "Intl": "readonly",
- "crypto": "readonly",
- "AbortController": "readonly"
- }
- }
- ]
-}
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/1-bug-report.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/bug-report.md
rename to .github/ISSUE_TEMPLATE/1-bug-report.md
diff --git a/.github/ISSUE_TEMPLATE/enhancement-request.md b/.github/ISSUE_TEMPLATE/2-enhancement-request.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/enhancement-request.md
rename to .github/ISSUE_TEMPLATE/2-enhancement-request.md
diff --git a/.github/ISSUE_TEMPLATE/3-language-feature.md b/.github/ISSUE_TEMPLATE/3-language-feature.md
new file mode 100644
index 0000000000..5ff2028ccb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/3-language-feature.md
@@ -0,0 +1,9 @@
+---
+name: Language specific issue
+about: Report a language-specific issue or feature request
+title: '[REPLACE ME WITH YOUR REQUESTED LANGUAGE]: '
+labels: 'area/linguistics'
+assignees: ''
+
+---
+
diff --git a/.github/ISSUE_TEMPLATE/4-tech-debt.md b/.github/ISSUE_TEMPLATE/4-tech-debt.md
new file mode 100644
index 0000000000..d562979ea9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/4-tech-debt.md
@@ -0,0 +1,10 @@
+---
+name: Tech Debt
+about: Track any tech debt or dev improvements
+title: ''
+labels: area/tech-debt
+assignees: ''
+
+---
+
+
diff --git a/.github/ISSUE_TEMPLATE/other-issue.md b/.github/ISSUE_TEMPLATE/5-other-issue.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/other-issue.md
rename to .github/ISSUE_TEMPLATE/5-other-issue.md
diff --git a/.github/actionlint-matcher.json b/.github/actionlint-matcher.json
new file mode 100644
index 0000000000..68878335f2
--- /dev/null
+++ b/.github/actionlint-matcher.json
@@ -0,0 +1,17 @@
+{
+ "problemMatcher": [
+ {
+ "owner": "actionlint",
+ "pattern": [
+ {
+ "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$",
+ "file": 1,
+ "line": 2,
+ "column": 3,
+ "message": 4,
+ "code": 5
+ }
+ ]
+ }
+ ]
+}
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
new file mode 100644
index 0000000000..a14ac58736
--- /dev/null
+++ b/.github/actions/setup/action.yml
@@ -0,0 +1,25 @@
+name: Setup
+description: Setup the workspace for the CI
+runs:
+ using: "composite"
+ steps:
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: "package.json"
+
+ - name: Restore dependencies
+ id: restore-dependencies
+ uses: actions/cache@v4
+ with:
+ path: node_modules
+ key: js-depend-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
+
+ - name: Install dependencies
+ if: steps.restore-dependencies.outputs.cache-hit != 'true'
+ shell: bash
+ run: npm ci
+
+ - name: Build third-party libraries
+ shell: bash
+ run: npm run build:libs
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 438e68c21e..2378bc5ac6 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -31,7 +31,7 @@ updates:
# These dependencies should be updated manually:
- dependency-name: "vitest"
# Benchmarking is an experimental feature in vitest:
- # https://github.com/themoeway/yomitan/pull/583#issuecomment-1925047371
+ # https://github.com/yomidevs/yomitan/pull/583#issuecomment-1925047371
- dependency-name: "@vitest/coverage-v8"
# Pinned to stay on the same version as vitest
- dependency-name: "@types/node"
diff --git a/.github/release.yml b/.github/release.yml
index c3cb6841ff..7e3539e344 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -6,7 +6,7 @@ changelog:
- title: Breaking Changes
labels:
- kind/breaking-change
- - title: Enhancement
+ - title: Enhancements
labels:
- kind/enhancement
- title: Bug Fixes
diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml
new file mode 100644
index 0000000000..d5d639a6af
--- /dev/null
+++ b/.github/workflows/actionlint.yml
@@ -0,0 +1,14 @@
+name: Lint GitHub Actions workflows
+on: [push, pull_request]
+
+jobs:
+ actionlint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Check workflow files
+ run: |
+ echo "::add-matcher::.github/actionlint-matcher.json"
+ bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/v1.7.4/scripts/download-actionlint.bash)
+ ./actionlint -color
+ shell: bash
diff --git a/.github/workflows/auto-approve-run.yml b/.github/workflows/auto-approve-run.yml
index e57f838867..799d32ac4c 100644
--- a/.github/workflows/auto-approve-run.yml
+++ b/.github/workflows/auto-approve-run.yml
@@ -14,7 +14,7 @@ jobs:
if: github.actor == 'djahandarie'
steps:
- name: Download workflow artifact
- uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
+ uses: dawidd6/action-download-artifact@80620a5d27ce0ae443b965134db88467fc607b43 # v7
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: auto-approve.yml
diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml
index d86ead8985..be48f97208 100644
--- a/.github/workflows/auto-approve.yml
+++ b/.github/workflows/auto-approve.yml
@@ -9,9 +9,9 @@ jobs:
shell: bash
env:
PR_NUM: ${{ github.event.number }}
- run: echo $PR_NUM > pr_num.txt
+ run: echo "$PR_NUM" > pr_num.txt
- name: Upload the PR number
uses: actions/upload-artifact@v4
with:
name: pr_num
- path: ./pr_num.txt
\ No newline at end of file
+ path: ./pr_num.txt
diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
deleted file mode 100644
index 775e94a711..0000000000
--- a/.github/workflows/bench.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: Performance Benchmarks
-
-on:
- push:
- branches: [master]
- pull_request:
- workflow_dispatch:
-jobs:
- benchmark:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup node
- uses: actions/setup-node@v4
- with:
- node-version-file: "package.json"
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build Libs
- run: npm run build-libs
-
- - name: Run Benchmarks
- uses: CodSpeedHQ/action@v2
- with:
- token: ${{ secrets.CODSPEED_TOKEN }}
- run: npm run bench
diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml
index 4cea3b06b3..3a619879b5 100644
--- a/.github/workflows/broken-links.yml
+++ b/.github/workflows/broken-links.yml
@@ -20,8 +20,8 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build Legal
- run: npm run license-report
- - uses: lycheeverse/lychee-action@c053181aa0c3d17606addfe97a9075a32723548a
+ run: npm run license-report:html
+ - uses: lycheeverse/lychee-action@v2.1.0
with:
fail: true
jobSummary: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4ea950b841..78efc70eef 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,58 +10,71 @@ on:
merge_group:
jobs:
- test:
+ tests:
+ name: ${{ matrix.name }}
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - name: JavaScript
+ test: test:js
+ - name: TypeScript (main)
+ test: test:ts:main
+ - name: TypeScript (dev)
+ test: test:ts:dev
+ - name: TypeScript (test)
+ test: test:ts:test
+ - name: TypeScript (bench)
+ test: test:ts:bench
+ - name: CSS
+ test: test:css
+ - name: HTML
+ test: test:html
+ - name: Markdown
+ test: test:md
+ - name: JSON
+ test: test:json
+ - name: Unit Tests
+ test: test:unit
+ - name: Unit Tests (options)
+ test: test:unit:options
steps:
- - name: Checkout
- uses: actions/checkout@v4
+ - uses: actions/checkout@v4
+ - uses: ./.github/actions/setup
- - name: Setup node
- uses: actions/setup-node@v4
- with:
- node-version-file: "package.json"
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build Libs
- run: npm run build-libs
-
- - name: Lint JS
- run: npm run test-lint-js
- env:
- CI: true
-
- - name: Validate JS Types
- run: npm run test-ts
- env:
- CI: true
-
- - name: Lint CSS
- run: npm run test-lint-css
- env:
- CI: true
-
- - name: Lint HTML
- run: npm run test-lint-html
- env:
- CI: true
+ - name: Run ${{ matrix.name }} tests
+ run: npm run ${{ matrix.test }}
- - name: Tests
- run: npm run test-code
- env:
- CI: true
+ test-build:
+ name: Test Build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/actions/setup
- name: Build Legal
- run: npm run license-report
+ run: npm run license-report:html
- name: Build
run: npm run build
- name: Validate manifest.json of the extension
- uses: cardinalby/schema-validator-action@76c68bfc941bd2dc82859f2528984999d1df36a4 # v3.1.0
+ uses: cardinalby/schema-validator-action@2166123eb256fa40baef7e22ab1379708425efc7 # v3.1.1
with:
file: ext/manifest.json
schema: "https://json.schemastore.org/chrome-manifest.json"
fixSchemas: true
+
+ bench:
+ name: Benchmarks
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/actions/setup
+
+ - name: Run Benchmarks
+ uses: CodSpeedHQ/action@513a19673a831f139e8717bf45ead67e47f00044 # v3.2
+ with:
+ token: ${{ secrets.CODSPEED_TOKEN }}
+ run: npm run bench
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index cf7f1f145d..a6f38b252e 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -10,7 +10,8 @@
# supported CodeQL languages.
#
name: "CodeQL"
-
+permissions:
+ contents: read
on:
push:
branches: [ "master" ]
@@ -50,7 +51,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3
+ uses: github/codeql-action/init@v3.27.5
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -64,7 +65,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v3
+ uses: github/codeql-action/autobuild@v3.27.5
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -77,6 +78,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v3.27.5
with:
category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/create-prerelease-on-tag.yml b/.github/workflows/create-prerelease-on-tag.yml
index 6f82a85e32..f86e4fd6a0 100644
--- a/.github/workflows/create-prerelease-on-tag.yml
+++ b/.github/workflows/create-prerelease-on-tag.yml
@@ -22,11 +22,12 @@ jobs:
with:
node-version-file: "package.json"
+ # intentially do not use cache to keep the build more comprehensible and sandboxed
- name: Install dependencies
run: npm ci
- name: Build Legal
- run: npm run license-report
+ run: npm run license-report:html
- name: Build
run: npm run-script build -- --all --version ${{ github.ref_name }}
@@ -36,25 +37,26 @@ jobs:
id: hash
run: |
cd builds
- echo "hashes=$(sha256sum * | base64 -w0)" >> "$GITHUB_OUTPUT"
+ echo "hashes=$(sha256sum -- * | base64 -w0)" >> "$GITHUB_OUTPUT"
- name: Release
id: release
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # pin@v0.1.15
+ uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # pin@v2
with:
generate_release_notes: true
prerelease: true
files: builds/*
- name: Dispatch publish-chrome-development
- uses: aurelien-baudet/workflow-dispatch@93e95b157d791ae7f42aef8f8a0d3d723eba1c31 # pin@v2
+ uses: aurelien-baudet/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 # pin@v2
with:
workflow: publish-chrome-development
token: ${{ secrets.GITHUB_TOKEN }}
wait-for-completion: false
+ inputs: '{ "upload_url": "${{ steps.release.outputs.upload_url }}" }'
- name: Dispatch publish-firefox-development
- uses: aurelien-baudet/workflow-dispatch@93e95b157d791ae7f42aef8f8a0d3d723eba1c31 # pin@v2
+ uses: aurelien-baudet/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 # pin@v2
with:
workflow: publish-firefox-development
token: ${{ secrets.GITHUB_TOKEN }}
@@ -67,7 +69,7 @@ jobs:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true
diff --git a/.github/workflows/delay.yml b/.github/workflows/delay.yml
index 11d29bd9b5..e77da22ca4 100644
--- a/.github/workflows/delay.yml
+++ b/.github/workflows/delay.yml
@@ -31,7 +31,7 @@ jobs:
actions: write
steps:
- name: Start the next attempt
- uses: aurelien-baudet/workflow-dispatch@93e95b157d791ae7f42aef8f8a0d3d723eba1c31 # pin@v2
+ uses: aurelien-baudet/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 # pin@v2
with:
workflow: ${{ github.event.inputs.workflow }}
token: ${{ secrets.GITHUB_TOKEN }}
@@ -39,6 +39,5 @@ jobs:
inputs: |
{
"attemptNumber": "${{ github.event.inputs.attemptNumber }}",
- "maxAttempts": "${{ github.event.inputs.maxAttempts }}",
- "environment": "${{ github.event.inputs.environment }}"
+ "maxAttempts": "${{ github.event.inputs.maxAttempts }}"
}
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000000..915f101f6e
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,27 @@
+# Dependency Review Action
+#
+# This Action will scan dependency manifest files that change as part of a Pull Request,
+# surfacing known-vulnerable versions of the packages declared or updated in the PR.
+# Once installed, if the workflow run is marked as required,
+# PRs introducing known-vulnerable packages will be blocked from merging.
+#
+# Source repository: https://github.com/actions/dependency-review-action
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+ contents: read
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
+ with:
+ egress-policy: audit
+
+ - name: 'Checkout Repository'
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ - name: 'Dependency Review'
+ uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 1c2ba30671..5c2f1db420 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -10,24 +10,23 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@v2
+ with:
+ egress-policy: audit
+
- name: Remove all fonts
run: rm -rf /usr/share/fonts
- uses: actions/checkout@v4
- name: Install CJK fonts
- uses: awalsh128/cache-apt-pkgs-action@44c33b32f808cdddd5ac0366d70595ed63661ed8 # v1.3.1
+ uses: awalsh128/cache-apt-pkgs-action@a6c3917cc929dd0345bfb2d3feaf9101823370ad # v1.4.2
with:
packages: fonts-ipafont-mincho
execute_install_scripts: true
- - uses: actions/setup-node@v4
- with:
- cache: "npm"
- node-version-file: "package.json"
-
- - name: Install dependencies
- run: npm ci
+ - uses: ./.github/actions/setup
- name: Build
run: npm run build
@@ -47,17 +46,17 @@ jobs:
- name: Grab latest dictionaries from dictionaries branch
uses: actions/checkout@v4
with:
- repository: themoeway/yomitan # so that this works on forks
+ repository: yomidevs/yomitan # so that this works on forks
ref: dictionaries
path: dictionaries
- name: Grab latest screenshots from master branch
- uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # pin@v2
+ uses: dawidd6/action-download-artifact@80620a5d27ce0ae443b965134db88467fc607b43 # pin@v2
continue-on-error: true
id: master-screenshots
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- repo: themoeway/yomitan # so that this works on forks
+ repo: yomidevs/yomitan # so that this works on forks
name: playwright-screenshots
branch: master
workflow: playwright.yml
@@ -101,3 +100,9 @@ jobs:
name: playwright-output
path: playwright-output
if: github.event_name == 'pull_request'
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: playwright-results-json
+ path: playwright-results.json
+ if: github.event_name == 'pull_request'
diff --git a/.github/workflows/playwright_comment.yml b/.github/workflows/playwright_comment.yml
index 8ca33c2830..9a9938e24d 100644
--- a/.github/workflows/playwright_comment.yml
+++ b/.github/workflows/playwright_comment.yml
@@ -15,8 +15,13 @@ jobs:
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
+ with:
+ egress-policy: audit
+
- name: Grab playwright-output from PR run
- uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # pin@v2
+ uses: dawidd6/action-download-artifact@80620a5d27ce0ae443b965134db88467fc607b43 # v7
continue-on-error: true
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -24,57 +29,114 @@ jobs:
name: playwright-output
- name: Grab master-screenshots-outcome from PR run
- uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # pin@v2
+ uses: dawidd6/action-download-artifact@80620a5d27ce0ae443b965134db88467fc607b43 # v7
continue-on-error: true
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
run_id: ${{ github.event.workflow_run.id }}
name: master-screenshots-outcome
+ - name: Grab playwright-results-json from PR run
+ uses: dawidd6/action-download-artifact@80620a5d27ce0ae443b965134db88467fc607b43 # v7
+ continue-on-error: true
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ run_id: ${{ github.event.workflow_run.id }}
+ name: playwright-results-json
+
+ - name: Dry-run grab playwright-report from PR run so we have its ID
+ uses: dawidd6/action-download-artifact@80620a5d27ce0ae443b965134db88467fc607b43 # v7
+ id: playwright-report
+ continue-on-error: true
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ run_id: ${{ github.event.workflow_run.id }}
+ name: playwright-report
+ dry_run: true
+
+ - name: Store playwright-report ID
+ id: playwright-report-artifact-id
+ env:
+ ARTIFACTS_JSON: ${{ steps.playwright-report.outputs.artifacts }}
+ run: |
+ ID=$(echo "$ARTIFACTS_JSON" | jq -r '.[0].id');
+ echo "id=$ID" >> "$GITHUB_OUTPUT"
+
+ - name: Generate summary from playwright-results.json (expected to fail to comment)
+ id: playwright-summary
+ uses: daun/playwright-report-summary@v3
+ with:
+ report-file: playwright-results.json
+
- name: Load artifacts into environment variables
id: playwright
run: |
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
- echo "PLAYWRIGHT_OUTPUT<<$EOF" >> $GITHUB_OUTPUT
- cat ./playwright-output >> $GITHUB_OUTPUT
- echo "$EOF" >> $GITHUB_OUTPUT
- echo "MASTER_SCREENSHOTS_OUTCOME<<$EOF" >> $GITHUB_OUTPUT
- cat ./master-screenshots-outcome >> $GITHUB_OUTPUT
- echo "$EOF" >> $GITHUB_OUTPUT
- echo "FAILED=$(grep -c '^ *[0-9] failed$' $GITHUB_OUTPUT)" >> $GITHUB_OUTPUT
+ {
+ echo "PLAYWRIGHT_OUTPUT<<$EOF";
+ cat ./playwright-output;
+ echo "$EOF";
+ echo "MASTER_SCREENSHOTS_OUTCOME<<$EOF";
+ cat ./master-screenshots-outcome;
+ echo "$EOF";
+ echo "FAILED=$(grep -c '^ *[0-9] failed$' ./playwright-output)";
+ echo "FLAKY=$(grep -c '^ *[0-9] flaky$' ./playwright-output)"
+ } >> "$GITHUB_OUTPUT"
# this is required because github.event.workflow_run.pull_requests is not available for PRs from forks
- - name: "Get PR information"
- uses: potiuk/get-workflow-origin@e2dae063368361e4cd1f510e8785cd73bca9352e # pin@v1_5
+ - name: Get PR context
id: source-run-info
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
- sourceRunId: ${{ github.event.workflow_run.id }}
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # Find the most recently updated open PR at the repo with the requested commit:
+ SEARCH_QUERY: >-
+ type:pr state:open sort:updated-desc
+ repo:${{ github.repository }}
+ ${{ github.event.workflow_run.head_sha }}
+ # Minimal graphql search query to fetch the PR `number` field:
+ GQL: |-
+ query($filter: String!) {
+ search( query: $filter, type: ISSUE, first: 1) {
+ nodes { ... on PullRequest { number } }
+ }
+ }
+ # Formats the GQL response into a `key=value` string + basic error handling
+ JQ_FILTER: >-
+ .data.search.nodes[0]
+ | if (.number == null) then error("Could not find PR number") end
+ | "pullRequestNumber=\(.number)"
+ run: |
+ gh api graphql --field "filter=$SEARCH_QUERY" --raw-field "query=$GQL" --jq "$JQ_FILTER" >> "${GITHUB_OUTPUT}"
- name: "[Comment] Couldn't download screenshots from master branch"
- uses: mshick/add-pr-comment@dd126dd8c253650d181ad9538d8b4fa218fc31e8 # pin@v2
+ uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
if: steps.playwright.outputs.MASTER_SCREENSHOTS_OUTCOME == 'failure'
with:
issue: ${{ steps.source-run-info.outputs.pullRequestNumber }}
message: |
:heavy_exclamation_mark: Could not fetch screenshots from master branch, so had nothing to make a visual comparison against; please check the "master-screenshots" step in the workflow run and rerun it before merging.
- - name: "[Comment] Success: No visual differences introduced by this PR"
- uses: mshick/add-pr-comment@dd126dd8c253650d181ad9538d8b4fa218fc31e8 # pin@v2
- if: steps.playwright.outputs.MASTER_SCREENSHOTS_OUTCOME != 'failure' && steps.playwright.outputs.FAILED == 0
+ - name: "[Comment] Warning: Visual differences caused by this PR; please check the playwright report"
+ uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
+ if: steps.playwright.outputs.MASTER_SCREENSHOTS_OUTCOME != 'failure' && steps.playwright.outputs.FAILED != 0
with:
issue: ${{ steps.source-run-info.outputs.pullRequestNumber }}
message: |
- :heavy_check_mark: No visual differences introduced by this PR.
+ :warning: Visual changes detected by playwright; please check the report to verify if they are desirable.
- View Playwright Report (note: open the "playwright-report" artifact)
-
- - name: "[Comment] Warning: Visual differences introduced by this PR"
- uses: mshick/add-pr-comment@dd126dd8c253650d181ad9538d8b4fa218fc31e8 # pin@v2
- if: steps.playwright.outputs.MASTER_SCREENSHOTS_OUTCOME != 'failure' && steps.playwright.outputs.FAILED != 0
+ - name: "[Comment] Success (but flaky): No visual differences introduced by this PR (but flaky tests detected)"
+ uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
+ if: steps.playwright.outputs.MASTER_SCREENSHOTS_OUTCOME != 'failure' && steps.playwright.outputs.FLAKY != 0 && steps.playwright.outputs.FAILED == 0
with:
issue: ${{ steps.source-run-info.outputs.pullRequestNumber }}
message: |
- :warning: Visual differences introduced by this PR; please validate if they are desirable.
+ :heavy_check_mark: No visual changes detected by playwright, but flaky tests were detected; please try to fix the tests.
- View Playwright Report (note: open the "playwright-report" artifact)
+ - name: "[Comment] Success: No visual differences introduced by this PR"
+ uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
+ if: steps.playwright.outputs.MASTER_SCREENSHOTS_OUTCOME != 'failure' && steps.playwright.outputs.FLAKY == 0 && steps.playwright.outputs.FAILED == 0
+ with:
+ issue: ${{ steps.source-run-info.outputs.pullRequestNumber }}
+ message: |
+ :heavy_check_mark: No visual changes detected by playwright.
+ update-only: true
diff --git a/.github/workflows/publish-chrome-development.yml b/.github/workflows/publish-chrome-development.yml
index 3b580e3dae..22ec2038cc 100644
--- a/.github/workflows/publish-chrome-development.yml
+++ b/.github/workflows/publish-chrome-development.yml
@@ -10,6 +10,9 @@ on:
description: "Max attempts"
required: false
default: "10"
+ upload_url:
+ description: "The upload_url from the release created by create-prerelease-on-tag.yml"
+ required: true
permissions:
contents: read
jobs:
@@ -18,13 +21,13 @@ jobs:
environment: cd
outputs:
result: ${{ steps.webStorePublish.outcome }}
- releaseUploadUrl: ${{ steps.getZipAsset.outputs.releaseUploadUrl }}
permissions:
actions: write
+ contents: write
steps:
- name: Get the next attempt number
id: getNextAttemptNumber
- uses: cardinalby/js-eval-action@b34865f1d9cfdf35356013627474857cfe0d5091 # pin@v1.0.7
+ uses: cardinalby/js-eval-action@e905fd3681d757e992c976f61c2784dcb4060e13 # pin@v1.0.9
env:
attemptNumber: ${{ github.event.inputs.attemptNumber }}
maxAttempts: ${{ github.event.inputs.maxAttempts }}
@@ -38,14 +41,14 @@ jobs:
return attempt < max ? attempt + 1 : '';
}
- - uses: robinraju/release-downloader@368754b9c6f47c345fcfbf42bcb577c2f0f5f395 # pin@v1.9
+ - uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # pin@v1.11
with:
tag: ${{ github.ref_name }}
fileName: "*"
- name: Fetch Google API access token
id: fetchAccessToken
- uses: cardinalby/google-api-fetch-token-action@24c99245e2a2494cc4c4b1037203d319a184b15b # pin@v1.0.3
+ uses: cardinalby/google-api-fetch-token-action@f455422472a558d48d939e77a65cdcec38e707b5 # pin@v1.0.4
with:
clientId: ${{ secrets.G_CLIENT_ID }}
clientSecret: ${{ secrets.G_CLIENT_SECRET }}
@@ -54,7 +57,7 @@ jobs:
- name: Upload to Google Web Store
id: webStoreUpload
continue-on-error: true
- uses: cardinalby/webext-buildtools-chrome-webstore-upload-action@8db7a005529498d95d3e2e0166f6f4050d2b96a5 # pin@v1.0.10
+ uses: cardinalby/webext-buildtools-chrome-webstore-upload-action@3d829e042b559c35f7fb71676cbaf6031892a313 # v1.0.11
with:
zipFilePath: yomitan-chrome-dev.zip
extensionId: ${{ secrets.G_DEVELOPMENT_EXTENSION_ID }}
@@ -65,7 +68,7 @@ jobs:
# Schedule a next attempt if store refused to accept new version because it
# still has a previous one in review
- name: Start the next attempt with the delay
- uses: aurelien-baudet/workflow-dispatch@93e95b157d791ae7f42aef8f8a0d3d723eba1c31 # pin@v2
+ uses: aurelien-baudet/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 # pin@v2
if: |
steps.getNextAttemptNumber.outputs.result &&
steps.webStoreUpload.outputs.inReviewError == 'true'
@@ -95,3 +98,34 @@ jobs:
with:
extensionId: ${{ secrets.G_DEVELOPMENT_EXTENSION_ID }}
apiAccessToken: ${{ steps.fetchAccessToken.outputs.accessToken }}
+
+ release-crx:
+ runs-on: ubuntu-latest
+ environment: cd
+ permissions:
+ actions: write
+ contents: write
+
+ steps:
+ - uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # pin@v1.11
+ with:
+ tag: ${{ github.ref_name }}
+ fileName: "*"
+
+ - name: Sign Chrome crx for offline distribution
+ uses: cardinalby/webext-buildtools-chrome-crx-action@v2
+ with:
+ zipFilePath: 'yomitan-chrome-dev.zip'
+ crxFilePath: 'yomitan-chrome-dev.crx'
+ privateKey: ${{ secrets.CHROME_CRX_PRIVATE_KEY }}
+
+ - name: Upload offline crx release asset
+ id: uploadReleaseAsset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ inputs.upload_url }}
+ asset_path: yomitan-chrome-dev.crx
+ asset_name: yomitan-chrome-dev.crx
+ asset_content_type: application/x-chrome-extension
diff --git a/.github/workflows/publish-chrome.yml b/.github/workflows/publish-chrome.yml
index 3dc37039e3..d88b61c047 100644
--- a/.github/workflows/publish-chrome.yml
+++ b/.github/workflows/publish-chrome.yml
@@ -18,13 +18,12 @@ jobs:
environment: cd
outputs:
result: ${{ steps.webStorePublish.outcome }}
- releaseUploadUrl: ${{ steps.getZipAsset.outputs.releaseUploadUrl }}
permissions:
actions: write
steps:
- name: Get the next attempt number
id: getNextAttemptNumber
- uses: cardinalby/js-eval-action@b34865f1d9cfdf35356013627474857cfe0d5091 # pin@v1.0.7
+ uses: cardinalby/js-eval-action@e905fd3681d757e992c976f61c2784dcb4060e13 # pin@v1.0.9
env:
attemptNumber: ${{ github.event.inputs.attemptNumber }}
maxAttempts: ${{ github.event.inputs.maxAttempts }}
@@ -38,14 +37,14 @@ jobs:
return attempt < max ? attempt + 1 : '';
}
- - uses: robinraju/release-downloader@368754b9c6f47c345fcfbf42bcb577c2f0f5f395 # pin@v1.9
+ - uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # pin@v1.11
with:
tag: ${{ github.ref_name }}
fileName: "*"
- name: Fetch Google API access token
id: fetchAccessToken
- uses: cardinalby/google-api-fetch-token-action@24c99245e2a2494cc4c4b1037203d319a184b15b # pin@v1.0.3
+ uses: cardinalby/google-api-fetch-token-action@f455422472a558d48d939e77a65cdcec38e707b5 # pin@v1.0.4
with:
clientId: ${{ secrets.G_CLIENT_ID }}
clientSecret: ${{ secrets.G_CLIENT_SECRET }}
@@ -54,7 +53,7 @@ jobs:
- name: Upload to Google Web Store
id: webStoreUpload
continue-on-error: true
- uses: cardinalby/webext-buildtools-chrome-webstore-upload-action@8db7a005529498d95d3e2e0166f6f4050d2b96a5 # pin@v1.0.10
+ uses: cardinalby/webext-buildtools-chrome-webstore-upload-action@3d829e042b559c35f7fb71676cbaf6031892a313 # v1.0.11
with:
zipFilePath: yomitan-chrome.zip
extensionId: ${{ secrets.G_STABLE_EXTENSION_ID }}
@@ -65,7 +64,7 @@ jobs:
# Schedule a next attempt if store refused to accept new version because it
# still has a previous one in review
- name: Start the next attempt with the delay
- uses: aurelien-baudet/workflow-dispatch@93e95b157d791ae7f42aef8f8a0d3d723eba1c31 # pin@v2
+ uses: aurelien-baudet/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 # pin@v2
if: |
steps.getNextAttemptNumber.outputs.result &&
steps.webStoreUpload.outputs.inReviewError == 'true'
diff --git a/.github/workflows/publish-edge.yml b/.github/workflows/publish-edge.yml
new file mode 100644
index 0000000000..f3fc289981
--- /dev/null
+++ b/.github/workflows/publish-edge.yml
@@ -0,0 +1,24 @@
+name: publish-edge
+on: workflow_dispatch
+permissions:
+ contents: read
+jobs:
+ upload-on-webstore:
+ runs-on: ubuntu-latest
+ environment: cd
+ outputs:
+ result: ${{ steps.webStorePublish.outcome }}
+ steps:
+ - uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # pin@v1.11
+ with:
+ tag: ${{ github.ref_name }}
+ fileName: "*"
+
+ - name: Publish on Microsoft Edge Addons
+ uses: wdzeng/edge-addon@v2
+ id: webStorePublish
+ with:
+ product-id: 18e6c4cd-6383-4f38-95e9-92a629f60817
+ zip-path: yomitan-edge.zip
+ client-id: ${{ secrets.EDGE_CLIENT_ID }}
+ api-key: ${{ secrets.EDGE_API_KEY }}
\ No newline at end of file
diff --git a/.github/workflows/publish-firefox-development.yml b/.github/workflows/publish-firefox-development.yml
index c92b9d24c9..bcd568b13c 100644
--- a/.github/workflows/publish-firefox-development.yml
+++ b/.github/workflows/publish-firefox-development.yml
@@ -20,7 +20,7 @@ jobs:
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- - uses: robinraju/release-downloader@368754b9c6f47c345fcfbf42bcb577c2f0f5f395 # pin@v1.9
+ - uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # pin@v1.11
with:
tag: ${{ github.ref_name }}
fileName: "*"
@@ -28,7 +28,7 @@ jobs:
- name: Sign Firefox xpi for offline distribution
id: ffSignXpi
continue-on-error: true
- uses: cardinalby/webext-buildtools-firefox-sign-xpi-action@94a2e58141e33c4306a72a93f191e8540189df92 # pin@v1.0.6
+ uses: cardinalby/webext-buildtools-firefox-sign-xpi-action@6c31e947111a95f05682fc98c6340367cce49cdc # pin@v1.0.8
with:
timeoutMs: 1200000
extensionId: ${{ secrets.FF_OFFLINE_EXT_ID }}
@@ -101,7 +101,7 @@ jobs:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true
diff --git a/.github/workflows/publish-firefox.yml b/.github/workflows/publish-firefox.yml
index f6499d01a7..f6d7a5d41b 100644
--- a/.github/workflows/publish-firefox.yml
+++ b/.github/workflows/publish-firefox.yml
@@ -8,17 +8,19 @@ jobs:
runs-on: ubuntu-latest
environment: cd
steps:
- - uses: robinraju/release-downloader@368754b9c6f47c345fcfbf42bcb577c2f0f5f395 # pin@v1.9
+ - uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # pin@v1.11
with:
tag: ${{ github.ref_name }}
+ zipBall: true
fileName: "*"
- name: Deploy to Firefox Addons
id: addonsDeploy
- uses: cardinalby/webext-buildtools-firefox-addons-action@924ad87df7e4af50a654c164ad9e498dce260ffa # pin@v1.0.9
+ uses: cardinalby/webext-buildtools-firefox-addons-action@987e338100095280ec8daf942e5640aeb55d3647 # pin@v1.0.10
continue-on-error: true
with:
zipFilePath: yomitan-firefox.zip
+ sourcesZipFilePath: yomitan-${{ github.ref_name }}.zip
extensionId: ${{ secrets.FF_EXTENSION_ID }}
jwtIssuer: ${{ secrets.FF_JWT_ISSUER }}
jwtSecret: ${{ secrets.FF_JWT_SECRET }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b700e2c459..9b61a0d351 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -8,18 +8,24 @@ jobs:
runs-on: ubuntu-latest
permissions:
actions: write
- contents: write
steps:
- name: Dispatch publish-chrome
- uses: aurelien-baudet/workflow-dispatch@93e95b157d791ae7f42aef8f8a0d3d723eba1c31 # pin@v2
+ uses: aurelien-baudet/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 # pin@v2
with:
workflow: publish-chrome
token: ${{ secrets.GITHUB_TOKEN }}
wait-for-completion: false
- name: Dispatch publish-firefox
- uses: aurelien-baudet/workflow-dispatch@93e95b157d791ae7f42aef8f8a0d3d723eba1c31 # pin@v2
+ uses: aurelien-baudet/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 # pin@v2
with:
workflow: publish-firefox
token: ${{ secrets.GITHUB_TOKEN }}
wait-for-completion: false
+
+ - name: Dispatch publish-edge
+ uses: aurelien-baudet/workflow-dispatch@3133c5d135c7dbe4be4f9793872b6ef331b53bc7 # pin@v2
+ with:
+ workflow: publish-edge
+ token: ${{ secrets.GITHUB_TOKEN }}
+ wait-for-completion: false
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 2128cfc6c3..04d6e3079c 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -34,7 +34,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
- uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
+ uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
with:
results_file: results.sarif
results_format: sarif
@@ -64,6 +64,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12
+ uses: github/codeql-action/upload-sarif@3d3d628990a5f99229dd9fa1821cc5a4f31b613b # v2.22.12
with:
sarif_file: results.sarif
diff --git a/.github/workflows/touch-google-refresh-token.yml b/.github/workflows/touch-google-refresh-token.yml
index 9c4e2ec9bc..03b407513b 100644
--- a/.github/workflows/touch-google-refresh-token.yml
+++ b/.github/workflows/touch-google-refresh-token.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
environment: cd
steps:
- - uses: cardinalby/google-api-fetch-token-action@24c99245e2a2494cc4c4b1037203d319a184b15b # pin@v1.0.3
+ - uses: cardinalby/google-api-fetch-token-action@f455422472a558d48d939e77a65cdcec38e707b5 # pin@v1.0.4
with:
clientId: ${{ secrets.G_CLIENT_ID }}
clientSecret: ${{ secrets.G_CLIENT_SECRET }}
diff --git a/.gitignore b/.gitignore
index 459af2b711..74b9494584 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,5 +14,3 @@ dictionaries/
ext/manifest.json
ext/lib/*
-
-ext/legal-npm.html
diff --git a/.htmlvalidate.json b/.htmlvalidate.json
index bb3c522105..c9b9f8ab1f 100644
--- a/.htmlvalidate.json
+++ b/.htmlvalidate.json
@@ -15,4 +15,4 @@
"elements": [
"html5"
]
-}
\ No newline at end of file
+}
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 36af219892..db4b7380a7 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1,2 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-
+echo "Husky pre-commit hook"
npx lint-staged
diff --git a/.lycheeignore b/.lycheeignore
index 0e782c8218..45e028fec3 100644
--- a/.lycheeignore
+++ b/.lycheeignore
@@ -1,8 +1,12 @@
://localhost
-://127.0.0.1
+://127\.0\.0\.1
%7Bversion%7D
-https://www.edrdg.org/jmdict/edict.html_blanknoopenerhttps://www.edrdg.org/wiki/index.php/KANJIDIC_Project_blanknoopenerhttps://www.edrdg.org/_blanknoopenerhttps://www.edrdg.org/edrdg/licence.html_blanknoopener
^chrome://
^edge://
^about:
^data:
+^https://chrome\.google\.com/webstore/detail/rikaikun/jipdnfibhldikgcjhfnomkfpcebammhp$
+^https://chrome\.google\.com/webstore/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn$
+^https://chrome\.google\.com/webstore/detail/yomitan-development-build/glnaenfapkkecknnmginabpmgkenenml$
+^https://forvo\.com/$
+^https://code.visualstudio.com/docs/languages/json#_json-schemas-and-settings$
\ No newline at end of file
diff --git a/.stylelintrc.json b/.stylelintrc.json
index eb18af0129..d83b25bdf8 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -59,4 +59,4 @@
"@stylistic/string-quotes": "single",
"@stylistic/unit-case": "lower"
}
-}
\ No newline at end of file
+}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index a5e243212b..cfa9a290fe 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -2,6 +2,7 @@
"recommendations": [
"dbaeumer.vscode-eslint",
"html-validate.vscode-html-validate",
- "stylelint.vscode-stylelint"
+ "stylelint.vscode-stylelint",
+ "codeandstuff.package-json-upgrade"
]
-}
\ No newline at end of file
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000000..3f4fa17564
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,20 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch via NPM",
+ "request": "launch",
+ "runtimeArgs": [
+ "run",
+ "test:unit",
+ "--"
+ // "test/text-source-range.test.js" // Replace and uncomment this line to run a single test file
+ ],
+ "runtimeExecutable": "npm",
+ "skipFiles": [
+ "/**"
+ ],
+ "type": "node"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index bb9b3c80bd..e61cf30ca7 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,18 +5,20 @@
"source.addMissingImports": "explicit",
"source.organizeImports": "explicit",
"source.fixAll.eslint": "explicit"
- },
+ }
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.addMissingImports": "never",
"source.organizeImports": "never",
"source.fixAll.eslint": "explicit"
- },
+ }
},
"eslint.format.enable": true,
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false,
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false,
+ "javascript.format.enable": false,
+ "typescript.format.enable": false,
"javascript.preferences.importModuleSpecifierEnding": "js",
"editor.tabSize": 4,
"editor.insertSpaces": true,
@@ -30,5 +32,15 @@
"files.trimTrailingWhitespace": true,
"html-validate.validate": [
"html"
- ]
+ ],
+ "json.schemas": [
+ {
+ "fileMatch": [
+ "/ext/data/recommended-dictionaries.json"
+ ],
+ "url": "/ext/data/schemas/recommended-dictionaries-schema.json"
+ }
+ ],
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix"
}
diff --git a/CODEOWNERS b/CODEOWNERS
index fe1015b183..ef576e9e5d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1 +1 @@
-* @themoeway/yomitan-codeowners
+* @yomidevs/yomitan-codeowners
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 28d02c0baf..7dbef60bd7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Issues and Features
-Issues reported on [GitHub](https://github.com/themoeway/yomitan/issues) should include information about:
+Issues reported on [GitHub](https://github.com/yomidevs/yomitan/issues) should include information about:
- What the problem, question, or request is.
- What browser is being used.
@@ -22,7 +22,7 @@ Below are a few guidelines to ensure contributions have a good level of quality
## Setup
Yomitan uses [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/) tools for building and testing.
-After installing these Node.js, the development environment can be set up by running `npm ci`.
+After installing these, the development environment can be set up by running `npm ci` and subsequently `npm run build`.
## Testing
@@ -30,6 +30,14 @@ Unit tests, integration tests, and various other tests can be executed by runnin
Other individual tests can be looked up in the [package.json](package.json) file, and the source for specific tests
can be found in the [test](test) directory
+### Playwright
+
+Steps to run [playwright](https://playwright.dev/) tests locally:
+
+1. Run `npx playwright install` to install the headless browsers
+2. Copy the dictionary test data located in the `dictionaries` branch to a directory named `dictionaries` via `git clone --branch dictionaries git@github.com:yomidevs/yomitan.git dictionaries` ([source](https://github.com/yomidevs/yomitan/blob/086e043856ad54cf13cb65f9ba4c63afe8a22cc3/.github/workflows/playwright.yml#L52-L57)).
+3. Now you can run `npx playwright test`. The first run might produce some benign errors complaining about `Error: A snapshot doesn't exist at ...writing actual.`, but subsequent runs should succeed.
+
## Building
By default, the development repository is configured for Chrome, and the [ext](ext) directory can be directly
@@ -57,6 +65,19 @@ Several command line arguments are available for these scripts:
If no arguments are specified, the command is equivalent to `build.bat --all`.
+### Loading an unpacked build into Chromium browsers
+
+After building, you can load the compiled extension into Chromium browsers.
+
+- Navigate to the [extensions page](chrome://extensions/)
+- Turn on the toggle on the top right that says "Developer Mode"
+- Click "Load Unpacked" on the top left
+- Select the `ext` folder.
+
+Immediately you should see the "Welcome" page!
+
+Note: Yomitan may or may not update when you make and save new code changes locally. It depends on what file you've changed. Yomitan runs as collection of two programs. There is the background process called the "service worker" and there is the frontend called the "content_script". The frontend will reload on save, but to update the backend you need to click on the update icon next to the extension in `chrome://extensions/`. If you make changes to the manifest you will need to rerun `npm run build` to regenerate the manifest file.
+
### Build Tools
The build process can use the [7-zip](https://www.7-zip.org/) archiving tool to create the packed zip builds
@@ -75,7 +96,7 @@ The generated `ext/manfiest.json` should not be committed.
Linting rules are defined for a few types of files, and validation is performed as part of the standard tests
run by `npm test` and the continuous integration process.
-- [.eslintrc.json](.eslintrc.json) rules are used for JavaScript files.
+- [eslint.config.js](eslint.config.js) rules are used for JavaScript files.
- [.stylelintrc.json](.stylelintrc.json) rules are used for CSS files.
- [.htmlvalidate.json](.htmlvalidate.json) rules are used for HTML files.
diff --git a/README.md b/README.md
index fffedcaff7..32a14406a4 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,56 @@
-# Yomitan
+# Yomitan
-[![Chrome Release]()](https://chrome.google.com/webstore/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn)
-[![Firefox Release]()](https://addons.mozilla.org/en-US/firefox/addon/yomitan/)
-[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/themoeway/yomitan/badge)](https://securityscorecards.dev/viewer/?uri=github.com/themoeway/yomitan)
-[![Discord](https://dcbadge.vercel.app/api/server/UGNPMDE7zC?style=flat)](https://discord.gg/UGNPMDE7zC)
+[![Get Yomitan for Chrome]()](https://chrome.google.com/webstore/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn)
+[![Get Yomitan for Firefox]()](https://addons.mozilla.org/en-US/firefox/addon/yomitan/)
+[![Get Yomitan for Edge](https://img.shields.io/badge/dynamic/json?logo=puzzle&label=get%20yomitan%20for%20edge&style=for-the-badge&query=%24.version&url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2Faddons%2Fgetproductdetailsbycrxid%2Fidelnfbbmikgfiejhgmddlbkfgiifnnn)](https://microsoftedge.microsoft.com/addons/detail/yomitan/idelnfbbmikgfiejhgmddlbkfgiifnnn)
+[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/yomidevs/yomitan/badge?style=for-the-badge)](https://securityscorecards.dev/viewer/?uri=github.com/yomidevs/yomitan)
-:wave: **This project is a community fork of Yomichan** (which was [sunset](https://foosoft.net/posts/sunsetting-the-yomichan-project/) by its owner on Feb 26 2023). We have made a number of foundational changes to ensure **the project stays alive, works on latest browser versions, and is easy to contribute to**:
+[![Discord](https://dcbadge.vercel.app/api/server/YkQrXW6TXF?style=for-the-badge)](https://discord.gg/YkQrXW6TXF)
-- Completed the Manifest V2 → V3 transition, [read why here!](https://developer.chrome.com/blog/resuming-the-transition-to-mv3/).
-- Switched to using ECMAScript modules and npm-sourced dependencies.
-- Implemented an end-to-end CI/CD pipeline.
-- Switched to standard testing frameworks, vitest and playwrights.
+# Visit [yomitan.wiki](https://yomitan.wiki) to learn more!
-In addition, we are beginning to make important bug fixes and minor enhancements:
+:wave: **Yomitan is [the successor](https://foosoft.net/posts/passing-the-torch-to-yomitan/) to Yomichan** ([migration guide](https://yomitan.wiki/yomichan-migration/)) which was [sunset](https://foosoft.net/posts/sunsetting-the-yomichan-project/) by its owner on Feb 26, 2023. We have made a number of foundational changes to ensure **the project stays alive, works on latest browser versions, and is easy to contribute to**.
-- Improve dictionary import speeds by 2x~10x.
-- Add functionality to import/export multiple dictionaries, enabling portability across devices.
-- And [more](https://github.com/themoeway/yomitan/pulls?q=is%3Apr+is%3Amerged+-label%3Aarea%2Fdependencies+-label%3Akind%2Fmeta).
+📢 **New contributors [welcome](#contributing)!**
-Since the owner requested forks be uniquely named, we have chosen a new name, _yomitan_. (_-tan_ is an honorific used for anthropomorphic moe characters.) While we've made some substantial changes, the majority of the extension's functionality is thanks to hard work of foosoft and numerous other open source contributors from 2016-2023.
+📢 **Interested in adding a new language to Yomitan? See [here](./docs/development/language-features.md) for thorough documentation!**
-## Contributing
-
-Since this is a distributed effort, we **highly welcome new contributors**! Feel free to browse the [issue tracker](https://github.com/themoeway/yomitan/issues), and read our [contributing guidelines](./CONTRIBUTING.md). You can also find us on [TheMoeWay Discord](https://discord.gg/UGNPMDE7zC) at [#yomitan-development](https://discord.com/channels/617136488840429598/1081538711742844980).
-
-## What's Yomitan?
+## What is Yomitan?
-Yomitan 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 [10ten Japanese Reader (formerly Rikaichamp)](https://addons.mozilla.org/en-US/firefox/addon/10ten-ja-reader/) for Firefox and [Rikaikun](https://chrome.google.com/webstore/detail/rikaikun/jipdnfibhldikgcjhfnomkfpcebammhp?hl=en) for Chrome, but it stands apart in its goal of being an all-encompassing learning tool as opposed to a mere browser-based dictionary.
+Yomitan turns your web browser into a tool for building language literacy by helping you **read** texts that would otherwise be too difficult to tackle in [a variety of supported languages](https://yomitan.wiki/supported-languages/).
-Yomitan provides advanced features not available in other browser-based dictionaries:
+Yomitan provides powerful features not available in other browser-based dictionaries:
-- Interactive popup definition window for displaying search results.
-- On-demand audio playback for select dictionary definitions.
-- Kanji stroke order diagrams are just a click away for most characters.
-- Custom search page for easily executing custom search queries.
-- Support for multiple dictionary formats including [EPWING](https://ja.wikipedia.org/wiki/EPWING) via the [Yomitan Import](https://github.com/themoeway/yomitan-import) tool.
-- Automatic note creation for the [Anki](https://apps.ankiweb.net/) flashcard program via the [AnkiConnect](https://foosoft.net/projects/anki-connect) plugin.
-- Clean, modern code makes it easy for developers to [contribute](https://github.com/themoeway/yomitan/blob/master/CONTRIBUTING.md) new features.
+- 💬 Interactive popup definition window for displaying search results.
+- 🔊 Built-in native pronunciation audio with the ability to add your own [custom audio sources](https://yomitan.wiki/advanced/#default-audio-sources).
+- ✍️ Kanji stroke order diagrams are just a click away.
+- 📝 [Automatic flashcard creation](https://yomitan.wiki/anki/) for the [Anki](https://apps.ankiweb.net/) flashcard program via the [AnkiConnect](https://foosoft.net/projects/anki-connect) plugin.
+- 🔍 Custom search page for easily executing custom search queries.
+- 📖 Support for multiple dictionary formats including [EPWING](https://ja.wikipedia.org/wiki/EPWING) via the [Yomitan Import](https://github.com/yomidevs/yomitan-import) tool.
+- ✨ Clean, modern code makes it easy for developers to [contribute](#contributing) new features and languages.
[![Term definitions](img/ss-terms-thumb.png)](img/ss-terms.png)
[![Kanji information](img/ss-kanji-thumb.png)](img/ss-kanji.png)
[![Dictionary options](img/ss-dictionaries-thumb.png)](img/ss-dictionaries.png)
[![Anki options](img/ss-anki-thumb.png)](img/ss-anki.png)
-## Helpful information
+## Documentation/How To
-- [Migrating from Yomichan (legacy)](./docs/yomichan-migration.md#migrating-from-yomichan)
-- [Importing standardised and custom dictionaries](./docs/dictionaries.md#dictionaries)
-- [Anki integration and flashcards creation](./docs/anki-integration.md#anki-integration)
-- [Advanced options, including MeCab](./docs/advanced-options.md#advanced-options)
-- [Frequently asked questions](./docs/faq.md#frequently-asked-questions)
-- [Keyboard shortcuts](./docs/keyboard-shortcuts.md)
+**Please visit the [Yomitan Wiki](https://yomitan.wiki) for the most up-to-date usage documentation.**
+
+### Developer Documentation
+
+- Dictionaries
+ - 🛠️ [Making Yomitan Dictionaries](./docs/making-yomitan-dictionaries.md)
+- Anki Integration
+ - 🔧 [Anki handlebar templates](./docs/templates.md)
+- Advanced Features
+- Troubleshooting
+ - 🕷️ [Known browser bugs](./docs/browser-bugs.md)
## Installation
-Yomitan comes in two flavors: _stable_ and _testing_. New changes are initially introduced into the _testing_ version, and after some time spent ensuring that they are 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.
+Yomitan comes in two flavors: _stable_ and _testing_. New changes are initially introduced into the _testing_ version, and after some time spent ensuring that they are relatively bug free, they will be promoted to the _stable_ version. If you are technically savvy and don't mind [submitting issues](https://github.com/yomidevs/yomitan/issues/new/choose) on GitHub, try the _testing_ version; otherwise, the _stable_ version will be your best bet. Check [contributing](#contributing) for more information on how to help.
- **Google Chrome**
@@ -63,59 +58,63 @@ Yomitan comes in two flavors: _stable_ and _testing_. New changes are initially
- [testing](https://chrome.google.com/webstore/detail/yomitan-development-build/glnaenfapkkecknnmginabpmgkenenml)
- **Mozilla Firefox**
+
- [stable](https://addons.mozilla.org/en-US/firefox/addon/yomitan/)
- - [testing](https://github.com/themoeway/yomitan/releases) ※
+ - [testing](https://github.com/yomidevs/yomitan/releases) ※
+
+- **Microsoft Edge**
+ - [stable](https://microsoftedge.microsoft.com/addons/detail/yomitan/idelnfbbmikgfiejhgmddlbkfgiifnnn)
+ - Testing: Coming soon
+
+※ Unlike Chrome, Firefox does not allow extensions meant for testing to be hosted in the marketplace. You will have to download the desired version and side-load it yourself. You only need to do this once, and you will get updates automatically.
+
+## Contributing
+
+🚀 **Dip your toes into contributing by looking at issues with the label [good first issue](https://github.com/yomidevs/yomitan/issues?q=is%3Aissue+is%3Aopen+label%3A%22gоοd+fіrst+іssսe%22).**
-※ 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.
+Since this is a distributed effort, we **highly welcome new contributors**! Feel free to browse the [issue tracker](https://github.com/yomidevs/yomitan/issues), and read our [contributing guidelines](./CONTRIBUTING.md).
-## Basic Usage
+Here are some ways anyone can help:
-1. Click the _Yomitan_ button in the browser bar to open the quick-actions popup.
+- Try using the Yomitan dev build. Not only do you get cutting edge features, but you can help uncover bugs and give feedback to developers early on.
+- Document any UI/UX friction in GitHub Issues. We're looking to make Yomitan more accessible to non-technical users.
+- All the issues in `area/bug` older than 2 months need help reproducing. If anything interests you, please try to reproduce it and report your results. We can't easily tell if these issues are one-off, have since been resolved, or are no longer relevant.
-
+> The current active maintainers of Yomitan spend a lot of their time debugging and triaging issues. When someone files a bug report, we need to assess the frequency and severity of the bug. It is extremely helpful if we get multiple reports of people who experience a bug or people who can contribute additional detail to an existing bug report.
- - The _cog_ button will open the Settings page.
- - The _magnifying glass_ button will open the Search page.
- - The _question mark_ button will open the Information page.
- - The _profile_ button will appear when multiple profiles exist, allowing the current profile to be quickly changed.
+If you're looking to code, please let us know what you plan on working on before submitting a Pull Request. This gives the core maintainers an opportunity to provide feedback early on before you dive too deep. You can do this by opening a GitHub Issue with the proposal.
-2. Import the dictionaries you wish to use for term and kanji searches, head over to the [the dictionary docs](./docs/dictionaries.md) to get set up! If you do not have any dictionaries installed or enabled, Yomitan will warn you that it is not ready for use by displaying an orange exclamation mark over its icon. This exclamation mark will disappear once you have installed and enabled at least one dictionary.
+Some contributions we always appreciate:
-
+- Well-written tests covering different functionalities. This includes [playwright tests](https://github.com/yomidevs/yomitan/tree/master/test/playwright), [benchmark tests](https://github.com/yomidevs/yomitan/tree/master/benches), and unit tests.
+- Increasing our type coverage.
+- More and better documentation!
-3. Webpage text can be scanned by moving the cursor while holding a modifier key, which is Shift by default. If definitions are found for the text at the cursor position, a popup window containing term definitions will open. This window can be dismissed by clicking anywhere outside of it.
+Information on how to setup and build the codebase can be found [here](./CONTRIBUTING.md#setup).
-
+If you want to add or improve support for a language, read the documentation on [language features](./docs/development/language-features.md).
-4. Click on the _speaker_ button to hear the term pronounced by a native speaker. If an audio sample is not available, you will hear a short click instead. You can configure the sources used to retrieve audio samples in the options page.
+Feel free to join us on the [Yomitan Discord](https://discord.gg/YkQrXW6TXF).
-5. Click on individual kanji in the term definition results to view additional information about those characters, including stroke order diagrams, readings, meanings, as well as other useful data.
+## Building Yomitan
-
+1. Install [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/).
-To further enhance your Yomitan experience, it's worth [integrating with Anki](./docs/anki-integration.md), a spaced-repetition flashcard program to help solidify the words you encounter.
+2. Run `npm ci` to set up the environment.
-## Licenses
+3. Run `npm run license-report:html` to generate any missing or changed license information.
-Required licensing notices for this project follow below:
+4. Run `npm run build` for a plain testing build or `npm run-script build -- --all --version {version}` for a release build (replacing `{version}` with a version number).
-- **EDRDG License** \
- This package uses the [EDICT](https://www.edrdg.org/jmdict/edict.html) and
- [KANJIDIC](https://www.edrdg.org/wiki/index.php/KANJIDIC_Project) dictionary files. These files are the property of
- the [Electronic Dictionary Research and Development Group](https://www.edrdg.org/), and are used in conformance with
- the Group's [license](https://www.edrdg.org/edrdg/licence.html).
+5. The builds for each browser and release branch can be found in the `builds` directory.
-- **Kanjium License** \
- The pitch accent notation, verb particle data, phonetics, homonyms and other additions or modifications to EDICT,
- KANJIDIC or KRADFILE were provided by Uros Ozvatic through his free database.
+For more information, see [Contributing](./CONTRIBUTING.md#setup).
## Third-Party Libraries
Yomitan uses several third-party libraries to function.
-
+
| Name | Installed version | License type | Link |
| :------------------ | :---------------- | :----------- | :----------------------------------------------- |
@@ -125,3 +124,4 @@ Yomitan uses several third-party libraries to function.
| yomitan-handlebars | 1.0.0 | MIT | n/a |
| parse5 | 7.1.2 | MIT | git://github.com/inikulin/parse5.git |
| wanakana | 5.3.1 | MIT | git+ssh://git@github.com/WaniKani/WanaKana.git |
+| hangul.js | 0.2.6 | MIT | git+https://github.com/e-/Hangul.js.git |
diff --git a/SECURITY.md b/SECURITY.md
index 2f42f91328..6c8c717233 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,4 +2,4 @@
## Reporting a Vulnerability
-Please report vulnerabilties using GitHub's built-in functionality: https://github.com/themoeway/yomitan/security/advisories/new
+Please report vulnerabilties using GitHub's built-in functionality: https://github.com/yomidevs/yomitan/security/advisories/new
diff --git a/benches/canary.bench.js b/benches/canary.bench.js
new file mode 100644
index 0000000000..25759ed67f
--- /dev/null
+++ b/benches/canary.bench.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 Yomitan 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 .
+ */
+
+import {bench, describe} from 'vitest';
+
+
+describe('Canary', () => {
+ bench('Loop 1 through 100,000', async () => {
+ let sum = 0;
+ for (let i = 0; i < 100_000; i++) {
+ sum += i;
+ }
+ console.debug(sum);
+ });
+});
diff --git a/benches/language-transformer.bench.js b/benches/japanese-language-transformer.bench.js
similarity index 71%
rename from benches/language-transformer.bench.js
rename to benches/japanese-language-transformer.bench.js
index 8212ae867f..d253920526 100644
--- a/benches/language-transformer.bench.js
+++ b/benches/japanese-language-transformer.bench.js
@@ -15,26 +15,20 @@
* along with this program. If not, see .
*/
-import fs from 'fs';
-import {fileURLToPath} from 'node:url';
-import path from 'path';
import {bench, describe} from 'vitest';
-import {parseJson} from '../dev/json.js';
+import {japaneseTransforms} from '../ext/js/language/ja/japanese-transforms.js';
import {LanguageTransformer} from '../ext/js/language/language-transformer.js';
-const dirname = path.dirname(fileURLToPath(import.meta.url));
-
-/** @type {import('language-transformer').LanguageTransformDescriptor} */
-const descriptor = parseJson(fs.readFileSync(path.join(dirname, '..', 'ext', 'data/language/japanese-transforms.json'), {encoding: 'utf8'}));
const languageTransformer = new LanguageTransformer();
-languageTransformer.addDescriptor(descriptor);
+languageTransformer.addDescriptor(japaneseTransforms);
-describe('language transformer', () => {
+describe('japanese language transformer', () => {
describe('basic tests', () => {
const adjectiveInflections = [
'愛しい',
'愛しそう',
'愛しすぎる',
+ '愛し過ぎる',
'愛しかったら',
'愛しかったり',
'愛しくて',
@@ -44,7 +38,7 @@ describe('language transformer', () => {
'愛しかった',
'愛しくありません',
'愛しくありませんでした',
- '愛しき'
+ '愛しき',
];
const verbInflections = [
@@ -56,6 +50,7 @@ describe('language transformer', () => {
'食べられる',
'食べられる',
'食べさせる',
+ '食べさす',
'食べさせられる',
'食べろ',
'食べない',
@@ -66,6 +61,7 @@ describe('language transformer', () => {
'食べられない',
'食べられない',
'食べさせない',
+ '食べささない',
'食べさせられない',
'食べ',
'食べれば',
@@ -74,6 +70,7 @@ describe('language transformer', () => {
'食べなさい',
'食べそう',
'食べすぎる',
+ '食べ過ぎる',
'食べたい',
'食べたら',
'食べたり',
@@ -81,20 +78,37 @@ describe('language transformer', () => {
'食べぬ',
'食べ',
'食べましょう',
+ '食べましょっか',
'食べよう',
+ '食べよっか',
+ '食べるまい',
+ '食べまい',
+ '食べておく',
'食べとく',
+ '食べないでおく',
+ '食べないどく',
'食べている',
'食べておる',
'食べてる',
'食べとる',
- '食べてしまう'
+ '食べてしまう',
+ '食べん',
+ '食べんかった',
+ '食べんばかり',
+ '食べんとする',
+ '食べますまい',
+ '食べましたら',
+ '食べますれば',
+ '食べませんかった',
];
const inflectionCombinations = [
'抱き抱えていなければ',
'抱きかかえていなければ',
'打ち込んでいませんでした',
- '食べさせられたくなかった'
+ '食べさせられたくなかった',
+ '食べんとしませんかった',
+ '食べないどきたくありません',
];
const kuruInflections = [
@@ -106,6 +120,7 @@ describe('language transformer', () => {
'こられる',
'こられる',
'こさせる',
+ 'こさす',
'こさせられる',
'こい',
'こない',
@@ -116,8 +131,8 @@ describe('language transformer', () => {
'こられない',
'こられない',
'こさせない',
+ 'こささない',
'こさせられない',
- 'くるな',
'きまして',
'くれば',
'きちゃう',
@@ -125,6 +140,7 @@ describe('language transformer', () => {
'きなさい',
'きそう',
'きすぎる',
+ 'き過ぎる',
'きたい',
'きたら',
'きたり',
@@ -134,13 +150,28 @@ describe('language transformer', () => {
'こねば',
'き',
'きましょう',
+ 'きましょっか',
'こよう',
+ 'こよっか',
+ 'くるまい',
+ 'こまい',
+ 'きておく',
'きとく',
+ 'こないでおく',
+ 'こないどく',
'きている',
'きておる',
'きてる',
'きとる',
- 'きてしまう'
+ 'きてしまう',
+ 'こん',
+ 'こんかった',
+ 'こんばかり',
+ 'こんとする',
+ 'きますまい',
+ 'きましたら',
+ 'きますれば',
+ 'きませんかった',
];
const suruInflections = [
@@ -154,6 +185,7 @@ describe('language transformer', () => {
'せられる',
'される',
'させる',
+ 'さす',
'せさせる',
'させられる',
'せさせられる',
@@ -167,9 +199,9 @@ describe('language transformer', () => {
'されない',
'させない',
'せさせない',
+ 'ささない',
'させられない',
'せさせられない',
- 'するな',
'しまして',
'すれば',
'しちゃう',
@@ -177,6 +209,7 @@ describe('language transformer', () => {
'しなさい',
'しそう',
'しすぎる',
+ 'し過ぎる',
'したい',
'したら',
'したり',
@@ -185,13 +218,28 @@ describe('language transformer', () => {
'せざる',
'せねば',
'しましょう',
+ 'しましょっか',
'しよう',
+ 'しよっか',
+ 'するまい',
+ 'しまい',
+ 'しておく',
'しとく',
+ 'しないでおく',
+ 'しないどく',
'している',
'しておる',
'してる',
'しとる',
- 'してしまう'
+ 'してしまう',
+ 'せん',
+ 'せんかった',
+ 'せんばかり',
+ 'せんとする',
+ 'しますまい',
+ 'しましたら',
+ 'しますれば',
+ 'しませんかった',
];
const kansaibenInflections = [
@@ -202,18 +250,19 @@ describe('language transformer', () => {
'買わへんかった',
'買うて',
'買うた',
- '買うたら'
+ '買うたら',
+ '買うたり',
];
const basicTransformations = [...adjectiveInflections, ...verbInflections, ...inflectionCombinations];
- bench(`transformations (n=${basicTransformations.length})`, () => {
+ bench(`japanese transformations (n=${basicTransformations.length})`, () => {
for (const transform of basicTransformations) {
languageTransformer.transform(transform);
}
});
const transformationsFull = [...basicTransformations, ...kuruInflections, ...suruInflections, ...kansaibenInflections];
- bench(`transformations-full (n=${transformationsFull.length})`, () => {
+ bench(`japanese transformations-full (n=${transformationsFull.length})`, () => {
for (const transform of transformationsFull) {
languageTransformer.transform(transform);
}
diff --git a/benches/jsconfig.json b/benches/jsconfig.json
index 8743868a01..9cf87cdd1a 100644
--- a/benches/jsconfig.json
+++ b/benches/jsconfig.json
@@ -9,7 +9,7 @@
"noImplicitAny": true,
"strictPropertyInitialization": true,
"suppressImplicitAnyIndexErrors": false,
- "skipLibCheck": false,
+ "skipLibCheck": true,
"baseUrl": ".",
"paths": {
"*": ["../types/ext/*"],
@@ -18,15 +18,23 @@
"test/*": ["../types/test/*"],
"rollup/parseAst": ["../types/other/rollup-parse-ast"],
"ext/json-schema": ["../types/ext/json-schema"],
- "json-schema": ["json-schema"]
+ "json-schema": ["json-schema"],
+ "chai": ["../node_modules/@vitest/expect/dist/chai.d.cts"]
},
"types": [
"chrome",
+ "dom-webcodecs",
"firefox-webext-browser",
"handlebars",
"jszip",
"parse5",
"wanakana"
+ ],
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable",
+ "WebWorker"
]
},
"include": [
@@ -40,6 +48,7 @@
],
"exclude": [
"../node_modules",
- "../dev/lib"
+ "../dev/lib",
+ "../ext/lib"
]
}
diff --git a/benches/spanish-language-transformer.bench.js b/benches/spanish-language-transformer.bench.js
new file mode 100644
index 0000000000..8dc60ceb09
--- /dev/null
+++ b/benches/spanish-language-transformer.bench.js
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023-2024 Yomitan 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 .
+ */
+
+import {bench, describe} from 'vitest';
+import {spanishTransforms} from '../ext/js/language/es/spanish-transforms.js';
+import {LanguageTransformer} from '../ext/js/language/language-transformer.js';
+
+const languageTransformer = new LanguageTransformer();
+languageTransformer.addDescriptor(spanishTransforms);
+
+describe('spanish language transformer', () => {
+ describe('basic tests', () => {
+ const nounInflections = [
+ 'gatos',
+ 'sofás',
+ 'tisús',
+ 'tisúes',
+ 'autobuses',
+ 'ciudades',
+ 'clics',
+ 'síes',
+ 'zigzags',
+ 'luces',
+ 'canciones',
+ ];
+
+ const verbPresentInflections = [
+ 'hablo',
+ 'hablas',
+ 'habla',
+ 'hablamos',
+ 'habláis',
+ 'hablan',
+ 'como',
+ 'comes',
+ 'come',
+ 'comemos',
+ 'coméis',
+ 'comen',
+ 'vivo',
+ 'vives',
+ 'vive',
+ 'vivimos',
+ 'vivís',
+ 'viven',
+ 'tengo',
+ 'tienes',
+ 'tiene',
+ 'tenemos',
+ 'tenéis',
+ 'tienen',
+ 'exijo',
+ 'extingo',
+ 'escojo',
+ 'quepo',
+ 'caigo',
+ 'conozco',
+ 'doy',
+ 'hago',
+ 'pongo',
+ 'sé',
+ 'salgo',
+ 'traduzco',
+ 'traigo',
+ 'valgo',
+ 'veo',
+ 'soy',
+ 'estoy',
+ 'voy',
+ 'he',
+ ];
+
+ const verbPreteriteInflections = [
+ 'hablé',
+ 'hablaste',
+ 'habló',
+ 'hablamos',
+ 'hablasteis',
+ 'hablaron',
+ 'comí',
+ 'comiste',
+ 'comió',
+ 'comimos',
+ 'comisteis',
+ 'comieron',
+ 'viví',
+ 'viviste',
+ 'vivió',
+ 'vivimos',
+ 'vivisteis',
+ 'vivieron',
+ 'tuve',
+ 'tuviste',
+ 'tuvo',
+ 'tuvimos',
+ 'tuvisteis',
+ 'tuvieron',
+ 'exigí',
+ 'extinguí',
+ 'escogí',
+ 'cupe',
+ 'caí',
+ 'conocí',
+ 'di',
+ 'hice',
+ 'puse',
+ 'supe',
+ 'salí',
+ 'traduje',
+ 'traje',
+ 'valí',
+ 'vi',
+ 'fui',
+ 'estuve',
+ 'fui',
+ 'hube',
+ ];
+
+ const basicTransformations = [...nounInflections, ...verbPresentInflections, ...verbPreteriteInflections];
+ bench(`spanish transformations (n=${basicTransformations.length})`, () => {
+ for (const transform of basicTransformations) {
+ languageTransformer.transform(transform);
+ }
+ });
+ });
+});
diff --git a/benches/translator.bench.js b/benches/translator.bench.js
index 1231c31c11..aac1b62cfb 100644
--- a/benches/translator.bench.js
+++ b/benches/translator.bench.js
@@ -20,12 +20,15 @@ import {fileURLToPath} from 'node:url';
import path from 'path';
import {bench, describe} from 'vitest';
import {parseJson} from '../dev/json.js';
-import {createFindKanjiOptions, createFindTermsOptions} from '../test/utilities/translator.js';
import {createTranslatorContext} from '../test/fixtures/translator-test.js';
+import {setupStubs} from '../test/utilities/database.js';
+import {createFindKanjiOptions, createFindTermsOptions} from '../test/utilities/translator.js';
+
+setupStubs();
const dirname = path.dirname(fileURLToPath(import.meta.url));
const dictionaryName = 'Test Dictionary 2';
-const translator = await createTranslatorContext(path.join(dirname, '..', 'test', 'data/dictionaries/valid-dictionary1'), dictionaryName);
+const {translator} = await createTranslatorContext(path.join(dirname, '..', 'test', 'data/dictionaries/valid-dictionary1'), dictionaryName);
describe('Translator', () => {
const testInputsFilePath = path.join(dirname, '..', 'test', 'data/translator-test-inputs.json');
@@ -33,10 +36,9 @@ describe('Translator', () => {
const {optionsPresets, tests} = parseJson(readFileSync(testInputsFilePath, {encoding: 'utf8'}));
const findKanjiTests = tests.filter((data) => data.options === 'kanji');
- const findTermTests = tests.filter((data) => data.options === 'default');
- const findTermWithTextTransformationsTests = tests.filter((data) => data.options !== 'kanji' && data.options !== 'default');
+ const findTermTests = tests.filter((data) => data.options !== 'kanji');
- bench(`Translator.prototype.findTerms - no text transformations (n=${findTermTests.length})`, async () => {
+ bench(`Translator.prototype.findTerms - (n=${findTermTests.length})`, async () => {
for (const data of /** @type {import('test/translator').TestInputFindTerm[]} */ (findTermTests)) {
const {mode, text} = data;
const options = createFindTermsOptions(dictionaryName, optionsPresets, data.options);
@@ -44,14 +46,6 @@ describe('Translator', () => {
}
});
- bench(`Translator.prototype.findTerms - text transformations (n=${findTermWithTextTransformationsTests.length})`, async () => {
- for (const data of /** @type {import('test/translator').TestInputFindTerm[]} */ (findTermWithTextTransformationsTests)) {
- const {mode, text} = data;
- const options = createFindTermsOptions(dictionaryName, optionsPresets, data.options);
- await translator.findTerms(mode, text, options);
- }
- });
-
bench(`Translator.prototype.findKanji - (n=${findKanjiTests.length})`, async () => {
for (const data of /** @type {import('test/translator').TestInputFindKanji[]} */ (findKanjiTests)) {
const {text} = data;
diff --git a/dev/bin/build-libs.js b/dev/bin/build-libs.js
index a614407eab..bd7c53ccd8 100644
--- a/dev/bin/build-libs.js
+++ b/dev/bin/build-libs.js
@@ -18,4 +18,4 @@
import {buildLibs} from '../build-libs.js';
-buildLibs();
+await buildLibs();
diff --git a/dev/bin/build.js b/dev/bin/build.js
index 190964d564..0ec7cc020d 100644
--- a/dev/bin/build.js
+++ b/dev/bin/build.js
@@ -23,10 +23,10 @@ import JSZip from 'jszip';
import {fileURLToPath} from 'node:url';
import path from 'path';
import readline from 'readline';
+import {parseArgs} from 'util';
import {buildLibs} from '../build-libs.js';
import {ManifestUtil} from '../manifest-util.js';
-import {getAllFiles, testMain} from '../util.js';
-import {parseArgs} from 'util';
+import {getAllFiles} from '../util.js';
const dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -55,11 +55,11 @@ async function createZip(directory, excludeFiles, outputFileName, sevenZipExes,
'a',
outputFileName,
'.',
- ...excludeArguments
+ ...excludeArguments,
],
{
- cwd: directory
- }
+ cwd: directory,
+ },
);
return;
} catch (e) {
@@ -85,7 +85,7 @@ async function createJSZip(directory, excludeFiles, outputFileName, onUpdate, dr
zip.file(
fileName.replace(/\\/g, '/'),
fs.readFileSync(path.join(directory, fileName), {encoding: null, flag: 'r'}),
- {}
+ {},
);
}
@@ -96,7 +96,7 @@ async function createJSZip(directory, excludeFiles, outputFileName, onUpdate, dr
const data = await zip.generateAsync({
type: 'nodebuffer',
compression: 'DEFLATE',
- compressionOptions: {level: 9}
+ compressionOptions: {level: 9},
}, onUpdate);
process.stdout.write('\n');
@@ -191,17 +191,21 @@ async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath,
fs.writeFileSync(manifestPath, ManifestUtil.createManifestString(modifiedManifest).replace('$YOMITAN_VERSION', yomitanVersion));
}
- if (!dryRun || dryRunBuildZip) {
- await createZip(extDir, excludeFiles, fullFileName, sevenZipExes, onUpdate, dryRun);
- }
+ if (fileName.endsWith('.zip')) {
+ if (!dryRun || dryRunBuildZip) {
+ await createZip(extDir, excludeFiles, fullFileName, sevenZipExes, onUpdate, dryRun);
+ }
- if (!dryRun) {
- if (Array.isArray(fileCopies)) {
+ if (!dryRun && Array.isArray(fileCopies)) {
for (const fileName2 of fileCopies) {
const fileName2Safe = path.basename(fileName2);
fs.copyFileSync(fullFileName, path.join(buildDir, fileName2Safe));
}
}
+ } else {
+ if (!dryRun) {
+ fs.cpSync(extDir, fullFileName, {recursive: true});
+ }
}
}
@@ -219,38 +223,40 @@ function ensureFilesExist(directory, files) {
}
}
-/**
- * @param {string[]} argv
- */
-export async function main(argv) {
+/** */
+export async function main() {
/** @type {import('util').ParseArgsConfig['options']} */
const parseArgsConfigOptions = {
all: {
type: 'boolean',
- default: false
+ default: false,
},
default: {
type: 'boolean',
- default: false
+ default: false,
},
manifest: {
- type: 'string'
+ type: 'string',
},
dryRun: {
type: 'boolean',
- default: false
+ default: false,
},
dryRunBuildZip: {
type: 'boolean',
- default: false
+ default: false,
},
version: {
type: 'string',
- default: '0.0.0.0'
- }
+ default: '0.0.0.0',
+ },
+ target: {
+ type: 'string',
+ },
};
- const {values: args} = parseArgs({args: argv, options: parseArgsConfigOptions});
+ const argv = process.argv.slice(2);
+ const {values: args, positionals: targets} = parseArgs({args: argv, options: parseArgsConfigOptions, allowPositionals: true});
const dryRun = /** @type {boolean} */ (args.dryRun);
const dryRunBuildZip = /** @type {boolean} */ (args.dryRunBuildZip);
@@ -266,8 +272,11 @@ export async function main(argv) {
try {
await buildLibs();
const variantNames = /** @type {string[]} */ ((
- argv.length === 0 || args.all ?
- manifestUtil.getVariants().filter(({buildable}) => buildable !== false).map(({name}) => name) : []
+ args.target ?
+ [args.target] :
+ (argv.length === 0 || args.all ?
+ manifestUtil.getVariants().filter(({buildable}) => buildable !== false).map(({name}) => name) :
+ targets)
));
await build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip, yomitanVersion);
} finally {
@@ -281,4 +290,4 @@ export async function main(argv) {
}
}
-testMain(main, process.argv.slice(2));
+await main();
diff --git a/dev/bin/dictionary-validate.js b/dev/bin/dictionary-validate.js
index e7c3562e09..47057c94a3 100644
--- a/dev/bin/dictionary-validate.js
+++ b/dev/bin/dictionary-validate.js
@@ -24,7 +24,7 @@ async function main() {
if (dictionaryFileNames.length === 0) {
console.log([
'Usage:',
- ' node dictionary-validate [--ajv] ...'
+ ' node dictionary-validate [--ajv] ...',
].join('\n'));
return;
}
@@ -39,4 +39,4 @@ async function main() {
await testDictionaryFiles(mode, dictionaryFileNames);
}
-main();
+await main();
diff --git a/dev/bin/schema-validate.js b/dev/bin/schema-validate.js
index 74a42444bd..dc40e55f4c 100644
--- a/dev/bin/schema-validate.js
+++ b/dev/bin/schema-validate.js
@@ -27,7 +27,7 @@ function main() {
if (args.length < 2) {
console.log([
'Usage:',
- ' node schema-validate [--ajv] ...'
+ ' node schema-validate [--ajv] ...',
].join('\n'));
return;
}
@@ -43,15 +43,18 @@ function main() {
const schema = parseJson(schemaSource);
for (const dataFileName of args.slice(1)) {
+ // eslint-disable-next-line no-restricted-syntax
const start = performance.now();
try {
console.log(`Validating ${dataFileName}...`);
const dataSource = fs.readFileSync(dataFileName, {encoding: 'utf8'});
const data = parseJson(dataSource);
createJsonSchema(mode, schema).validate(data);
+ // eslint-disable-next-line no-restricted-syntax
const end = performance.now();
console.log(`No issues detected (${((end - start) / 1000).toFixed(2)}s)`);
} catch (e) {
+ // eslint-disable-next-line no-restricted-syntax
const end = performance.now();
console.log(`Encountered an error (${((end - start) / 1000).toFixed(2)}s)`);
console.warn(e);
diff --git a/dev/build-libs.js b/dev/build-libs.js
index 15ab3c8d9d..6bafc5e356 100644
--- a/dev/build-libs.js
+++ b/dev/build-libs.js
@@ -20,13 +20,27 @@ import Ajv from 'ajv';
import standaloneCode from 'ajv/dist/standalone/index.js';
import esbuild from 'esbuild';
import fs from 'fs';
+import {createRequire} from 'module';
import path from 'path';
import {fileURLToPath} from 'url';
import {parseJson} from './json.js';
+const require = createRequire(import.meta.url);
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
const extDir = path.join(dirname, '..', 'ext');
+/**
+ * @param {string} out
+ */
+async function copyWasm(out) {
+ // copy from node modules '@resvg/resvg-wasm/index_bg.wasm' to out
+ const resvgWasmPath = path.dirname(require.resolve('@resvg/resvg-wasm'));
+ const wasmPath = path.join(resvgWasmPath, 'index_bg.wasm');
+ fs.copyFileSync(wasmPath, path.join(out, 'resvg.wasm'));
+}
+
+
/**
* @param {string} scriptPath
*/
@@ -41,8 +55,8 @@ async function buildLib(scriptPath) {
outfile: path.join(extDir, 'lib', path.basename(scriptPath)),
external: ['fs'],
banner: {
- js: '// @ts-nocheck'
- }
+ js: '// @ts-nocheck',
+ },
});
}
@@ -52,7 +66,7 @@ async function buildLib(scriptPath) {
export async function buildLibs() {
const devLibPath = path.join(dirname, 'lib');
const files = await fs.promises.readdir(devLibPath, {
- withFileTypes: true
+ withFileTypes: true,
});
for (const f of files) {
if (f.isFile()) {
@@ -64,13 +78,14 @@ export async function buildLibs() {
const schemaFileNames = fs.readdirSync(schemaDir);
const schemas = schemaFileNames.map((schemaFileName) => {
/** @type {import('ajv').AnySchema} */
+ // eslint-disable-next-line sonarjs/prefer-immediate-return
const result = parseJson(fs.readFileSync(path.join(schemaDir, schemaFileName), {encoding: 'utf8'}));
return result;
});
const ajv = new Ajv({
schemas,
code: {source: true, esm: true},
- allowUnionTypes: true
+ allowUnionTypes: true,
});
const moduleCode = standaloneCode(ajv);
@@ -78,4 +93,6 @@ export async function buildLibs() {
const patchedModuleCode = "// @ts-nocheck\nimport {ucs2length} from './ucs2length.js';" + moduleCode.replaceAll('require("ajv/dist/runtime/ucs2length").default', 'ucs2length');
fs.writeFileSync(path.join(extDir, 'lib/validate-schemas.js'), patchedModuleCode);
+
+ await copyWasm(path.join(extDir, 'lib'));
}
diff --git a/dev/data-error.js b/dev/data-error.js
index 5659245b81..eb7f71bc70 100644
--- a/dev/data-error.js
+++ b/dev/data-error.js
@@ -18,12 +18,14 @@
/**
* Schema validation error type.
*/
-class DataError extends Error {
+export class DataError extends Error {
/**
* @param {string} message
*/
constructor(message) {
super(message);
+ /** @type {string} */
+ this.name = 'DataError';
/** @type {unknown} */
this._data = void 0;
}
@@ -32,7 +34,3 @@ class DataError extends Error {
get data() { return this._data; }
set data(value) { this._data = value; }
}
-
-module.exports = {
- DataError
-};
diff --git a/dev/data/legal-npm.css b/dev/data/legal-npm.css
index d607460ff8..dde8d5d2b4 100644
--- a/dev/data/legal-npm.css
+++ b/dev/data/legal-npm.css
@@ -11,6 +11,47 @@ td {
padding: 3px;
}
-th {
- background-color: #ffffff;
+/* Light mode */
+@media (prefers-color-scheme: light) {
+ body {
+ background-color: #f8f9fa;
+ color: #000000;
+ }
+ th {
+ background-color: #ffffff;
+ }
+}
+
+/* Dark mode */
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #0a0a0a;
+ color: #ffffff;
+ }
+ th {
+ background-color: #1e1e1e;
+ }
+
+ /* Scrollbars */
+ :root {
+ scrollbar-color: #444444 #2f2f2f;
+ }
+ :root::-webkit-scrollbar {
+ width: auto;
+ }
+ :root::-webkit-scrollbar-button {
+ height: 0;
+ }
+ :root::-webkit-scrollbar-thumb {
+ background-color: #444444;
+ }
+ :root::-webkit-scrollbar-track {
+ background-color: #444444;
+ }
+ :root::-webkit-scrollbar-track-piece {
+ background-color: #2f2f2f;
+ }
+ :root::-webkit-scrollbar-corner {
+ background-color: #2f2f2f;
+ }
}
diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json
index 93841f4661..0dd2a22f6b 100644
--- a/dev/data/manifest-variants.json
+++ b/dev/data/manifest-variants.json
@@ -1,9 +1,9 @@
{
"manifest": {
"manifest_version": 3,
- "name": "Yomitan",
+ "name": "Yomitan Popup Dictionary",
"version": "$YOMITAN_VERSION",
- "description": "Japanese dictionary with Anki integration",
+ "description": "Popup dictionary for language learning",
"author": {
"email": "themoeway@googlegroups.com"
},
@@ -64,7 +64,8 @@
"unlimitedStorage",
"declarativeNetRequest",
"scripting",
- "offscreen"
+ "offscreen",
+ "contextMenus"
],
"optional_permissions": [
"clipboardRead",
@@ -101,7 +102,8 @@
"resources": [
"popup.html",
"template-renderer.html",
- "js/*"
+ "js/*",
+ "lib/resvg.wasm"
],
"matches": [
""
@@ -109,7 +111,7 @@
}
],
"content_security_policy": {
- "extension_pages": "default-src 'self'; img-src blob: 'self'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *",
+ "extension_pages": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; img-src blob: 'self'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *",
"sandbox": "sandbox allow-scripts; default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'unsafe-inline'"
}
},
@@ -185,7 +187,7 @@
"path": [
"author"
],
- "value": "TheMoeWay"
+ "value": "Yomidevs"
},
{
"action": "delete",
@@ -237,7 +239,7 @@
"content_security_policy",
"extension_pages"
],
- "value": "default-src 'self'; script-src 'self'; img-src blob: 'self'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *"
+ "value": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; img-src blob: 'self'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *"
},
{
"action": "set",
@@ -308,7 +310,7 @@
"gecko",
"update_url"
],
- "value": "https://raw.githubusercontent.com/themoeway/yomitan/metadata/updates.json"
+ "value": "https://raw.githubusercontent.com/yomidevs/yomitan/metadata/updates.json"
}
],
"excludeFiles": [
@@ -318,6 +320,11 @@
"js/background/offscreen-main.js"
]
},
+ {
+ "name": "firefox-android",
+ "inherit": "firefox",
+ "fileName": "yomitan-firefox-android"
+ },
{
"name": "safari",
"modifications": [
@@ -326,7 +333,7 @@
"path": [
"author"
],
- "value": "TheMoeWay"
+ "value": "Yomidevs"
},
{
"action": "remove",
@@ -370,6 +377,14 @@
"js/background/offscreen.js",
"js/background/offscreen-main.js"
]
+ },
+ {
+ "name": "edge",
+ "inherit": "base",
+ "fileName": "yomitan-edge.zip",
+ "excludeFiles": [
+ "background.html"
+ ]
}
]
}
diff --git a/dev/data/structured-content-overrides.css b/dev/data/structured-content-overrides.css
index 46859b1bfe..38d8836173 100644
--- a/dev/data/structured-content-overrides.css
+++ b/dev/data/structured-content-overrides.css
@@ -28,23 +28,6 @@
.gloss-image-link:hover {
/* remove-rule */
}
-.gloss-image-container-overlay {
- font-size: initial;
- line-height: initial;
- color: initial;
-}
-.gloss-image-background {
- background-color: currentColor;
-}
-:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image,
-:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background,
-:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image,
-:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background {
- /* remove-rule */
-}
-.gloss-image-link-text {
- line-height: initial;
-}
.gloss-sc-thead,
.gloss-sc-tfoot,
.gloss-sc-th {
@@ -75,3 +58,19 @@
:root[data-glossary-layout-mode=compact] .gloss-sc-ul[data-sc-content=glossary] .gloss-sc-li:not(:first-child)::before {
/* remove-rule */
}
+.gloss-sc-details {
+ /* remove-rule */
+}
+.gloss-sc-summary {
+ /* remove-rule */
+}
+.gloss-image-background {
+ background-color: currentColor;
+ /* remove-property display */
+}
+.gloss-image-link:not([data-appearance=monochrome]) .gloss-image-background {
+ display: none;
+}
+.gloss-image-link[data-appearance=monochrome] .gloss-image {
+ opacity: 0;
+}
diff --git a/dev/dictionary-archive-util.js b/dev/dictionary-archive-util.js
new file mode 100644
index 0000000000..d8dc7290e2
--- /dev/null
+++ b/dev/dictionary-archive-util.js
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 Yomitan 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 .
+ */
+
+import {BlobWriter, TextReader, TextWriter, Uint8ArrayReader, ZipReader, ZipWriter} from '@zip.js/zip.js';
+import {readFileSync, readdirSync} from 'fs';
+import {join} from 'path';
+import {parseJson} from './json.js';
+
+/**
+ * Creates a zip archive from the given dictionary directory.
+ * @param {string} dictionaryDirectory
+ * @param {string} [dictionaryName]
+ * @returns {Promise}
+ */
+export async function createDictionaryArchiveData(dictionaryDirectory, dictionaryName) {
+ const fileNames = readdirSync(dictionaryDirectory);
+ const zipFileWriter = new BlobWriter();
+ // Level 0 compression used since decompression in the node environment is not supported.
+ // See dev/lib/zip.js for more details.
+ const zipWriter = new ZipWriter(zipFileWriter, {
+ level: 0,
+ });
+ for (const fileName of fileNames) {
+ if (/\.json$/.test(fileName)) {
+ const content = readFileSync(join(dictionaryDirectory, fileName), {encoding: 'utf8'});
+ /** @type {import('dictionary-data').Index} */
+ const json = parseJson(content);
+ if (fileName === 'index.json' && typeof dictionaryName === 'string') {
+ json.title = dictionaryName;
+ }
+ await zipWriter.add(fileName, new TextReader(JSON.stringify(json, null, 0)));
+ } else {
+ const content = readFileSync(join(dictionaryDirectory, fileName), {encoding: null});
+ await zipWriter.add(fileName, new Blob([content]).stream());
+ }
+ }
+ const blob = await zipWriter.close();
+ return await blob.arrayBuffer();
+}
+
+/**
+ * @param {import('@zip.js/zip.js').Entry} entry
+ * @returns {Promise}
+ */
+export async function readArchiveEntryDataString(entry) {
+ if (typeof entry.getData === 'undefined') { throw new Error('Cannot get index data'); }
+ return await entry.getData(new TextWriter());
+}
+
+/**
+ * @template [T=unknown]
+ * @param {import('@zip.js/zip.js').Entry} entry
+ * @returns {Promise}
+ */
+export async function readArchiveEntryDataJson(entry) {
+ const indexContent = await readArchiveEntryDataString(entry);
+ return parseJson(indexContent);
+}
+
+/**
+ * @param {ArrayBuffer} data
+ * @returns {Promise}
+ */
+export async function getDictionaryArchiveEntries(data) {
+ const zipFileReader = new Uint8ArrayReader(new Uint8Array(data));
+ const zipReader = new ZipReader(zipFileReader);
+ return await zipReader.getEntries();
+}
+
+/**
+ * @template T
+ * @param {import('@zip.js/zip.js').Entry[]} entries
+ * @param {string} fileName
+ * @returns {Promise}
+ */
+export async function getDictionaryArchiveJson(entries, fileName) {
+ const entry = entries.find((item) => item.filename === fileName);
+ if (typeof entry === 'undefined') { throw new Error(`File not found: ${fileName}`); }
+ return await readArchiveEntryDataJson(entry);
+}
+
+/**
+ * @returns {string}
+ */
+export function getIndexFileName() {
+ return 'index.json';
+}
+
+/**
+ * @param {ArrayBuffer} data
+ * @returns {Promise}
+ */
+export async function getDictionaryArchiveIndex(data) {
+ const entries = await getDictionaryArchiveEntries(data);
+ return await getDictionaryArchiveJson(entries, getIndexFileName());
+}
diff --git a/dev/dictionary-validate.js b/dev/dictionary-validate.js
index 18bba99ee1..77fa0fca31 100644
--- a/dev/dictionary-validate.js
+++ b/dev/dictionary-validate.js
@@ -17,10 +17,10 @@
*/
import fs from 'fs';
-import JSZip from 'jszip';
import path from 'path';
import {performance} from 'perf_hooks';
import {fileURLToPath} from 'url';
+import {getDictionaryArchiveEntries, getDictionaryArchiveJson, getIndexFileName, readArchiveEntryDataJson} from './dictionary-archive-util.js';
import {parseJson} from './json.js';
import {createJsonSchema} from './schema-validate.js';
import {toError} from './to-error.js';
@@ -39,30 +39,31 @@ function readSchema(relativeFileName) {
/**
* @param {import('dev/schema-validate').ValidateMode} mode
- * @param {import('jszip')} zip
+ * @param {import('@zip.js/zip.js').Entry[]} entries
* @param {import('dev/dictionary-validate').SchemasDetails} schemasDetails
*/
-async function validateDictionaryBanks(mode, zip, schemasDetails) {
- for (const [fileName, file] of Object.entries(zip.files)) {
+async function validateDictionaryBanks(mode, entries, schemasDetails) {
+ for (const entry of entries) {
+ const {filename} = entry;
for (const [fileNameFormat, schema] of schemasDetails) {
- if (!fileNameFormat.test(fileName)) { continue; }
+ if (!fileNameFormat.test(filename)) { continue; }
let jsonSchema;
try {
jsonSchema = createJsonSchema(mode, schema);
} catch (e) {
const e2 = toError(e);
- e2.message += `\n(in file ${fileName})}`;
+ e2.message += `\n(in file ${filename})`;
throw e2;
}
- const data = parseJson(await file.async('string'));
+ const data = await readArchiveEntryDataJson(entry);
try {
jsonSchema.validate(data);
} catch (e) {
const e2 = toError(e);
- e2.message += `\n(in file ${fileName})}`;
+ e2.message += `\n(in file ${filename})`;
throw e2;
}
break;
@@ -73,18 +74,14 @@ async function validateDictionaryBanks(mode, zip, schemasDetails) {
/**
* Validates a dictionary from its zip archive.
* @param {import('dev/schema-validate').ValidateMode} mode
- * @param {import('jszip')} archive
+ * @param {ArrayBuffer} archiveData
* @param {import('dev/dictionary-validate').Schemas} schemas
*/
-export async function validateDictionary(mode, archive, schemas) {
- const indexFileName = 'index.json';
- const indexFile = archive.files[indexFileName];
- if (!indexFile) {
- throw new Error('No dictionary index found in archive');
- }
-
+export async function validateDictionary(mode, archiveData, schemas) {
+ const entries = await getDictionaryArchiveEntries(archiveData);
+ const indexFileName = getIndexFileName();
/** @type {import('dictionary-data').Index} */
- const index = parseJson(await indexFile.async('string'));
+ const index = await getDictionaryArchiveJson(entries, indexFileName);
const version = index.format || index.version;
try {
@@ -92,7 +89,7 @@ export async function validateDictionary(mode, archive, schemas) {
jsonSchema.validate(index);
} catch (e) {
const e2 = toError(e);
- e2.message += `\n(in file ${indexFileName})}`;
+ e2.message += `\n(in file ${indexFileName})`;
throw e2;
}
@@ -102,10 +99,10 @@ export async function validateDictionary(mode, archive, schemas) {
[/^term_meta_bank_(\d+)\.json$/, schemas.termMetaBankV3],
[/^kanji_bank_(\d+)\.json$/, version === 1 ? schemas.kanjiBankV1 : schemas.kanjiBankV3],
[/^kanji_meta_bank_(\d+)\.json$/, schemas.kanjiMetaBankV3],
- [/^tag_bank_(\d+)\.json$/, schemas.tagBankV3]
+ [/^tag_bank_(\d+)\.json$/, schemas.tagBankV3],
];
- await validateDictionaryBanks(mode, archive, schemasDetails);
+ await validateDictionaryBanks(mode, entries, schemasDetails);
}
/**
@@ -121,7 +118,7 @@ export function getSchemas() {
tagBankV3: readSchema('../ext/data/schemas/dictionary-tag-bank-v3-schema.json'),
termBankV1: readSchema('../ext/data/schemas/dictionary-term-bank-v1-schema.json'),
termBankV3: readSchema('../ext/data/schemas/dictionary-term-bank-v3-schema.json'),
- termMetaBankV3: readSchema('../ext/data/schemas/dictionary-term-meta-bank-v3-schema.json')
+ termMetaBankV3: readSchema('../ext/data/schemas/dictionary-term-meta-bank-v3-schema.json'),
};
}
@@ -134,15 +131,17 @@ export async function testDictionaryFiles(mode, dictionaryFileNames) {
const schemas = getSchemas();
for (const dictionaryFileName of dictionaryFileNames) {
+ // eslint-disable-next-line no-restricted-syntax
const start = performance.now();
try {
console.log(`Validating ${dictionaryFileName}...`);
const source = fs.readFileSync(dictionaryFileName);
- const archive = await JSZip.loadAsync(source);
- await validateDictionary(mode, archive, schemas);
+ await validateDictionary(mode, source.buffer, schemas);
+ // eslint-disable-next-line no-restricted-syntax
const end = performance.now();
console.log(`No issues detected (${((end - start) / 1000).toFixed(2)}s)`);
} catch (e) {
+ // eslint-disable-next-line no-restricted-syntax
const end = performance.now();
console.log(`Encountered an error (${((end - start) / 1000).toFixed(2)}s)`);
console.warn(e);
diff --git a/dev/generate-css-json.js b/dev/generate-css-json.js
index b196e41125..79baf6f54e 100644
--- a/dev/generate-css-json.js
+++ b/dev/generate-css-json.js
@@ -31,13 +31,13 @@ export function getTargets() {
{
cssFilePath: path.join(dirname, '..', 'ext/css/structured-content.css'),
overridesCssFilePath: path.join(dirname, 'data/structured-content-overrides.css'),
- outputPath: path.join(dirname, '..', 'ext/data/structured-content-style.json')
+ outputPath: path.join(dirname, '..', 'ext/data/structured-content-style.json'),
},
{
cssFilePath: path.join(dirname, '..', 'ext/css/display-pronunciation.css'),
overridesCssFilePath: path.join(dirname, 'data/display-pronunciation-overrides.css'),
- outputPath: path.join(dirname, '..', 'ext/data/pronunciation-style.json')
- }
+ outputPath: path.join(dirname, '..', 'ext/data/pronunciation-style.json'),
+ },
];
}
@@ -91,6 +91,7 @@ function removeProperty(styles, property, removedProperties) {
* @returns {string}
*/
export function formatRulesJson(rules) {
+ // This is similar to the following code, but formatted a but more succinctly:
// return JSON.stringify(rules, null, 4);
const indent1 = ' ';
const indent2 = indent1.repeat(2);
@@ -101,11 +102,11 @@ export function formatRulesJson(rules) {
for (const {selectors, styles} of rules) {
if (ruleIndex > 0) { result += ','; }
result += `\n${indent1}{\n${indent2}"selectors": `;
- if (selectors.length === 1) {
- result += `[${JSON.stringify(selectors[0], null, 4)}]`;
- } else {
- result += JSON.stringify(selectors, null, 4).replace(/\n/g, '\n' + indent2);
- }
+ result += (
+ selectors.length === 1 ?
+ `[${JSON.stringify(selectors[0], null, 4)}]` :
+ JSON.stringify(selectors, null, 4).replace(/\n/g, '\n' + indent2)
+ );
result += `,\n${indent2}"styles": [`;
let styleIndex = 0;
for (const [key, value] of styles) {
@@ -151,7 +152,10 @@ export function generateRules(cssFilePath, overridesCssFilePath) {
const styles = [];
if (typeof declarations !== 'undefined') {
for (const declaration of declarations) {
- if (declaration.type !== 'declaration') { console.log(declaration); continue; }
+ if (declaration.type !== 'declaration') {
+ console.log(declaration);
+ continue;
+ }
const {property, value} = /** @type {css.Declaration} */ (declaration);
if (typeof property !== 'string' || typeof value !== 'string') { continue; }
styles.push([property, value]);
diff --git a/dev/jsconfig.json b/dev/jsconfig.json
index d94651083b..c3b7e3d2f8 100644
--- a/dev/jsconfig.json
+++ b/dev/jsconfig.json
@@ -9,37 +9,105 @@
"noImplicitAny": true,
"strictPropertyInitialization": true,
"suppressImplicitAnyIndexErrors": false,
- "skipLibCheck": false,
+ "skipLibCheck": true,
"baseUrl": ".",
"paths": {
- "anki-templates": ["../types/ext/anki-templates"],
- "anki-templates-internal": ["../types/ext/anki-templates-internal"],
- "cache-map": ["../types/ext/cache-map"],
- "core": ["../types/ext/core"],
- "css-style-applier": ["../types/ext/css-style-applier"],
- "database": ["../types/ext/database"],
- "dictionary": ["../types/ext/dictionary"],
- "dictionary-data": ["../types/ext/dictionary-data"],
- "dictionary-data-util": ["../types/ext/dictionary-data-util"],
- "dictionary-database": ["../types/ext/dictionary-database"],
- "dictionary-importer": ["../types/ext/dictionary-importer"],
- "dictionary-importer-media-loader": ["../types/ext/dictionary-importer-media-loader"],
- "dynamic-property": ["../types/ext/dynamic-property"],
- "error": ["../types/ext/error"],
- "event-listener-collection": ["../types/ext/event-listener-collection"],
- "japanese-util": ["../types/ext/japanese-util"],
- "ext/json-schema": ["../types/ext/json-schema"],
- "language-transformer": ["../types/ext/language-transformer"],
- "language-transformer-internal": ["../types/ext/language-transformer-internal"],
- "log": ["../types/ext/log"],
- "settings": ["../types/ext/settings"],
- "structured-content": ["../types/ext/structured-content"],
- "translator": ["../types/ext/translator"],
- "translation": ["../types/ext/translation"],
- "translation-internal": ["../types/ext/translation-internal"],
- "dev/*": ["../types/dev/*"],
- "rollup/parseAst": ["../types/other/rollup-parse-ast"],
- "chai": ["../node_modules/@vitest/expect/dist/chai.d.cts"]
+ "api-map": [
+ "../types/ext/api-map"
+ ],
+ "anki-templates": [
+ "../types/ext/anki-templates"
+ ],
+ "anki-templates-internal": [
+ "../types/ext/anki-templates-internal"
+ ],
+ "cache-map": [
+ "../types/ext/cache-map"
+ ],
+ "core": [
+ "../types/ext/core"
+ ],
+ "css-style-applier": [
+ "../types/ext/css-style-applier"
+ ],
+ "database": [
+ "../types/ext/database"
+ ],
+ "dictionary": [
+ "../types/ext/dictionary"
+ ],
+ "dictionary-data": [
+ "../types/ext/dictionary-data"
+ ],
+ "dictionary-data-util": [
+ "../types/ext/dictionary-data-util"
+ ],
+ "dictionary-database": [
+ "../types/ext/dictionary-database"
+ ],
+ "dictionary-importer": [
+ "../types/ext/dictionary-importer"
+ ],
+ "dictionary-importer-media-loader": [
+ "../types/ext/dictionary-importer-media-loader"
+ ],
+ "dynamic-property": [
+ "../types/ext/dynamic-property"
+ ],
+ "error": [
+ "../types/ext/error"
+ ],
+ "event-listener-collection": [
+ "../types/ext/event-listener-collection"
+ ],
+ "japanese-util": [
+ "../types/ext/japanese-util"
+ ],
+ "language": [
+ "../types/ext/language"
+ ],
+ "language-descriptors": [
+ "../types/ext/language-descriptors"
+ ],
+ "CJK-util": [
+ "../types/ext/CJK-util"
+ ],
+ "ext/json-schema": [
+ "../types/ext/json-schema"
+ ],
+ "language-transformer": [
+ "../types/ext/language-transformer"
+ ],
+ "language-transformer-internal": [
+ "../types/ext/language-transformer-internal"
+ ],
+ "log": [
+ "../types/ext/log"
+ ],
+ "settings": [
+ "../types/ext/settings"
+ ],
+ "structured-content": [
+ "../types/ext/structured-content"
+ ],
+ "translator": [
+ "../types/ext/translator"
+ ],
+ "translation": [
+ "../types/ext/translation"
+ ],
+ "translation-internal": [
+ "../types/ext/translation-internal"
+ ],
+ "dev/*": [
+ "../types/dev/*"
+ ],
+ "rollup/parseAst": [
+ "../types/other/rollup-parse-ast"
+ ],
+ "chai": [
+ "../node_modules/@vitest/expect/dist/chai.d.cts"
+ ]
},
"types": [
"node",
@@ -49,7 +117,14 @@
"assert",
"css",
"chrome",
- "ajv"
+ "ajv",
+ "dom-webcodecs"
+ ],
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable",
+ "WebWorker"
]
},
"include": [
@@ -61,7 +136,7 @@
"../ext/js/data/database.js",
"../ext/js/data/json-schema.js",
"../ext/js/general/cache-map.js",
- "../ext/js/data/sandbox/anki-note-data-creator.js",
+ "../ext/js/data/anki-note-data-creator.js",
"../ext/js/general/cache-map.js",
"../ext/js/general/regex-util.js",
"../ext/js/general/text-source-map.js",
@@ -69,7 +144,7 @@
"../ext/js/dictionary/dictionary-importer.js",
"../ext/js/dictionary/dictionary-database.js",
"../ext/js/dictionary/dictionary-data-util.js",
- "../ext/js/language/sandbox/japanese-util.js",
+ "../ext/js/language/japanese-util.js",
"../ext/js/language/translator.js",
"../ext/js/media/media-util.js",
"../types/dev/**/*.ts",
diff --git a/dev/lib/dexie.js b/dev/lib/dexie.js
index 834260e001..83015e25e1 100644
--- a/dev/lib/dexie.js
+++ b/dev/lib/dexie.js
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-import Dexie from 'dexie';
-import 'dexie-export-import';
-export {Dexie};
+export {default as Dexie} from 'dexie';
+
+import 'dexie-export-import';
diff --git a/dev/lib/hangul-js.js b/dev/lib/hangul-js.js
new file mode 100644
index 0000000000..d1f29481b2
--- /dev/null
+++ b/dev/lib/hangul-js.js
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2024 Yomitan 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 .
+ */
+
+export * as Hangul from 'hangul-js';
diff --git a/dev/lib/parse5.js b/dev/lib/parse5.js
index 03249db475..ce92730dbd 100644
--- a/dev/lib/parse5.js
+++ b/dev/lib/parse5.js
@@ -14,4 +14,5 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
export * from 'parse5';
diff --git a/dev/lib/resvg-wasm.js b/dev/lib/resvg-wasm.js
new file mode 100644
index 0000000000..6c969bafd6
--- /dev/null
+++ b/dev/lib/resvg-wasm.js
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2023-2024 Yomitan 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 .
+ */
+
+export * from '@resvg/resvg-wasm';
diff --git a/dev/lib/ucs2length.js b/dev/lib/ucs2length.js
index ce9027de52..7ca3c54510 100644
--- a/dev/lib/ucs2length.js
+++ b/dev/lib/ucs2length.js
@@ -14,7 +14,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
import ucs2length from 'ajv/dist/runtime/ucs2length.js';
+
const ucs2length2 = ucs2length.default;
-export {ucs2length2 as ucs2length};
+export {ucs2length2 as ucs2length};
diff --git a/dev/lib/wanakana.js b/dev/lib/wanakana.js
index b2679cec2a..24cf63ab51 100644
--- a/dev/lib/wanakana.js
+++ b/dev/lib/wanakana.js
@@ -14,4 +14,5 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
export * from 'wanakana';
diff --git a/dev/lib/z-worker.js b/dev/lib/z-worker.js
index 142ed8fc31..4e5fe3183e 100644
--- a/dev/lib/z-worker.js
+++ b/dev/lib/z-worker.js
@@ -14,4 +14,5 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
import '../../node_modules/@zip.js/zip.js/lib/z-worker.js';
diff --git a/dev/lib/zip.js b/dev/lib/zip.js
index 007b4285e0..ee603c25ff 100644
--- a/dev/lib/zip.js
+++ b/dev/lib/zip.js
@@ -14,4 +14,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
+/**
+ * This script is importing a file within the '@zip.js/zip.js' dependency rather than
+ * simply importing '@zip.js/zip.js'.
+ *
+ * This is done in order to only import the subset of functionality that the extension needs.
+ *
+ * In this case, this subset only includes the components to support decompression using web workers.
+ *
+ * Therefore, if this file or the built library file is imported in a development, testing, or
+ * benchmark script, it will not be able to properly decompress the data of compressed zip files.
+ *
+ * As a workaround, testing zip data can be generated using {level: 0} compression.
+ */
+
export * from '@zip.js/zip.js/lib/zip.js';
diff --git a/dev/manifest-util.js b/dev/manifest-util.js
index 6ab817909c..97f7b598c6 100644
--- a/dev/manifest-util.js
+++ b/dev/manifest-util.js
@@ -24,15 +24,6 @@ import {parseJson} from './json.js';
const dirname = path.dirname(fileURLToPath(import.meta.url));
-/**
- * @template [T=unknown]
- * @param {T} value
- * @returns {T}
- */
-function clone(value) {
- return parseJson(JSON.stringify(value));
-}
-
export class ManifestUtil {
constructor() {
@@ -70,7 +61,7 @@ export class ManifestUtil {
}
}
- return clone(this._manifest);
+ return structuredClone(this._manifest);
}
/**
@@ -108,7 +99,7 @@ export class ManifestUtil {
const {stdout, stderr, status} = childProcess.spawnSync(command, args, {
cwd: dirname,
stdio: 'pipe',
- shell: false
+ shell: false,
});
if (status !== 0) {
const message = stderr.toString('utf8').trim();
@@ -189,7 +180,7 @@ export class ManifestUtil {
const {start, deleteCount, items} = modification;
/** @type {unknown[]} */
const value = this._getObjectProperties(manifest, path2, path2.length);
- const itemsNew = items.map((v) => clone(v));
+ const itemsNew = items.map((v) => structuredClone(v));
value.splice(start, deleteCount, ...itemsNew);
}
break;
@@ -229,7 +220,7 @@ export class ManifestUtil {
const {items} = modification;
/** @type {unknown[]} */
const value = this._getObjectProperties(manifest, path2, path2.length);
- const itemsNew = items.map((v) => clone(v));
+ const itemsNew = items.map((v) => structuredClone(v));
value.push(...itemsNew);
}
break;
@@ -335,11 +326,10 @@ export class ManifestUtil {
* @returns {import('dev/manifest').Manifest}
*/
_createVariantManifest(manifest, variant) {
- let modifiedManifest = clone(manifest);
+ let modifiedManifest = structuredClone(manifest);
for (const {modifications} of this._getInheritanceChain(variant)) {
modifiedManifest = this._applyModifications(modifiedManifest, modifications);
}
return modifiedManifest;
}
}
-
diff --git a/dev/schema-validate.js b/dev/schema-validate.js
index d1ffcf8202..b25a614e95 100644
--- a/dev/schema-validate.js
+++ b/dev/schema-validate.js
@@ -18,6 +18,7 @@
import Ajv from 'ajv';
import {readFileSync} from 'fs';
+import {fileURLToPath} from 'url';
import {JsonSchema} from '../ext/js/data/json-schema.js';
import {DataError} from './data-error.js';
import {parseJson} from './json.js';
@@ -30,9 +31,9 @@ class JsonSchemaAjv {
const ajv = new Ajv({
meta: false,
strictTuples: false,
- allowUnionTypes: true
+ allowUnionTypes: true,
});
- const metaSchemaPath = require.resolve('ajv/dist/refs/json-schema-draft-07.json');
+ const metaSchemaPath = fileURLToPath(import.meta.resolve('ajv/dist/refs/json-schema-draft-07.json'));
/** @type {import('ajv').AnySchemaObject} */
const metaSchema = parseJson(readFileSync(metaSchemaPath, {encoding: 'utf8'}));
ajv.addMetaSchema(metaSchema);
diff --git a/dev/util.js b/dev/util.js
index 89bd95dae2..67d8d9d8be 100644
--- a/dev/util.js
+++ b/dev/util.js
@@ -17,9 +17,7 @@
*/
import fs from 'fs';
-import JSZip from 'jszip';
import path from 'path';
-import {parseJson} from './json.js';
/**
* @param {string} baseDirectory
@@ -40,70 +38,10 @@ export function getAllFiles(baseDirectory, predicate = null) {
if (typeof predicate !== 'function' || predicate(relativeFileName, false)) {
results.push(relativeFileName);
}
- } else if (stats.isDirectory()) {
- if (typeof predicate !== 'function' || predicate(relativeFileName, true)) {
- directories.push(fullFileName);
- }
+ } else if (stats.isDirectory() && (typeof predicate !== 'function' || predicate(relativeFileName, true))) {
+ directories.push(fullFileName);
}
}
}
return results;
}
-
-/**
- * Creates a zip archive from the given dictionary directory.
- * @param {string} dictionaryDirectory
- * @param {string} [dictionaryName]
- * @returns {import('jszip')}
- */
-export function createDictionaryArchive(dictionaryDirectory, dictionaryName) {
- const fileNames = fs.readdirSync(dictionaryDirectory);
-
- // const zipFileWriter = new BlobWriter();
- // const zipWriter = new ZipWriter(zipFileWriter);
- const archive = new JSZip();
-
- for (const fileName of fileNames) {
- if (/\.json$/.test(fileName)) {
- const content = fs.readFileSync(path.join(dictionaryDirectory, fileName), {encoding: 'utf8'});
- const json = parseJson(content);
- if (fileName === 'index.json' && typeof dictionaryName === 'string') {
- /** @type {import('dictionary-data').Index} */ (json).title = dictionaryName;
- }
- archive.file(fileName, JSON.stringify(json, null, 0));
-
- // await zipWriter.add(fileName, new TextReader(JSON.stringify(json, null, 0)));
- } else {
- const content = fs.readFileSync(path.join(dictionaryDirectory, fileName), {encoding: null});
- archive.file(fileName, content);
-
- // console.log('adding');
- // const r = new TextReader(content);
- // console.log(r.readUint8Array(0, 10));
- // console.log('reader done');
- // await zipWriter.add(fileName, r);
- // console.log('??');
- }
- }
- // await zipWriter.close();
-
- // Retrieves the Blob object containing the zip content into `zipFileBlob`. It
- // is also returned by zipWriter.close() for more convenience.
- // const zipFileBlob = await zipFileWriter.getData();
- return archive;
-
- // return zipFileBlob;
-}
-
-/**
- * @param {(...args: import('core').SafeAny[]) => (unknown|Promise)} func
- * @param {...import('core').SafeAny} args
- */
-export async function testMain(func, ...args) {
- try {
- await func(...args);
- } catch (e) {
- console.log(e);
- process.exit(-1);
- }
-}
diff --git a/docs/advanced-options.md b/docs/advanced-options.md
deleted file mode 100644
index f1740029cb..0000000000
--- a/docs/advanced-options.md
+++ /dev/null
@@ -1,10 +0,0 @@
-## Advanced Options
-
-Click the `Advanced` toggle switch in the bottom left corner of the Settings page to enable advanced options.
-
-### Parse sentences using MeCab
-
-[MeCab](https://taku910.github.io/mecab/) is a third-party program which uses its own dictionaries and parsing algorithm to decompose sentences into individual words. MeCab may provide more accurate parsing results than Yomitan's internal parser.
-
-In order for Yomitan to use it, both MeCab and a native messaging component must be installed.
-A setup guide can be found [here](https://github.com/themoeway/yomitan-mecab-installer/blob/master/README.md).
diff --git a/docs/anki-integration.md b/docs/anki-integration.md
deleted file mode 100644
index 3773009abd..0000000000
--- a/docs/anki-integration.md
+++ /dev/null
@@ -1,109 +0,0 @@
-## Anki Integration
-
-Yomitan features automatic flashcard creation for [Anki](https://apps.ankiweb.net/), a free application designed to help you
-retain knowledge. This feature requires the prior installation of an Anki plugin called [AnkiConnect](https://foosoft.net/projects/anki-connect).
-Check the respective project page for more information about how to set up this software.
-
-### Flashcard Configuration
-
-Before flashcards can be automatically created, you must configure the templates used to create term and/or kanji notes.
-If you are unfamiliar with Anki deck and model management, this would be a good time to reference the [Anki
-Manual](https://docs.ankiweb.net/#/). In short, you must specify what information should be included in the
-flashcards that Yomitan creates through AnkiConnect.
-
-Flashcard fields can be configured with the following steps:
-
-1. Open the Yomitan options page and scroll down to the section labeled _Anki Options_.
-2. Tick the checkbox labeled _Enable Anki integration_ (Anki must be running with [AnkiConnect](https://foosoft.net/projects/anki-connect) installed).
-3. Select the type of template to configure by clicking on either the _Terms_ or _Kanji_ tabs.
-4. Select the Anki deck and model to use for new creating new flashcards of this type.
-5. Fill the model fields with markers corresponding to the information you wish to include (several can be used at
- once). Advanced users can also configure the actual [Handlebars](https://handlebarsjs.com/) templates used to create
- the flashcard contents (this is strictly optional).
-
- #### Markers for Term Cards
-
- | Marker | Description |
- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
- | `{audio}` | Audio sample of a native speaker's pronunciation in MP3 format (if available). |
- | `{clipboard-image}` | An image which is stored in the system clipboard, if present. |
- | `{clipboard-text}` | Text which is stored in the system clipboard, if present. |
- | `{cloze-body}` | Raw, inflected term as it appeared before being reduced to dictionary form by Yomitan. |
- | `{cloze-body-kana}` | Kana reading for `{cloze-body}`. |
- | `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. |
- | `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. |
- | `{conjugation}` | Conjugation path from the raw inflected term to the source term. |
- | `{dictionary}` | Name of the dictionary from which the card is being created (unavailable in _grouped_ mode). |
- | `{document-title}` | Title of the web page that the term appeared in. |
- | `{expression}` | Term expressed as kanji (will be displayed in kana if kanji is not available). |
- | `{frequencies}` | Frequency information for the term. |
- | `{frequency-harmonic-rank}` | The harmonic mean of frequency data for the current term. Defaults to rank 9999999 when frequency data is not found, indicating extremely low rank-based term usage. |
- | `{frequency-harmonic-occurrence}` | The harmonic mean of frequency data for the current term. Defaults to 0 occurrences when frequency data is not found, the lowest possible occurrence-based term usage. |
- | `{frequency-average-rank}` | The average of frequency data for the current term. Defaults to rank 9999999 when frequency data is not found, indicating extremely low rank-based term usage. |
- | `{frequency-average-occurrence}` | The average of frequency data for the current term. Defaults to 0 occurrences when frequency data is not found, the lowest possible occurrence-based term usage. |
- | `{furigana}` | Term expressed as kanji with furigana displayed above it (e.g. 日本語). |
- | `{furigana-plain}` | Term expressed as kanji with furigana displayed next to it in brackets (e.g. 日本語[にほんご]). |
- | `{glossary}` | List of definitions for the term (output format depends on whether running in _grouped_ mode). |
- | `{glossary-brief}` | List of definitions for the term in a more compact format. |
- | `{glossary-no-dictionary}` | List of definitions for the term, except the dictionary tag is omitted. |
- | `{part-of-speech}` | Part of speech information for the term. |
- | `{pitch-accents}` | List of pitch accent downstep notations for the term. |
- | `{pitch-accent-graphs}` | List of pitch accent graphs for the term. |
- | `{pitch-accent-positions}` | List of accent downstep positions for the term as a number. |
- | `{pitch-accent-categories}`| List of pitch accent categories for the term (e.g. heiban, kifuku, atamadaka, odaka, nakadaka). |
- | `{reading}` | Kana reading for the term (empty for terms where the expression is the reading). |
- | `{screenshot}` | Screenshot of the web page taken at the time the term was added. |
- | `{search-query}` | The full search query shown on the search page. |
- | `{selection-text}` | The selected text on the search page or popup. |
- | `{sentence}` | Sentence, quote, or phrase that the term appears in from the source content. |
- | `{sentence-furigana}` | Sentence, quote, or phrase that the term appears in from the source content, with furigana added. |
- | `{tags}` | Grammar and usage tags providing information about the term (unavailable in _grouped_ mode). |
- | `{url}` | Address of the web page in which the term appeared in. |
-
- #### Markers for Kanji Cards
-
- | Marker | Description |
- | --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
- | `{character}` | Unicode glyph representing the current kanji. |
- | `{clipboard-image}` | An image which is stored in the system clipboard, if present. |
- | `{clipboard-text}` | Text which is stored in the system clipboard, if present. |
- | `{cloze-body}` | Raw, inflected parent term as it appeared before being reduced to dictionary form by Yomitan. |
- | `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. |
- | `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. |
- | `{dictionary}` | Name of the dictionary from which the card is being created. |
- | `{document-title}` | Title of the web page that the kanji appeared in. |
- | `{frequencies}` | Frequency information for the kanji. |
- | `{frequency-harmonic-rank}` | The harmonic mean of frequency data for the current kanji. Defaults to rank 9999999 when frequency data is not found, indicating extremely low rank-based kanji usage. |
- | `{frequency-harmonic-occurrence}` | The harmonic mean of frequency data for the current kanji. Defaults to 0 occurrences when frequency data is not found, the lowest possible occurrence-based kanji usage. |
- | `{frequency-average-rank}` | The average of frequency data for the current kanji. Defaults to rank 9999999 when frequency data is not found, indicating extremely low rank-based kanji usage. |
- | `{frequency-average-occurrence}` | The average of frequency data for the current kanji. Defaults to 0 occurrences when frequency data is not found, the lowest possible occurrence-based kanji usage. |
- | `{glossary}` | List of definitions for the kanji. |
- | `{kunyomi}` | Kunyomi (Japanese reading) for the kanji expressed as katakana. |
- | `{onyomi}` | Onyomi (Chinese reading) for the kanji expressed as hiragana. |
- | `{screenshot}` | Screenshot of the web page taken at the time the kanji was added. |
- | `{search-query}` | The full search query shown on the search page. |
- | `{selection-text}` | The selected text on the search page or popup. |
- | `{sentence}` | Sentence, quote, or phrase that the character appears in from the source content. |
- | `{sentence-furigana}` | Sentence, quote, or phrase that the character appears in from the source content, with furigana added. |
- | `{stroke-count}` | Number of strokes that the kanji character has. |
- | `{url}` | Address of the web page in which the kanji appeared in. |
-
-When creating your model for Yomitan, _make sure that you pick a unique field to be first_; fields that will
-contain `{expression}` or `{character}` are ideal candidates for this. Anki does not allow duplicate flashcards to be
-added to a deck by default; it uses the first field in the model to check for duplicates. For example, if you have `{reading}`
-configured to be the first field in your model and 橋 is already in your deck, you will not
-be able to create a flashcard for 箸 because they share the same reading.
-
-### Flashcard Creation
-
-Once Yomitan is configured, it becomes trivial to create new flashcards with a single click. You will see the following
-icons next to term definitions:
-
-- Clicking ![](../img/btn-add-expression.png) adds the current expression as kanji (e.g. 食べる).
-- Clicking ![](../img/btn-add-reading.png) adds the current expression as hiragana or katakana (e.g. たべる).
-
-Below are some troubleshooting tips you can try if you are unable to create new flashcards:
-
-- Individual icons will appear grayed out if a flashcard cannot be created for the current definition (e.g. it already exists in the deck).
-- If all of the buttons appear grayed out, then you should double-check your deck and model configuration settings.
-- If no icons appear at all, make sure that Anki is running in the background and that [AnkiConnect](https://foosoft.net/projects/anki-connect) has been installed.
diff --git a/docs/development/language-features.md b/docs/development/language-features.md
new file mode 100644
index 0000000000..a28a07e21d
--- /dev/null
+++ b/docs/development/language-features.md
@@ -0,0 +1,417 @@
+# Contributing to a Language
+
+Improving Yomitan's features for the language(s) you are interested in is pretty simple, and a great way to help yourself and others. This guide will help you get started.
+
+## Adding a Language
+
+
+
+If your language is not already available in the Language dropdown, here is how you can add it with just a few lines. As an example, we'll use [PR #913](https://github.com/yomidevs/yomitan/pull/913/files), where a first-time contributor added Dutch.
+
+```js
+// language-descriptors.js
+{
+ iso: 'nl',
+ iso639_3: 'nld',
+ name: 'Dutch',
+ exampleText: 'lezen',
+ textPreprocessors: capitalizationPreprocessors
+}
+```
+
+1. Look up the ISO 639-1 and ISO 639-3 codes for your language. If it is a rarer language it might not have a ISO-639-1 code - if that's the case, use ISO 639-3 for both `iso` and `iso639_3`.
+2. Place your language in the `languageDescriptors` array in `language-descriptors.js`. The languages are sorted alphabetically by ISO code.
+3. The example text is usually some form of the verb "to read" in your language, but it can be any example you feel is good. This will be shown in the preview popup on the settings page.
+4. If your language uses the Latin or Cyrillic script, or another script with capitalization, you will want to use the `capitalizationPreprocessors`. We'll cover this part in more detail a bit later. The `textPreprocessors` field can also be left out.
+
+When in doubt, look at the other languages in the file for ideas. The same applies to `language-descriptors.d.ts`:
+
+```ts
+// language-descriptors.d.ts
+nl: {
+ pre: CapitalizationPreprocessors;
+}
+```
+
+This is just for some type safety. The first key is the ISO code. Most languages will then only have a `pre` key (the other one is `post`), and its value is the type of text preprocessors you used in `language-descriptors.js`. Use the TypeScript operator `&` as needed. If you didn't use any text preprocessors, you can set the value to `Record`.
+
+That's it! Your language should now be selectable from the dropdown, and may work perfectly fine already. If you don't already have a dictionary to test with, check out [Dictionaries](../dictionaries.md). For more advanced features, read on.
+
+## Language Features
+
+You should first have the repo set up locally according to the instructions in the [contributing guidelines](../../CONTRIBUTING.md).
+
+A language descriptor in `language-descriptors.js` has several optional fields for more advanced features. We've already mentioned `textPreprocessors`, but there are also `languageTransforms`, `textPostprocessors`, `isTextLookupWorthy`, and `readingNormalizer`. Let's go through them (see also the full type definition in `language-descriptors.d.ts`).
+
+### Text Preprocessors
+
+The scanned text may not exactly match the word in the dictionary. For example, an English dictionary will likely contain the word "read", but the text may contain "Read" or "READ". To handle cases like this, we use text preprocessors.
+
+```ts
+// from language.d.ts
+export type TextProcessor = {
+ name: string;
+ description: string;
+ options: TextProcessorOptions;
+ process: TextProcessorFunction;
+};
+```
+
+Every text preprocessor has:
+
+- A `name` and `description`
+- An array of `options`, most commonly just `[false, true]`, that control the behavior of the `process` function.
+- A `process` function that takes a string and a setting and returns a string
+
+Here are the `CapitalizationPreprocessors` used in the Dutch example:
+
+```js
+/** @type {import('language').TextProcessor} */
+export const decapitalize = {
+ name: "Decapitalize text",
+ description: "CAPITALIZED TEXT → capitalized text",
+ options: basicTextProcessorOptions, // [false, true]
+ process: (str, setting) => (setting ? str.toLowerCase() : str),
+};
+
+/** @type {import('language').TextProcessor} */
+export const capitalizeFirstLetter = {
+ name: "Capitalize first letter",
+ description: "lowercase text → Lowercase text",
+ options: basicTextProcessorOptions, // [false, true]
+ process: (str, setting) =>
+ setting ? str.charAt(0).toUpperCase() + str.slice(1) : str,
+};
+```
+
+When applying preprocessors, each combination will be separately applied and looked up. Since each of these two preprocessors has two options, there are 2\*2=4 possible combinations. For the input string `reaD`, the following strings will be looked up:
+
+- `reaD` (both preprocessors off)
+- `ReaD` (only `capitalizeFirstLetter` on)
+- `read` (only `decapitalize` on)
+- `Read` (both preprocessors on)
+
+Note that the order of text processors can matter. Had we put capitalizeFirstLetter before decapitalize, the 4th string would be `read` instead of `Read`.
+
+#### Letter Variants
+
+A letter or a group of letters may have multiple variants in a language. For example, in German, "ß" can be written as "ss" and vice versa, or in Japanese every kana has a hiragana and a katakana variant. To handle this, we use a bidirectional conversion preprocessor.
+
+```js
+// from german-text-preprocessors.js
+/** @type {import('language').BidirectionalConversionPreprocessor} */
+export const eszettPreprocessor = {
+ name: 'Convert "ß" to "ss"',
+ description: "ß → ss, ẞ → SS and vice versa",
+ options: ["off", "direct", "inverse"],
+ process: (str, setting) => {
+ switch (setting) {
+ case "off":
+ return str;
+ case "direct":
+ return str.replace(/ẞ/g, "SS").replace(/ß/g, "ss");
+ case "inverse":
+ return str.replace(/SS/g, "ẞ").replace(/ss/g, "ß");
+ }
+ },
+};
+```
+
+These have three options: off, direct, and inverse, and the `process` function must handle each of them.
+
+#### Removing Diacritics
+
+In some cases (e.g. German umlauts), diacritics are near-ubiquitous. However some languages (such as Latin, Arabic etc) do not commonly use diacritics, but only in specific kinds of texts (e.g dictionaries, texts for children or learners). In these cases, the dictionaries that Yomitan uses will likely not contain diacritics, but the text may contain them. To handle this, we use a diacritics removal preprocessor.
+
+This kind of text processing is to a degree interdependent with the dictionaries available for the language.
+
+### Deinflection Rules (a.k.a. Language Transforms)
+
+
+
+Deinflection is the process of converting a word to its base or dictionary form. For example, "running" would be deinflected to "run". This is useful for finding the word in the dictionary, as well as helping the user understand the grammar (morphology) of the language.
+
+These grammatical rules are located in files such as `english-transforms.js`.
+
+> Not all the grammatical rules of a language can or need to be implemented in the transforms file. Even a little bit goes a long way, and you can always add more rules later. For every couple rules you add, write some tests in the respective file ([see the writing tests section below](#writing-deinflection-tests)). This will help you verify that your rules are correct, and make sure nothing is accidentally broken along the way.
+
+Transforms files should export a `LanguageTransformDescriptor`, which is then imported in `language-descriptors.js`.
+
+```js
+// from language-transformer.d.ts
+export type LanguageTransformDescriptor = {
+ language: string;
+ conditions: ConditionMapObject;
+ transforms: TransformMapObject;
+};
+
+export type ConditionMapObject = {
+ [type in TCondition]: Condition;
+};
+
+export type TransformMapObject = {
+ [name: string]: Transform;
+};
+
+```
+
+- `language` is the ISO code of the language
+- `conditions` are an object containing parts of speech and grammatical forms that are used to check which deinflections make sense. They are referenced by the deinflection rules.
+- `transforms` are the actual deinflection rules
+- `TCondition` is an optional generic parameter that can be passed to `LanguageTransformDescriptor`. You can learn more about it at the end of this section.
+
+Let's try and write a bit of deinflection for English, from scratch.
+
+```js
+// english-transforms.js
+import { suffixInflection } from "../language-transforms.js";
+
+export const englishTransforms = {
+ language: "en",
+ conditions: {},
+ transforms: {
+ plural: {
+ name: "plural",
+ description: "Plural form of a noun",
+ rules: [suffixInflection("s", "", [], [])],
+ },
+ },
+};
+```
+
+This is a simple example for English, where the only deinflection rule is to remove the "s" from the end of a noun to get the singular form. The `suffixInflection` function is a helper that creates a deinflection rule for a suffix. It takes the suffix to remove, what to replace it with, and two more parameters for conditions, which we will look at next. The `suffixInflection` is the most common type of deinflection rule across languages. The inner `plural` is the displayed description while looking up, and the outer `plural` is a name only to be referenced internally within the file.
+
+For the input string "cats", the following strings will be looked up:
+
+- `cats` (no deinflection)
+- `cat` (deinflected by the `plural` rule)
+
+If the dictionary contains an entry for `cat`, it will successfully match the 2nd looked up string, (as shown in the image). Note the 🧩 symbol and the `plural` rule.
+
+However, this rule will also match the word "reads", and show the verb "read" from the dictionary, marked as being `plural`. This makes no sense, and we can use conditions to prevent it. Let's add a condition and use it in the rule.
+
+```js
+conditions: {
+ n: {
+ name: 'Noun',
+ isDictionaryForm: true,
+ },
+},
+transforms: {
+ "plural": {
+ name: "plural",
+ description: "Plural form of a noun",
+ rules: [
+ suffixInflection("s", "", [], ["n"])
+ ],
+ },
+},
+```
+
+Now, only dictionary entries marked with the same "n" condition will be eligible for matching the `plural` rule. The verb "read" should be marked as "v" in the dictionary, and will no longer be matched by the `plural` rule. The entries in the dictionary need to be marked with the exact same conditions defined in the `conditions` object. The `isDictionaryForm` field can be set to `false`, to allow some conditions to be used only in between rules, and not in the dictionary. In most cases however, it will be set to `true`.
+
+
+
+Now consider the word `dogs'`, as in the `the dogs' bones`. This is the possessive of a plural noun. We can add a rule for the possessive:
+
+```js
+{
+ name: "possessive",
+ description: "Possessive form of a noun",
+ rules: [
+ suffixInflection("'", "", [], ["n"])
+ ],
+},
+```
+
+However, the only `conditionOut` of this rule, `n`, does not match any `conditionIn` of the `plural` rule, because the `plural` rules `conditionsIn` are an empty array. To fix this, we can add a condition to the `plural` rule:
+
+```js
+{
+ name: "plural",
+ description: "Plural form of a noun",
+ rules: [
+ suffixInflection("s", "", ["n"], ["n"])
+ ],
+},
+```
+
+Now the rules will chain together, as shown in the image. Chaining can be very useful (for agglutinative languages it is indispensable), but may cause unexpected behavior. For example, `boss` will now display results for the word `bo` (e.g. the staff) with the `plural` rule applied twice, i.e. it can chain with itself because the `conditionsIn` and `conditionsOut` are the same. This leads us to the actual implementation of the `plural` rule in `english-transforms.js`:
+
+```js
+conditions: {
+ n: {
+ name: "Noun",
+ isDictionaryForm: true,
+ subConditions: ["np", "ns"],
+ },
+ np: {
+ name: "Noun plural",
+ isDictionaryForm: true,
+ },
+ ns: {
+ name: "Noun singular",
+ isDictionaryForm: true,
+ },
+},
+transforms: {
+ "plural": {
+ name: "plural",
+ description: "Plural form of a noun",
+ rules: [
+ suffixInflection("s", "", ["np"], ["ns"])
+ ],
+ },
+},
+```
+
+Since `ns` and `np` are subconditions of `n` they will both match with `n`, but not with each other. This covers all of the requirements we have considered.
+
+The `suffixInflection` is one of a few helper functions - you can write more complex rules, using regex and a function for deinflecting. There are examples of this across the language transforms files.
+
+#### Writing Deinflection Tests
+
+Now that you have added a couple deinflection rules, you might want to start writing some tests to check if the deinflections are behaving correctly. Let's say we wanted to test the behavior of our `plural` and `possessive` rules and even them combined. Our test file should look like this:
+
+```js
+// english-transforms.test.js
+import { englishTransforms } from "../../ext/js/language/en/english-transforms.js";
+import { LanguageTransformer } from "../../ext/js/language/language-transformer.js";
+import { testLanguageTransformer } from "../fixtures/language-transformer-test.js";
+
+const tests = [
+ {
+ category: "plurals and possessive",
+ valid: true,
+ tests: [
+ { term: "cat", source: "cats", rule: "ns", reasons: ["plural"] },
+ { term: "cat", source: "cat's", rule: "ns", reasons: ["possessive"] },
+ {
+ term: "cat",
+ source: "cats'",
+ rule: "ns",
+ reasons: ["plural", "possessive"],
+ },
+ ],
+ },
+];
+
+const languageTransformer = new LanguageTransformer();
+languageTransformer.addDescriptor(englishTransforms);
+testLanguageTransformer(languageTransformer, tests);
+```
+
+The part we want to examine is the `test` array. The other things are common across all test files.
+
+- `term` is the final form of the deinflected word.
+- `source` is the source word to be deinflected to `term`.
+- `rule` is the final condition of `term`. Here, we used `ns` because `cat` is a singular noun.
+- `reasons` represents the chain of deinflection rules needed to get from `source` to `term`.
+
+You can check that all the tests pass by running `npm run test:unit`.
+
+> This command runs all Yomitan unit test files. To only run a single test file, you can instead opt for `npx vitest `.
+
+Now, we may want to verify that `boss` really does not deinflect to `bo`. You can add to the `tests` array:
+
+```js
+{
+ category: 'invalid deinflections',
+ valid: false,
+ tests: [
+ {term: 'boss', source: 'bo', rule: 'ns', reasons: ['plural', 'plural']},
+ ],
+},
+```
+
+Here, by setting `valid` to `false`, we are telling the test function to fail this test case if only `boss` deinflects to `bo` with the `ns` condition under a double `plural` rule.
+
+You can also optionally pass a `preprocess` helper function to `testLanguageTransformer`. Refer to the language transforms test files for its specific use case.
+
+#### Opting in autocompletion
+
+If you want additional type-checking and autocompletion when writing your deinflection rules, you can add them with just a few extra lines of code. Due to the limitations of TypeScript and JSDoc annotations, we will have to perform some type magic in our transformations file, but you don't need to understand what they mean in detail.
+
+Your `english-transforms.js` file should look like this:
+
+```js
+// english-transforms.js
+import { suffixInflection } from "../language-transforms.js";
+
+/** @type {import('language-transformer').LanguageTransformDescriptor} */
+export const englishTransforms = {
+ language: "en",
+ conditions: {
+ n: {
+ name: "Noun",
+ isDictionaryForm: true,
+ subConditions: ["np", "ns"],
+ },
+ np: {
+ name: "Noun plural",
+ isDictionaryForm: true,
+ },
+ ns: {
+ name: "Noun singular",
+ isDictionaryForm: true,
+ },
+ },
+ transforms: {
+ // omitted
+ },
+};
+```
+
+To gain type-safety, we have to pass an additional `TCondition` type parameter to `LanguageTransformDescriptor`. (You can revisit its definition [at the top of this section](#deinflection-rules-aka-language-transforms))
+
+The passed type value should be the union type of all conditions in our transforms. To find this value, we first need to move the `conditions` object outside of `englishTransforms` and extract its type by adding a `/** @typedef {keyof typeof conditions} Condition */` comment at the start of the file. Then, you just need to pass it to the `LanguageTransformDescriptor` type declaration like so:
+
+```js
+// english-transforms.js
+import { suffixInflection } from "../language-transforms.js";
+
+/** @typedef {keyof typeof conditions} Condition */
+
+const conditions = {
+ n: {
+ name: "Noun",
+ isDictionaryForm: true,
+ subConditions: ["np", "ns"],
+ },
+ np: {
+ name: "Noun plural",
+ isDictionaryForm: true,
+ },
+ ns: {
+ name: "Noun singular",
+ isDictionaryForm: true,
+ },
+};
+
+/** @type {import('language-transformer').LanguageTransformDescriptor} */
+export const englishTransforms = {
+ language: "en",
+ conditions,
+ transforms: {
+ // omitted
+ },
+};
+```
+
+Now you should be able to check for types whenever writing a deinflection rule.
+
+### Text Postprocessors
+
+In special cases, text may need to be modified after deinflection. These work exactly like text preprocessors, but are applied after deinflection. Currently, this is only used for Korean, where the Hangul text is disassembled into jamo during preprocessing, and so must be reassembled after deinflection.
+
+### Text Lookup Worthiness
+
+Some features include checking whether a string is possibly a word in the language. For example, trying to look up, in an English dictionary, a word written with non-Latin characters (e.g. "日本語") will never yield any results. To prevent unnecessary lookups, an `isTextLookupWorthy` function can be provided, otherwise all text will be looked up.
+
+### Reading Normalizers
+
+In certain languages, dictionary entries may contain readings as a key to read words, e.g. Kana for Japanese and Pinyin for Chinese. Sometimes, dictionaries may be inconsistent in how they store these readings, leading to the word entries often being split when looked up even though they share the same reading. In these cases, you can use a `readingNormalizer` function to normalize the readings to a common format.
+
+## Stuck?
+
+If you have any questions, please feel free to open a Discussion on GitHub, or find us on the [Yomitan Discord](https://discord.gg/YkQrXW6TXF).
diff --git a/docs/development/npm-scripts.md b/docs/development/npm-scripts.md
new file mode 100644
index 0000000000..09d1803fcf
--- /dev/null
+++ b/docs/development/npm-scripts.md
@@ -0,0 +1,110 @@
+# npm Scripts
+
+This file documents the scripts available in the [package.json](../../package.json) file.
+Scripts can be executed by running `npm run `.
+
+- `anki:css-json:write`
+
+ Writes Anki structured content styling json for use when sending stuctured content dictionaries to Anki.
+
+ CSS rules are taken from `ext/css/structured-content.css` and converted into json.
+
+ CSS rule overrides and exclusions can be set in `dev/data/structured-content-overrides.css`.
+
+- `bench`
+ Runs performance benchmarks.
+
+- `build`
+ Builds packages for all of the primary build targets and outputs them to the builds folder in the root project directory.
+
+- `build:libs`
+ Rebuilds all of the third-party dependencies that the extension uses.
+
+- `build:serve:firefox-android`
+
+ > `adb` and `web-ext` are required to be installed on your computer for this command to work!
+
+ Builds for Firefox and then uses `web-ext` to serve the extension through `adb` to Firefox for Android. Prepend the environment variables WEB_EXT_TARGET and WEB_EXT_ADB_DEVICE for the command to succeed (example: `WEB_EXT_TARGET="firefox-android" WEB_EXT_ADB_DEVICE="emulator-5554" npm run build:serve:firefox-android`). WEB_EXT_TARGET will be "firefox-android" for vanilla Firefox, and you can find the value for WEB_EXT_ADB_DEVICE by running the command `adb devices`.
+
+ [Get started debugging Firefox for Android (recommended)](https://extensionworkshop.com/documentation/develop/getting-started-with-web-ext/#test-and-degug-an-extention)
+
+ [`web-ext run` documentation](https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#web-ext-run)
+
+- `build:serve:kiwi-browser`
+
+ > `adb` is required to be installed on your computer for this command to work!
+
+ Builds for Chromium and then uses `adb` to `push` the built zip file over to `/sdcard/yomitan`. You can then open up Kiwi Browser on the target phone and install the extension through that zip file.
+
+- `test`
+ Runs all of the tests.
+
+- `test:fast`
+ Runs most of the tests that are used more frequently in the typical development process.
+
+- `test:static-analysis`
+ Runs all of the static analysis tests (excluding JSON).
+
+- `test:js`
+ Runs [eslint](https://eslint.org/) on all of the JavaScript and TypeScript files in the project.
+
+- `test:json`
+ Runs all JSON tests.
+
+- `test:json:format`
+ Runs eslint on all of the JSON files in the project.
+
+- `test:json:types`
+ Performs type checking on all of the JSON files in the project.
+ Running this script often takes a long time since it has to validate a lot of files with complex types.
+
+- `test:css`
+ Runs [stylelint](https://stylelint.io/) on all of the CSS files in the project.
+
+- `test:html`
+ Runs [html-validate](https://html-validate.org/) on all of the HTML files in the project.
+
+- `test:md`
+ Runs [prettier](https://prettier.io/) on all of the Markdown files in the project.
+
+- `test:md:write`
+ Uses prettier to fix all issues it encounters with files.
+
+- `test:ts`
+ Runs [TypeScript](https://www.typescriptlang.org/) validation on all of the JavaScript and TypeScript files in the project.
+
+- `test:ts:main`
+ Runs [TypeScript](https://www.typescriptlang.org/) validation on the files in the [ext](../../ext/) folder.
+
+- `test:ts:dev`
+ Runs [TypeScript](https://www.typescriptlang.org/) validation on the files in the [dev](../../dev/) folder.
+
+- `test:ts:test`
+ Runs [TypeScript](https://www.typescriptlang.org/) validation on the files in the [test](../../test/) folder.
+
+- `test:ts:bench`
+ Runs [TypeScript](https://www.typescriptlang.org/) validation on the files in the [benches](../../benches/) folder.
+
+- `test:unit`
+ Runs all of the unit tests in the project using [vitest](https://vitest.dev/).
+
+- `test:unit:write`
+ Overwrites the expected test output data for some of the larger tests.
+ This usually only needs to be run when something modifies the format of dictionary entries or Anki data.
+
+- `test:unit:options`
+ Runs unit tests related to the extension's options and their upgrade process.
+
+- `test:build`
+ Performs a dry run of the build process without generating any files.
+
+- `license-report:html`
+ Generates a file containing license information about the third-party dependencies the extension uses.
+ The resulting file is located at ext/legal-npm.html.
+
+- `license-report:markdown`
+ Generates a Markdown table containing license information about the third-party dependencies the extension uses.
+ This table is located in the [README.md](../../README.md#third-party-libraries) file
+
+- `prepare`
+ Sets up [husky](https://typicode.github.io/husky/) for some git pre-commit tasks.
diff --git a/docs/dictionaries.md b/docs/dictionaries.md
index 73307c3760..0d096a7876 100644
--- a/docs/dictionaries.md
+++ b/docs/dictionaries.md
@@ -1,51 +1,3 @@
-## Dictionaries
+# Dictionaries
-There are several free Japanese dictionaries available for Yomitan, with two of them having glossaries available in
-different languages. You must download and import the dictionaries you wish to use in order to enable Yomitan
-definition lookups. If you have proprietary EPWING dictionaries that you would like to use, check the [Yomitan
-Import](https://github.com/themoeway/yomitan-import) page to learn how to convert and import them into Yomitan.
-
-Be aware that non-English dictionaries contain fewer entries than their English counterparts. Even if your primary
-language is not English, you may consider also importing the English version for better coverage.
-
-- [Jitendex](https://github.com/stephenmk/Jitendex) - Jitendex is an improved version of JMdict for Yomitan. It features better formatting and some other improvements, and is actively being improved by its author.
-- [JMdict](https://github.com/themoeway/jmdict-yomitan#jmdict-for-yomitan-1) - There are daily automatically updated builds of JMdict for Yomitan available in this repository. It is available in multiple languages and formats, but we recommend installing the more modern Jitendex for English users.
-- [JMnedict](https://github.com/themoeway/jmdict-yomitan#jmnedict-for-yomitan) - JMnedict is a dictionary that lists readings of person/place/organization names and other proper nouns.
-- [KANJIDIC](https://github.com/themoeway/jmdict-yomitan#kanjidic-for-yomitan) - KANJIDIC is an English dictionary listing readings, meanings, and other info about kanji characters.
-
-### Importing Dictionaries
-
-Yomitan also supports exporting and importing your entire collection of dictionaries.
-
-#### Importing a Dictionary Collection
-
-- Go to Yomitan's settings page (click on the extension's icon then click on the cog icon from the popup)
-- Click `Import Dictionary Collection` and select the database file you want to import
-- Wait for the import to finish then turn all the dictionaries back on from the `Dictionaries > Configure installed and enabled dictionaries` section
-- Refresh the browser tab to see the dictionaries in effect
-
-#### Exporting the Dictionary Collection
-
-- Click `Export Dictionary Collection` from the backup section of Yomitan's settings page
-- It will show you a progress report as it exports the data then initiates a
- download for a file named something like `yomitan-dictionaries-YYYY-MM-DD-HH-mm-ss.json`
- (e.g. `yomitan-dictionaries-2023-07-05-02-42-04.json`)
-
-### Importing and Exporting Personal Configuration
-
-Note that you can also similarly export and import your Yomitan settings from the `Backup` section of the Settings page.
-
-You should be able to replicate your exact Yomitan setup across devices by exporting your settings and dictionary collection from the source device then importing those from the destination.
-
-## Custom Dictionaries
-
-Yomitan supports the use of custom dictionaries, including the esoteric but popular
-[EPWING](https://ja.wikipedia.org/wiki/EPWING) format. They were often utilized in portable electronic dictionaries
-similar to the ones pictured below. These dictionaries are often sought after by language learners for their correctness
-and excellent coverage of the Japanese language.
-
-Unfortunately, as most of the dictionaries released in this format are proprietary, they are unable to be bundled with
-Yomitan. Instead, you will need to procure these dictionaries yourself and import them using [Yomitan
-Import](https://github.com/themoeway/yomitan-import). Check the project page for additional details.
-
-![Pocket EPWING dictionaries](../img/epwing-devices.jpg)
+### Please visit [yomitan.wiki/dictionaries](https://yomitan.wiki/dictionaries/) for the latest version of this documentation.
diff --git a/docs/faq.md b/docs/faq.md
deleted file mode 100644
index a972b0b9c0..0000000000
--- a/docs/faq.md
+++ /dev/null
@@ -1,58 +0,0 @@
-## Frequently Asked Questions
-
-**I can't scan text in Firefox!**
-
-In Firefox's Manifest V3, host permissions are treated as opt-in. For Yomitan to work properly, the recommended permissions
-must be explicitly set. In the Yomitan welcome page, go to the `Recommended Permissions (Important)` section and check `Enable recommended permissions`.
-
-**I'm having problems importing dictionaries in Firefox, what do I do?**
-
-Yomitan uses the cross-browser IndexedDB system for storing imported dictionary data into your user profile. Although
-everything "just works" in Chrome, depending on settings, Firefox users can run into problems due to browser bugs.
-Yomitan catches errors and tries to offer suggestions about how to work around Firefox issues, but in general at least
-one of the following solutions should work for you:
-
-- Make sure you have cookies enabled. It appears that disabling them also disables IndexedDB for some reason. You
- can still have cookies be disabled on other sites; just make sure to add the Yomitan extension to the whitelist of
- whatever tool you are using to restrict cookies. You can get the extension "URL" by looking at the address bar when
- you have the search page open.
-- Make sure that you have sufficient disk space available on the drive Firefox uses to store your user profile.
- Firefox limits the amount of space that can be used by IndexedDB to a small fraction of the disk space actually
- available on your computer.
-- Make sure that you have history set to "Remember history" enabled in your privacy settings. When this option is
- set to "Never remember history", IndexedDB access is once again disabled for an inexplicable reason.
-- As a last resort, try using the [Refresh Firefox](https://support.mozilla.org/en-US/kb/reset-preferences-fix-problems)
- feature to reset your user profile. It appears that the Firefox profile system can corrupt itself preventing
- IndexedDB from being accessible to Yomitan.
-
-**Will you add support for online dictionaries?**
-
-Online dictionaries will not be implemented because it is not possible to support them in a robust way. In order to
-perform Japanese deinflection, Yomitan must execute dozens of database queries for every single word. Factoring in
-network latency and the fragility of web scraping, it would not be possible to maintain a good and consistent user
-experience.
-
-**Is it possible to use Yomitan with files saved locally on my computer with Chrome?**
-
-In order to use Yomitan with local files in Chrome, you must first tick the _Allow access to file URLs_ checkbox
-for Yomitan on the extensions page. Due to the restrictions placed on browser addons in the WebExtensions model, it
-will likely never be possible to use Yomitan with PDF files.
-
-**Is it possible to delete individual dictionaries without purging the database?**
-
-Yomitan is able to delete individual dictionaries, but keep in mind that this process can be _very_ slow and can
-cause the browser to become unresponsive. The time it takes to delete a single dictionary can sometimes be roughly
-the same as the time it originally took to import, which can be significant for certain large dictionaries.
-
-**Why aren't EPWING dictionaries bundled with Yomitan?**
-
-The vast majority of EPWING dictionaries are proprietary, so they are unfortunately not able to be included in
-this extension due to copyright reasons.
-
-**When are you going to add support for $MYLANGUAGE?**
-
-Developing Yomitan requires a decent understanding of Japanese sentence structure and grammar, and other languages
-are likely to have their own unique set of rules for syntax, grammar, inflection, and so on. Supporting additional
-languages would not only require many additional changes to the codebase, it would also incur significant maintenance
-overhead and knowledge demands for the developers. Therefore, suggestions and contributions for supporting
-new languages will be declined, allowing Yomitan's focus to remain Japanese-centric.
diff --git a/docs/keyboard-shortcuts.md b/docs/keyboard-shortcuts.md
deleted file mode 100644
index bf12a84af4..0000000000
--- a/docs/keyboard-shortcuts.md
+++ /dev/null
@@ -1,25 +0,0 @@
-## Keyboard Shortcuts
-
-The following shortcuts are globally available:
-
-| Shortcut | Action |
-| ---------------------------------- | ------------------------ |
-| Alt + Insert | Open search page. |
-| Alt + Delete | Toggle extension on/off. |
-
-The following shortcuts are available on search results:
-
-| Shortcut | Action |
-| -------------------------------- | --------------------------------------- |
-| Esc | Cancel current search. |
-| Alt + PgUp | Page up through results. |
-| Alt + PgDn | Page down through results. |
-| Alt + End | Go to last result. |
-| Alt + Home | Go to first result. |
-| Alt + Up | Go to previous result. |
-| Alt + Down | Go to next result. |
-| Alt + B | Go to back to source term. |
-| Alt + E | Add current term as expression to Anki. |
-| Alt + R | Add current term as reading to Anki. |
-| Alt + P | Play audio for current term. |
-| Alt + K | Add current kanji to Anki. |
diff --git a/docs/making-yomitan-dictionaries.md b/docs/making-yomitan-dictionaries.md
new file mode 100644
index 0000000000..76a69d84ba
--- /dev/null
+++ b/docs/making-yomitan-dictionaries.md
@@ -0,0 +1,101 @@
+# Making Yomitan Dictionaries
+
+This document provides an overview on how to create your own Yomitan dictionary.
+
+- [Tools](#tools)
+- [Read the Schemas](#read-the-schemas)
+- [Packaging A Dictionary](#packaging-a-dictionary)
+- [Examples](#examples)
+- [Schema Validation](#schema-validation)
+- [Conjugation](#conjugation)
+- [Tag Categories](#tag-categories)
+
+## Tools
+
+- [Yomichan Dictionary Builder](https://github.com/MarvNC/yomichan-dict-builder/): A node package that simplifies the process of making dictionaries, particularly useful for those using TypeScript or JavaScript.
+- [hasUTF16SurrogatePairAt](https://www.npmjs.com/package/@stdlib/assert-has-utf16-surrogate-pair-at): Important for checking if a kanji/hanzi is a surrogate pair, which affects string operations in JavaScript.
+- [japanese-furigana-normalize](https://github.com/MarvNC/japanese-furigana-normalize): A utility function to normalize Japanese readings containing furigana, ensuring proper alignment with kanji characters.
+
+## Read the Schemas
+
+Familiarity with the [Yomitan schemas](https://github.com/yomidevs/yomitan/tree/master/ext/data/schemas) is essential. These schemas define the structure of Yomitan dictionaries. Helpful resources for interpreting JSON schemas include [codebeautify](https://codebeautify.org/jsonviewer/), [json-schema-viewer](https://json-schema-viewer.vercel.app/), and [jsonhero](https://jsonhero.io/).
+
+Below is a list of Yomitan dictionary schemas, their expected filenames, and their usage:
+
+| Schema | Expected Filename | Usage |
+| --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------- |
+| [`dictionary-index-schema.json`](https://github.com/yomidevs/yomitan/tree/master/ext/data/schemas/dictionary-index-schema.json) | `index.json` | Metadata about the dictionary. Please include as much detail as possible. |
+| [`dictionary-kanji-bank-v3-schema.json`](https://github.com/yomidevs/yomitan/tree/master/ext/data/schemas/dictionary-kanji-bank-v3-schema.json) | `kanji_bank_${number}.json` | Information used in the kanji viewer - meanings, readings, statistics, and codepoints. |
+| [`dictionary-kanji-meta-bank-v3-schema.json`](https://github.com/yomidevs/yomitan/tree/master/ext/data/schemas/dictionary-kanji-meta-bank-v3-schema.json) | `kanji_meta-bank_${number}.json` | Stores kanji frequency data. |
+| [`dictionary-tag-bank-v3-schema.json`](https://github.com/yomidevs/yomitan/tree/master/ext/data/schemas/dictionary-tag-bank-v3-schema.json) | `tag_bank_${number}.json` | Defines tags for kanji and term dictionaries, like parts of speech or kanken level. |
+| [`dictionary-term-bank-v3-schema.json`](https://github.com/yomidevs/yomitan/tree/master/ext/data/schemas/dictionary-term-bank-v3-schema.json) | `term_bank_${number}.json` | Stores dictionary readings, definitions, etc. |
+| [`dictionary-term-meta-bank-v3-schema.json`](https://github.com/yomidevs/yomitan/tree/master/ext/data/schemas/dictionary-term-meta-bank-v3-schema.json) | `term_meta_bank_${number}.json` | Stores meta information about terms, such as frequency data and pitch accent data. |
+
+## Adding Custom CSS
+
+You can add custom CSS to a dictionary simply by adding a `styles.css` file to the root of the dictionary zip archive. This file will be loaded by Yomitan and applied to the dictionary viewer with the styles scoped to the dictionary. For example, observe the `styles.css` file in the [official test dictionary](https://github.com/yomidevs/yomitan/tree/master/test/data/dictionaries/valid-dictionary1).
+
+## Packaging A Dictionary
+
+A dictionary can contain various types of information within the zip file. After creating an `index.json` and the relevant data files, zip them with all data `.json` files in the root directory of the zip, not in subfolders. Use the highest compression level possible to reduce the size.
+
+## Examples
+
+- The [official test dictionary](https://github.com/yomidevs/yomitan/tree/master/test/data/dictionaries/valid-dictionary1) showcases the full range of features available in Yomitan dictionaries.
+
+## Schema Validation
+
+To validate schemas, configure [VSCode](https://code.visualstudio.com/docs/languages/json#_json-schemas-and-settings) to validate schemas or use a website such as [jsonschemavalidator](https://www.jsonschemavalidator.net/).
+
+For VSCode validation, use the following settings JSON:
+
+```json
+ "json.schemas": [
+ {
+ "fileMatch": ["kanji_bank_*.json"],
+ "url": "https://github.com/yomidevs/yomitan/raw/master/ext/data/schemas/dictionary-kanji-bank-v3-schema.json"
+ },
+ {
+ "fileMatch": ["kanji_meta_bank_*.json"],
+ "url": "https://github.com/yomidevs/yomitan/raw/master/ext/data/schemas/dictionary-kanji-meta-bank-v3-schema.json"
+ },
+ {
+ "fileMatch": ["tag_bank_*.json"],
+ "url": "https://github.com/yomidevs/yomitan/raw/master/ext/data/schemas/dictionary-tag-bank-v3-schema.json"
+ },
+ {
+ "fileMatch": ["term_bank_*.json"],
+ "url": "https://github.com/yomidevs/yomitan/raw/master/ext/data/schemas/dictionary-term-bank-v3-schema.json"
+ },
+ {
+ "fileMatch": ["term_meta_bank_*.json"],
+ "url": "https://github.com/yomidevs/yomitan/raw/master/ext/data/schemas/dictionary-term-meta-bank-v3-schema.json"
+ }
+ ],
+```
+
+## Conjugation
+
+For Yomitan to conjugate Japanese terms, they need the appropriate part of speech tag. The part of speech labels are documented on the [official JMDict page](http://www.edrdg.org/jmdictdb/cgi-bin/edhelp.py?svc=jmdict&sid=#kw_pos). For other languages, find the part of speech tags in `ext/js/language/{language}/{language}-transforms.js` under the `conditions` label, for labels that aren't prefixed with "Intermediate".
+
+## Tag Categories
+
+The second item in the array of the tag bank schema determines the tag category, affecting the tag color in the user interface. The categories include:
+
+- name
+- expression
+- popular
+- frequent
+- archaism
+- dictionary
+- frequency
+- partOfSpeech
+- search
+- pronunciation-dictionary
+- search
+
+You can view the tag colors [here](https://github.com/yomidevs/yomitan/blob/48f1d012ad5045319d4e492dfbefa39da92817b2/ext/css/display.css#L136-L149).
+
+# Community Contributions
+
+If you have any questions, need help, or want to share a new dictionary, feel free to pop in the [Yomitan Discord server](/README.md#yomitan). We're happy to help you get started!
diff --git a/docs/operations/deployments.md b/docs/operations/deployments.md
new file mode 100644
index 0000000000..f24a5c70f9
--- /dev/null
+++ b/docs/operations/deployments.md
@@ -0,0 +1,25 @@
+# Deployments
+
+We deploy yomitan to the Firefox and Chrome webstore via two channels -- the dev build and the stable build. We do this via a series of GitHub Actions.
+
+Only collaborators with deployment permissions are allowed to deploy.
+
+## Deploying a dev build
+
+1. Tag the commit with a version number. Like: `git tag 24.4.28.0 HEAD` (do this after pulling in the latest changes in master) or `git tag 24.4.28.0 abc123`
+
+> [!WARNING]
+> You can not use leading zeroes in the version tags (e.g. `24.04.28.0`). Firefox store does not allow them and the deploy will fail.
+
+2. Push the tag to origin. `git push origin 24.4.28.0`
+3. The [`Create prerelease on tag`](https://github.com/yomidevs/yomitan/actions/workflows/create-prerelease-on-tag.yml) GH workflow will run and will publish a new release in [Releases](https://github.com/yomidevs/yomitan/releases) as well as kick off a workflow each for publishing to Firefox and Chrome.
+4. Find the corresponding `publish-chrome-development` GH action run and unblock the deployment.
+5. Find the corresponding `publish-firefox-development` GH action run and unblock the deployment.
+6. Wait anywhere between 5mins to a few hours for the build to show up on the [Chrome extension page](https://chromewebstore.google.com/detail/yomitan-development-build/glnaenfapkkecknnmginabpmgkenenml). Firefox does not have a yomitan dev listing and users would have to down the extension locally from "Assets" section of each release.
+
+## Deploying a stable build
+
+1. Go to ["Releases"](https://github.com/yomidevs/yomitan/releases) and pick a version you want to promote to stable.
+2. On the top right corner click on "Edit" and on the bottom there are two options `Set as a pre-release` and `Set as the latest release`. Uncheck `Set as a pre-release` and check `Set as the latest release`.
+3. This will trigger the [`release`](https://github.com/yomidevs/yomitan/actions/workflows/release.yml) workflow which will in turn trigger the `publish-chrome` and `publish-firefox` GH workflows.
+4. Unblock `publish-chrome` and `publish-firefox` respectively and wait 5 mins to a few hours for the extensions to reflect on [Chrome](https://chromewebstore.google.com/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/yomitan/)
diff --git a/docs/permissions.md b/docs/permissions.md
deleted file mode 100644
index d20d25f97d..0000000000
--- a/docs/permissions.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Yomitan Permissions
-
-- ``
- Yomitan requires access to all URLs in order to run scripts to scan text and show the definitions popup,
- request audio for playback and download, and connect with Anki.
-
-- `storage` and `unlimitedStorage`
- Yomitan uses storage permissions in order to save extension settings and dictionary data.
- `unlimitedStorage` is used to help prevent web browsers from unexpectedly
- deleting dictionary data.
-
-- `declarativeNetRequest`
- Yomitan uses this permission to ensure certain requests have valid and secure headers.
- This sometimes involves removing or changing the `Origin` request header,
- as this can be used to fingerprint browser configuration.
-
-- `scripting`
- Yomitan needs to inject content scripts and stylesheets into webpages in order to
- properly display the search popup.
-
-- `offscreen` _(Chrome only)_
- Yomitan uses this permission to create a secondary backend document that has DOM access, given that Manifest v3
- service workers do not. Service workers can then reach out to out to this document in order to complete
- actions that require access to DOM APIs, such as any that require clipboard access.
-
-- `clipboardWrite`
- Yomitan supports simulating the `Ctrl+C` (copy to clipboard) keyboard shortcut
- when a definitions popup is open and focused.
-
-- `clipboardRead` _(optional)_
- Yomitan supports automatically opening a search window when Japanese text is copied to the clipboard
- while the browser is running, depending on how certain settings are configured.
- This allows Yomitan to support scanning text from external applications, provided there is a way
- to copy text from those applications to the clipboard.
-
-- `nativeMessaging` _(optional, unavailable on Firefox for Android)_
- Yomitan has the ability to communicate with an optional native messaging component in order to support
- parsing large blocks of Japanese text using
- [MeCab](https://en.wikipedia.org/wiki/MeCab).
- The installation of this component is optional and is not included by default.
diff --git a/docs/templates.md b/docs/templates.md
index 3f51dc983f..e912ace3a6 100644
--- a/docs/templates.md
+++ b/docs/templates.md
@@ -3,7 +3,7 @@
## Helpers
Yomitan supports several custom Handlebars helpers for rendering templates.
-The source code for these templates can be found [here](../ext/js/templates/sandbox/anki-template-renderer.js).
+The source code for these templates can be found [here](../ext/js/templates/anki-template-renderer.js).
### `dumpObject`
@@ -773,7 +773,7 @@ These functions are used together in order to request media and other types of o
- "screenshot"
- "clipboardImage"
- "clipboardText"
-- "selectionText"
+- "popupSelectionText"
- "textFurigana" japaneseTextreadingMode="default|hiragana|katakana"
- "dictionaryMedia" fileNamedictionary="Dictionary Name"
@@ -790,7 +790,7 @@ These functions are used together in order to request media and other types of o
{{#if (hasMedia "clipboardText")}}The clipboard text is: {{getMedia "clipboardText"}}{{/if}}
- {{#if (hasMedia "selectionText")}}The selection text is: {{getMedia "selectionText"}}{{/if}}
+ {{#if (hasMedia "popupSelectionText")}}The popup selection text is: {{getMedia "popupSelectionText"}}{{/if}}
{{#if (hasMedia "textFurigana" "日本語")}}This is an example of text with generated furigana: {{getMedia "textFurigana" "日本語" escape=false}}{{/if}}
diff --git a/docs/yomichan-migration.md b/docs/yomichan-migration.md
deleted file mode 100644
index 291c296600..0000000000
--- a/docs/yomichan-migration.md
+++ /dev/null
@@ -1,31 +0,0 @@
-## Migrating from Yomichan
-
-### Exporting Data
-
-If you are an existing user of Yomichan, you can export your dictionary collection and settings such that they can be imported into Yomitan to reflect your setup exactly as it was.
-
-You can export your settings from Yomichan's Settings page. Go to the `Backup` section and click on `Export Settings`.
-
-Yomichan doesn't have first-class support to export the dictionary collection. Please follow the instructions provided in the following link to export your data:
-https://github.com/themoeway/yomichan-data-exporter#steps-to-export-the-data
-
-You can then import the exported files into Yomitan from the `Backup` section of the `Settings` page. Please see [the section on importing dictionaries](#importing-dictionaries) further below for more explicit steps.
-
-### Custom Templates
-
-If you do not use custom templates for Anki note creation, this section can be skipped.
-
-Due to security concerns, an alternate implementation of Handlebars is being used which behaves slightly differently.
-This revealed a bug in four of Yomitan's template helpers, which have now been fixed in the default templates. If your
-custom templates use the following helpers, please ensure their use matches the corrected forms.
-
-| Helper | Example | Corrected |
-| ---------------- | ------------------------------------------------------------- | ------------------------------------ |
-| `formatGlossary` | `{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}` | `{{formatGlossary ../dictionary .}}` |
-| `furigana` | `{{#furigana}}{{{definition}}}{{/furigana}}` | `{{furigana definition}}` |
-| `furiganaPlain` | `{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}` | `{{~furiganaPlain .~}}` |
-| `dumpObject` | `{{#dumpObject}}{{{.}}}{{/dumpObject}}` | `{{dumpObject .}}` |
-
-Authors of custom templates may be interested to know that other helpers previously used and documented in the block
-form (e.g. `{{#set "key" "value"}}{{/set}}`), while not broken by this change, may also be replaced with the less verbose
-form (e.g. `{{set "key" "value"}}`). The default templates and helper documentation have been changed to reflect this.
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000000..ff31945832
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,931 @@
+/*
+ * Copyright (C) 2024 Yomitan 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 .
+ */
+
+import {FlatCompat} from '@eslint/eslintrc';
+import js from '@eslint/js';
+import stylistic from '@stylistic/eslint-plugin';
+import vitest from '@vitest/eslint-plugin';
+import esbuild from 'esbuild';
+import header from 'eslint-plugin-header';
+// @ts-expect-error - Missing types https://github.com/import-js/eslint-plugin-import/issues/3133
+import importPlugin from 'eslint-plugin-import';
+import jsdoc from 'eslint-plugin-jsdoc';
+import jsonc from 'eslint-plugin-jsonc';
+import noUnsanitized from 'eslint-plugin-no-unsanitized';
+import sonarjs from 'eslint-plugin-sonarjs';
+import unicorn from 'eslint-plugin-unicorn';
+import unusedImports from 'eslint-plugin-unused-imports';
+import globals from 'globals';
+import parser from 'jsonc-eslint-parser';
+import path from 'node:path';
+import {fileURLToPath} from 'node:url';
+import typescriptEslint from 'typescript-eslint';
+
+const compat = new FlatCompat({
+ baseDirectory: path.dirname(fileURLToPath(import.meta.url)),
+ recommendedConfig: js.configs.recommended,
+ allConfig: js.configs.all,
+});
+
+// @ts-expect-error - This is a workaround https://github.com/Stuk/eslint-plugin-header/issues/57
+header.rules.header.meta.schema = false;
+
+/**
+ * @param {string[]} scriptPaths
+ * @returns {Promise}
+ */
+async function getDependencies(scriptPaths) {
+ const v = await esbuild.build({
+ entryPoints: scriptPaths,
+ bundle: true,
+ minify: false,
+ sourcemap: true,
+ target: 'es2022',
+ format: 'esm',
+ write: false,
+ metafile: true,
+ });
+ const dependencies = Object.keys(v.metafile.inputs);
+ const stringComparer = new Intl.Collator('en-US'); // Invariant locale
+ dependencies.sort((a, b) => stringComparer.compare(a, b));
+ return dependencies;
+}
+
+/**
+ * @type {import('eslint').Linter.Config[]}
+ */
+export default [
+ {
+ ignores: ['ext/lib/', 'dev/lib/handlebars/', '**/node_modules/', '**/builds/'],
+ },
+ ...compat.extends(
+ 'eslint:recommended',
+ 'plugin:jsonc/recommended-with-json',
+ 'plugin:eslint-comments/recommended',
+ ),
+ {
+ plugins: {
+ 'no-unsanitized': noUnsanitized,
+ header,
+ jsdoc,
+ jsonc,
+ 'unused-imports': unusedImports,
+ '@typescript-eslint': typescriptEslint.plugin,
+ '@stylistic': stylistic,
+ unicorn,
+ sonarjs,
+ 'import': importPlugin,
+ },
+
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ ...globals.webextensions,
+ },
+
+ parser: typescriptEslint.parser,
+ ecmaVersion: 2022,
+ sourceType: 'module',
+
+ parserOptions: {
+ ecmaFeatures: {
+ globalReturn: false,
+ impliedStrict: true,
+ },
+
+ project: [
+ './jsconfig.json',
+ './dev/jsconfig.json',
+ './test/jsconfig.json',
+ './benches/jsconfig.json',
+ ],
+ },
+ },
+
+ rules: {
+ 'accessor-pairs': 'error',
+ 'curly': ['error', 'all'],
+ 'default-case-last': 'error',
+ 'dot-notation': 'error',
+ 'eqeqeq': 'error',
+ 'func-names': ['error', 'always'],
+ 'guard-for-in': 'error',
+ 'grouped-accessor-pairs': 'error',
+ 'new-cap': 'error',
+ 'no-alert': 'error',
+ 'no-case-declarations': 'error',
+ 'no-caller': 'error',
+ 'no-const-assign': 'error',
+
+ 'no-constant-condition': ['error', {
+ checkLoops: false,
+ }],
+
+ 'no-constructor-return': 'error',
+ 'no-duplicate-imports': 'error',
+ 'no-eval': 'error',
+ 'no-extend-native': 'error',
+ 'no-global-assign': 'error',
+ 'no-implicit-globals': 'error',
+ 'no-implied-eval': 'error',
+ 'no-new': 'error',
+ 'no-new-native-nonconstructor': 'error',
+ 'no-octal': 'error',
+ 'no-octal-escape': 'error',
+ 'no-param-reassign': 'off',
+ 'no-promise-executor-return': 'error',
+ 'no-prototype-builtins': 'error',
+
+ 'no-restricted-syntax': ['error', {
+ message: 'Avoid using JSON.parse(), prefer parseJson.',
+ selector: 'MemberExpression[object.name=JSON][property.name=parse]',
+ }, {
+ message: 'Avoid using Response.json(), prefer readResponseJson.',
+ selector: 'MemberExpression[property.name=json]',
+ }, {
+ message: 'Avoid using performance, prefer safePerformance.',
+ selector: 'MemberExpression[object.name=performance]',
+ }],
+
+ 'no-self-compare': 'error',
+ 'no-sequences': 'error',
+
+ 'no-shadow': ['error', {
+ builtinGlobals: false,
+ }],
+
+ 'no-shadow-restricted-names': 'error',
+ 'no-template-curly-in-string': 'error',
+ 'no-undef': 'error',
+ 'no-undefined': 'error',
+
+ 'no-underscore-dangle': ['error', {
+ allowAfterThis: true,
+ allowAfterSuper: false,
+ allowAfterThisConstructor: false,
+ }],
+
+ 'no-unexpected-multiline': 'error',
+ 'no-unneeded-ternary': 'error',
+
+ 'no-unused-vars': ['error', {
+ vars: 'local',
+ args: 'after-used',
+ argsIgnorePattern: '^_',
+ caughtErrors: 'none',
+ }],
+
+ 'no-unused-expressions': 'error',
+ 'no-var': 'error',
+ 'no-with': 'error',
+
+ 'prefer-const': ['error', {
+ destructuring: 'all',
+ }],
+
+ 'radix': 'error',
+ 'require-atomic-updates': 'off',
+ 'sort-imports': 'off',
+ 'yoda': ['error', 'never'],
+ '@stylistic/array-bracket-newline': ['error', 'consistent'],
+ '@stylistic/array-bracket-spacing': ['error', 'never'],
+ '@stylistic/array-element-newline': ['error', 'consistent'],
+ '@stylistic/arrow-parens': ['error', 'always'],
+
+ '@stylistic/arrow-spacing': ['error', {
+ before: true,
+ after: true,
+ }],
+
+ '@stylistic/block-spacing': ['error', 'always'],
+
+ '@stylistic/brace-style': ['error', '1tbs', {
+ allowSingleLine: true,
+ }],
+
+ '@stylistic/comma-dangle': ['error', 'always-multiline'],
+
+ '@stylistic/comma-spacing': ['error', {
+ before: false,
+ after: true,
+ }],
+
+ '@stylistic/comma-style': ['error', 'last'],
+ '@stylistic/computed-property-spacing': ['error', 'never'],
+ '@stylistic/dot-location': ['error', 'property'],
+ '@stylistic/eol-last': ['error', 'always'],
+ '@stylistic/func-call-spacing': ['error', 'never'],
+ '@stylistic/function-call-argument-newline': ['error', 'consistent'],
+ '@stylistic/function-call-spacing': ['error', 'never'],
+ '@stylistic/function-paren-newline': ['error', 'multiline-arguments'],
+ '@stylistic/generator-star-spacing': ['error', 'before'],
+ '@stylistic/implicit-arrow-linebreak': ['error', 'beside'],
+
+ '@stylistic/indent': ['error', 4, {
+ SwitchCase: 1,
+ MemberExpression: 1,
+ flatTernaryExpressions: true,
+ ignoredNodes: ['ConditionalExpression'],
+ }],
+
+ '@stylistic/indent-binary-ops': ['error', 0],
+
+ '@stylistic/key-spacing': ['error', {
+ beforeColon: false,
+ afterColon: true,
+ mode: 'strict',
+ }],
+
+ '@stylistic/keyword-spacing': ['error', {
+ before: true,
+ after: true,
+ }],
+
+ '@stylistic/linebreak-style': ['error', 'unix'],
+ '@stylistic/lines-around-comment': 'off',
+
+ '@stylistic/lines-between-class-members': ['error', 'always', {
+ exceptAfterSingleLine: true,
+ }],
+
+ '@stylistic/max-len': 'off',
+
+ '@stylistic/max-statements-per-line': ['error', {
+ max: 2,
+ }],
+
+ '@stylistic/member-delimiter-style': ['error', {
+ multiline: {
+ delimiter: 'semi',
+ requireLast: true,
+ },
+
+ singleline: {
+ delimiter: 'comma',
+ requireLast: false,
+ },
+
+ multilineDetection: 'brackets',
+ }],
+
+ '@stylistic/multiline-ternary': ['error', 'always-multiline'],
+ '@stylistic/new-parens': 'error',
+
+ '@stylistic/newline-per-chained-call': ['error', {
+ ignoreChainWithDepth: 3,
+ }],
+
+ '@stylistic/no-confusing-arrow': 'error',
+ '@stylistic/no-extra-parens': 'off',
+ '@stylistic/no-extra-semi': 'error',
+ '@stylistic/no-floating-decimal': 'error',
+
+ '@stylistic/no-mixed-operators': ['error', {
+ allowSamePrecedence: true,
+ groups: [['&&', '||']],
+ }],
+
+ '@stylistic/no-mixed-spaces-and-tabs': 'error',
+ '@stylistic/no-multi-spaces': 'error',
+
+ '@stylistic/no-multiple-empty-lines': ['error', {
+ max: 2,
+ maxEOF: 0,
+ maxBOF: 0,
+ }],
+
+ '@stylistic/no-tabs': 'error',
+ '@stylistic/no-trailing-spaces': 'error',
+ '@stylistic/no-whitespace-before-property': 'error',
+ '@stylistic/nonblock-statement-body-position': ['error', 'beside'],
+ '@stylistic/object-curly-newline': 'error',
+ '@stylistic/object-curly-spacing': ['error', 'never'],
+
+ '@stylistic/object-property-newline': ['error', {
+ allowAllPropertiesOnSameLine: true,
+ }],
+
+ '@stylistic/one-var-declaration-per-line': ['error', 'initializations'],
+ '@stylistic/operator-linebreak': ['error', 'after'],
+ '@stylistic/padded-blocks': ['error', 'never'],
+
+ '@stylistic/padding-line-between-statements': ['error', {
+ blankLine: 'always',
+ prev: '*',
+ next: 'import',
+ }, {
+ blankLine: 'always',
+ prev: 'import',
+ next: '*',
+ }, {
+ blankLine: 'always',
+ prev: '*',
+ next: 'export',
+ }, {
+ blankLine: 'always',
+ prev: 'import',
+ next: 'let',
+ }, {
+ blankLine: 'always',
+ prev: 'import',
+ next: 'const',
+ }, {
+ blankLine: 'always',
+ prev: 'export',
+ next: 'let',
+ }, {
+ blankLine: 'always',
+ prev: 'export',
+ next: 'const',
+ }, {
+ blankLine: 'always',
+ prev: 'export',
+ next: 'export',
+ }, {
+ blankLine: 'always',
+ prev: 'export',
+ next: 'type',
+ }, {
+ blankLine: 'always',
+ prev: 'type',
+ next: 'export',
+ }, {
+ blankLine: 'always',
+ prev: 'type',
+ next: 'type',
+ }, {
+ blankLine: 'never',
+ prev: 'import',
+ next: 'import',
+ }],
+
+ '@stylistic/quote-props': ['error', 'consistent-as-needed', {
+ numbers: true,
+ }],
+
+ '@stylistic/quotes': ['error', 'single', 'avoid-escape'],
+ '@stylistic/rest-spread-spacing': ['error', 'never'],
+ '@stylistic/semi': 'error',
+
+ '@stylistic/semi-spacing': ['error', {
+ before: false,
+ after: true,
+ }],
+
+ '@stylistic/semi-style': ['error', 'last'],
+ '@stylistic/space-before-blocks': ['error', 'always'],
+
+ '@stylistic/space-before-function-paren': ['error', {
+ anonymous: 'never',
+ named: 'never',
+ asyncArrow: 'always',
+ }],
+
+ '@stylistic/space-in-parens': ['error', 'never'],
+
+ '@stylistic/space-infix-ops': ['error', {
+ int32Hint: false,
+ }],
+
+ '@stylistic/space-unary-ops': 'error',
+ '@stylistic/spaced-comment': ['error', 'always'],
+
+ '@stylistic/switch-colon-spacing': ['error', {
+ after: true,
+ before: false,
+ }],
+
+ '@stylistic/template-curly-spacing': ['error', 'never'],
+ '@stylistic/template-tag-spacing': ['error', 'never'],
+
+ '@stylistic/type-annotation-spacing': ['error', {
+ before: false,
+ after: true,
+
+ overrides: {
+ arrow: {
+ before: true,
+ after: true,
+ },
+ },
+ }],
+
+ '@stylistic/type-generic-spacing': 'error',
+ '@stylistic/type-named-tuple-spacing': 'error',
+ '@stylistic/wrap-iife': ['error', 'inside'],
+ '@stylistic/wrap-regex': 'off',
+
+ '@stylistic/yield-star-spacing': ['error', {
+ before: true,
+ after: false,
+ }],
+
+ 'no-unsanitized/method': 'error',
+ 'no-unsanitized/property': 'error',
+ 'jsdoc/check-access': 'error',
+ 'jsdoc/check-alignment': 'error',
+
+ 'jsdoc/check-line-alignment': ['error', 'never', {
+ wrapIndent: ' ',
+ }],
+
+ 'jsdoc/check-param-names': 'error',
+ 'jsdoc/check-property-names': 'error',
+ 'jsdoc/check-tag-names': 'error',
+ 'jsdoc/empty-tags': 'error',
+ 'jsdoc/check-types': 'error',
+ 'jsdoc/check-values': 'error',
+ 'jsdoc/implements-on-classes': 'error',
+ 'jsdoc/multiline-blocks': 'error',
+ 'jsdoc/no-bad-blocks': 'error',
+ 'jsdoc/no-multi-asterisks': 'error',
+ 'jsdoc/no-undefined-types': 'error',
+ 'jsdoc/require-asterisk-prefix': 'error',
+ 'jsdoc/require-description': 'off',
+ 'jsdoc/require-hyphen-before-param-description': ['error', 'never'],
+
+ 'jsdoc/require-jsdoc': ['error', {
+ require: {
+ ClassDeclaration: false,
+ FunctionDeclaration: true,
+ MethodDefinition: false,
+ },
+
+ contexts: [
+ 'MethodDefinition[kind=constructor]>FunctionExpression>BlockStatement>ExpressionStatement>AssignmentExpression[left.object.type=ThisExpression]',
+ 'ClassDeclaration>Classbody>PropertyDefinition',
+ 'MethodDefinition[kind!=constructor][kind!=set]',
+ 'MethodDefinition[kind=constructor][value.params.length>0]',
+ ],
+
+ checkGetters: 'no-setter',
+ checkSetters: 'no-getter',
+ }],
+
+ 'jsdoc/require-param': 'error',
+ 'jsdoc/require-param-description': 'off',
+ 'jsdoc/require-param-name': 'error',
+ 'jsdoc/require-param-type': 'error',
+ 'jsdoc/require-property': 'error',
+ 'jsdoc/require-property-description': 'off',
+ 'jsdoc/require-property-name': 'error',
+ 'jsdoc/require-property-type': 'error',
+ 'jsdoc/require-returns': 'error',
+ 'jsdoc/require-returns-check': 'error',
+ 'jsdoc/require-returns-description': 'off',
+ 'jsdoc/require-returns-type': 'error',
+ 'jsdoc/require-throws': 'error',
+ 'jsdoc/require-yields': 'error',
+ 'jsdoc/require-yields-check': 'error',
+
+ 'jsdoc/tag-lines': ['error', 'never', {
+ startLines: 0,
+ }],
+
+ 'jsdoc/valid-types': 'error',
+ 'jsonc/indent': ['error', 4],
+ 'jsonc/array-bracket-newline': ['error', 'consistent'],
+ 'jsonc/array-bracket-spacing': ['error', 'never'],
+ 'jsonc/array-element-newline': ['error', 'consistent'],
+ 'jsonc/comma-style': ['error', 'last'],
+
+ 'jsonc/key-spacing': ['error', {
+ beforeColon: false,
+ afterColon: true,
+ mode: 'strict',
+ }],
+
+ 'jsonc/no-octal-escape': 'error',
+
+ 'jsonc/object-curly-newline': ['error', {
+ consistent: true,
+ }],
+
+ 'jsonc/object-curly-spacing': ['error', 'never'],
+
+ 'jsonc/object-property-newline': ['error', {
+ allowAllPropertiesOnSameLine: true,
+ }],
+
+ 'eslint-comments/no-unused-disable': 'error',
+ 'unused-imports/no-unused-imports': 'error',
+ 'import/extensions': ['error', 'ignorePackages'],
+
+ 'unicorn/catch-error-name': ['error', {
+ ignore: ['^(e|error2?)$'],
+ }],
+
+ 'unicorn/custom-error-definition': 'error',
+ 'unicorn/empty-brace-spaces': 'error',
+ 'unicorn/error-message': 'error',
+ 'unicorn/expiring-todo-comments': 'error',
+ 'unicorn/explicit-length-check': 'error',
+ 'unicorn/new-for-builtins': 'error',
+ 'unicorn/no-abusive-eslint-disable': 'error',
+ 'unicorn/no-array-for-each': 'error',
+ 'unicorn/no-array-method-this-argument': 'error',
+ 'unicorn/no-array-push-push': 'error',
+ 'unicorn/no-array-reduce': 'error',
+ 'unicorn/no-console-spaces': 'error',
+ 'unicorn/no-document-cookie': 'error',
+ 'unicorn/no-empty-file': 'error',
+ 'unicorn/no-hex-escape': 'error',
+ 'unicorn/no-instanceof-array': 'error',
+ 'unicorn/no-invalid-remove-event-listener': 'error',
+ 'unicorn/no-lonely-if': 'error',
+ 'unicorn/no-nested-ternary': 'error',
+ 'unicorn/no-new-buffer': 'error',
+ 'unicorn/no-object-as-default-parameter': 'error',
+ 'unicorn/no-static-only-class': 'error',
+ 'unicorn/no-thenable': 'error',
+ 'unicorn/no-unnecessary-await': 'error',
+ 'unicorn/no-unnecessary-polyfills': 'error',
+ 'unicorn/no-unreadable-array-destructuring': 'error',
+ 'unicorn/no-unreadable-iife': 'error',
+ 'unicorn/no-useless-fallback-in-spread': 'error',
+ 'unicorn/no-useless-length-check': 'error',
+ 'unicorn/no-useless-promise-resolve-reject': 'error',
+ 'unicorn/no-useless-spread': 'error',
+ 'unicorn/no-useless-switch-case': 'error',
+ 'unicorn/no-useless-undefined': 'error',
+ 'unicorn/no-zero-fractions': 'error',
+ 'unicorn/prefer-array-find': 'error',
+ 'unicorn/prefer-array-flat': 'error',
+ 'unicorn/prefer-array-flat-map': 'error',
+ 'unicorn/prefer-array-index-of': 'error',
+ 'unicorn/prefer-array-some': 'error',
+ 'unicorn/prefer-date-now': 'error',
+ 'unicorn/prefer-default-parameters': 'error',
+ 'unicorn/prefer-dom-node-dataset': 'error',
+ 'unicorn/prefer-dom-node-text-content': 'error',
+ 'unicorn/prefer-event-target': 'error',
+ 'unicorn/prefer-export-from': 'error',
+ 'unicorn/prefer-includes': 'error',
+ 'unicorn/prefer-keyboard-event-key': 'error',
+ 'unicorn/prefer-logical-operator-over-ternary': 'error',
+ 'unicorn/prefer-modern-math-apis': 'error',
+ 'unicorn/prefer-module': 'error',
+ 'unicorn/prefer-native-coercion-functions': 'error',
+ 'unicorn/prefer-negative-index': 'error',
+ 'unicorn/prefer-number-properties': 'error',
+ 'unicorn/prefer-object-from-entries': 'error',
+ 'unicorn/prefer-prototype-methods': 'error',
+ 'unicorn/prefer-reflect-apply': 'error',
+ 'unicorn/prefer-regexp-test': 'error',
+ 'unicorn/prefer-set-has': 'error',
+ 'unicorn/prefer-set-size': 'error',
+ 'unicorn/prefer-spread': 'error',
+ 'unicorn/prefer-string-starts-ends-with': 'error',
+ 'unicorn/prefer-string-trim-start-end': 'error',
+ 'unicorn/prefer-switch': 'error',
+ 'unicorn/prefer-ternary': 'error',
+ 'unicorn/relative-url-style': 'error',
+ 'unicorn/require-array-join-separator': 'error',
+ 'unicorn/require-number-to-fixed-digits-argument': 'error',
+ 'unicorn/template-indent': 'error',
+ 'unicorn/throw-new-error': 'error',
+ 'sonarjs/max-switch-cases': 'error',
+ 'sonarjs/no-all-duplicated-branches': 'error',
+ 'sonarjs/no-collapsible-if': 'error',
+ 'sonarjs/no-collection-size-mischeck': 'error',
+ 'sonarjs/no-duplicated-branches': 'error',
+ 'sonarjs/no-element-overwrite': 'error',
+ 'sonarjs/no-empty-collection': 'error',
+ 'sonarjs/no-extra-arguments': 'error',
+ 'sonarjs/no-gratuitous-expressions': 'error',
+ 'sonarjs/no-identical-conditions': 'error',
+ 'sonarjs/no-identical-expressions': 'error',
+ 'sonarjs/no-identical-functions': 'error',
+ 'sonarjs/no-ignored-return': 'error',
+ 'sonarjs/no-inverted-boolean-check': 'error',
+ 'sonarjs/no-one-iteration-loop': 'error',
+ 'sonarjs/no-redundant-boolean': 'error',
+ 'sonarjs/no-redundant-jump': 'error',
+ 'sonarjs/no-same-line-conditional': 'error',
+ 'sonarjs/no-unused-collection': 'error',
+ 'sonarjs/no-use-of-empty-return-value': 'error',
+ 'sonarjs/no-useless-catch': 'error',
+ 'sonarjs/non-existent-operator': 'error',
+ 'sonarjs/prefer-immediate-return': 'error',
+ 'sonarjs/prefer-object-literal': 'error',
+ 'sonarjs/prefer-single-boolean-return': 'error',
+ 'sonarjs/prefer-while': 'error',
+ },
+ },
+ ...typescriptEslint.configs.recommendedTypeChecked.map((config) => ({
+ ...config,
+ files: [
+ '**/*.js',
+ '**/*.ts',
+ ],
+ })),
+ {
+ files: [
+ '**/*.js',
+ '**/*.ts',
+ ],
+
+ rules: {
+ '@typescript-eslint/no-floating-promises': ['error', {
+ ignoreIIFE: true,
+ }],
+
+ '@typescript-eslint/no-misused-promises': 'off',
+ '@typescript-eslint/no-redundant-type-constituents': 'error',
+ '@typescript-eslint/no-unsafe-argument': 'error',
+ '@typescript-eslint/no-unsafe-assignment': 'error',
+ '@typescript-eslint/no-unsafe-call': 'off',
+ '@typescript-eslint/no-unsafe-enum-comparison': 'off',
+ '@typescript-eslint/no-unsafe-member-access': 'off',
+ '@typescript-eslint/no-unsafe-return': 'off',
+ '@typescript-eslint/require-await': 'off',
+ '@typescript-eslint/restrict-template-expressions': 'off',
+ '@typescript-eslint/prefer-promise-reject-errors': 'off',
+
+ '@typescript-eslint/ban-ts-comment': ['error', {
+ 'ts-expect-error': {
+ descriptionFormat: '^ - .+$',
+ },
+ }],
+
+ '@typescript-eslint/no-empty-object-type': 'error',
+ '@typescript-eslint/no-unsafe-function-type': 'error',
+ '@typescript-eslint/no-wrapper-object-types': 'error',
+ '@typescript-eslint/no-explicit-any': 'error',
+
+ '@typescript-eslint/no-shadow': ['error', {
+ builtinGlobals: false,
+ }],
+
+ '@typescript-eslint/no-this-alias': 'error',
+
+ '@typescript-eslint/no-unused-vars': ['error', {
+ vars: 'local',
+ args: 'after-used',
+ argsIgnorePattern: '^_',
+ caughtErrors: 'none',
+ }],
+ },
+ },
+ {
+ files: [
+ '**/*.ts',
+ ],
+
+ rules: {
+ '@stylistic/block-spacing': 'off',
+
+ '@stylistic/comma-dangle': ['error', {
+ arrays: 'always-multiline',
+ objects: 'always-multiline',
+ imports: 'always-multiline',
+ exports: 'always-multiline',
+ functions: 'always-multiline',
+ enums: 'always-multiline',
+ generics: 'always-multiline',
+ tuples: 'always-multiline',
+ }],
+
+ '@stylistic/indent-binary-ops': 'off',
+
+ '@stylistic/no-multiple-empty-lines': ['error', {
+ max: 1,
+ maxEOF: 0,
+ maxBOF: 0,
+ }],
+
+ '@stylistic/no-extra-parens': ['error', 'all'],
+ },
+ },
+ {
+ files: [
+ '**/*.json',
+ ],
+
+ languageOptions: {
+ parser: parser,
+ },
+ },
+ {
+ files: [
+ '.vscode/launch.json',
+ ],
+
+ rules: {
+ 'jsonc/no-comments': 'off',
+ },
+ },
+ {
+ files: [
+ 'ext/data/schemas/options-schema.json',
+ ],
+
+ rules: {
+ '@stylistic/no-multi-spaces': 'off',
+ },
+ },
+ {
+ files: [
+ 'test/data/anki-note-builder-test-results.json',
+ 'test/data/database-test-cases.json',
+ 'test/data/translator-test-results-note-data1.json',
+ 'test/data/translator-test-results.json',
+ ],
+
+ rules: {
+ 'jsonc/indent': ['error', 2],
+ },
+ },
+ {
+ files: [
+ 'test/data/dictionaries/valid-dictionary1/term_bank_1.json',
+ 'test/data/dictionaries/valid-dictionary1/term_bank_2.json',
+ ],
+
+ rules: {
+ 'jsonc/array-element-newline': 'off',
+ 'jsonc/object-property-newline': 'off',
+ },
+ },
+ {
+ files: [
+ '**/*.js',
+ '**/*.ts',
+ ],
+
+ rules: {
+ 'header/header': ['error', 'block', {
+ pattern: ' \\* Copyright \\(C\\) (2023-)?2024 Yomitan Authors(\n \\* Copyright \\(C\\) (20(16|17|18|19|20|21)-)?2022 Yomichan Authors)?\n \\*\n \\* This program is free software: you can redistribute it and/or modify\n \\* it under the terms of the GNU General Public License as published by\n \\* the Free Software Foundation, either version 3 of the License, or\n \\* \\(at your option\\) any later version\\.\n \\*\n \\* This program is distributed in the hope that it will be useful,\n \\* but WITHOUT ANY WARRANTY; without even the implied warranty of\n \\* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE\\. See the\n \\* GNU General Public License for more details\\.\n \\*\n \\* You should have received a copy of the GNU General Public License\n \\* along with this program\\. If not, see \\.\n ',
+ }],
+ },
+ },
+ {
+ files: [
+ 'ext/**/*.js',
+ ],
+
+ rules: {
+ 'no-console': 'error',
+ },
+ },
+ {
+ files: [
+ 'test/**/*.js',
+ 'dev/**/*.js',
+ '**/integration.spec.js',
+ '**/playwright.config.js',
+ '**/playwright-util.js',
+ '**/visual.spec.js',
+ ],
+
+ languageOptions: {
+ globals: {
+ ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, 'off'])),
+ ...globals.node,
+ ...Object.fromEntries(Object.entries(globals.webextensions).map(([key]) => [key, 'off'])),
+ },
+ },
+ },
+ {
+ files: [
+ 'test/data/html/**/*.js',
+ ],
+
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
+ ...Object.fromEntries(Object.entries(globals.webextensions).map(([key]) => [key, 'off'])),
+ },
+
+ ecmaVersion: 5,
+ sourceType: 'script',
+ },
+ },
+ {
+ files: [
+ 'test/data/html/**/*.js',
+ ],
+ ignores: [
+ 'test/data/html/js/html-test-utilities.js',
+ ],
+
+ languageOptions: {
+ globals: {
+ HtmlTestUtilities: 'readonly',
+ },
+ },
+ },
+ {
+ files: [
+ 'test/**/*.test.js',
+ ],
+ plugins: {
+ vitest,
+ },
+ ...vitest.configs.recommended,
+ rules: {
+ 'vitest/prefer-to-be': 'off',
+ },
+ },
+ ...compat.extends('plugin:@typescript-eslint/disable-type-checked').map((config) => ({
+ ...config,
+ files: [
+ 'dev/lib/**/*.js',
+ 'eslint.config.js',
+ ],
+ })),
+ {
+ files: [
+ 'ext/js/templates/template-renderer-frame-main.js',
+ ...await getDependencies(['ext/js/templates/template-renderer-frame-main.js']),
+ ],
+
+ languageOptions: {
+ globals: {
+ ...Object.fromEntries(Object.entries(globals.webextensions).map(([key]) => [key, 'off'])),
+ },
+ },
+ },
+ {
+ files: [
+ 'ext/js/dictionary/dictionary-worker-main.js',
+ ...await getDependencies(['ext/js/dictionary/dictionary-worker-main.js']),
+ ],
+
+ languageOptions: {
+ globals: {
+ ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, 'off'])),
+ ...globals.worker,
+ },
+ },
+ },
+ {
+ files: [
+ 'ext/js/background/background-main.js',
+ ...await getDependencies(['ext/js/background/background-main.js']),
+ ],
+
+ languageOptions: {
+ globals: {
+ ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, 'off'])),
+ ...globals.serviceworker,
+ FileReader: 'readonly',
+ Intl: 'readonly',
+ crypto: 'readonly',
+ AbortController: 'readonly',
+ },
+ },
+ },
+ {
+ files: [
+ 'ext/data/recommended-dictionaries.json',
+ ],
+
+ rules: {
+ 'jsonc/sort-keys': ['error', {
+ pathPattern: '.*',
+ hasProperties: ['name'],
+ order: ['name', 'description', 'homepage', 'downloadUrl'],
+ }, {
+ pathPattern: '.*',
+
+ order: {
+ type: 'asc',
+ },
+ }],
+ },
+ },
+ {
+ files: [
+ 'ext/data/recommended-settings.json',
+ ],
+
+ rules: {
+ 'jsonc/sort-keys': ['error', {
+ pathPattern: '.*',
+ order: ['modification', 'description'],
+ }, {
+ pathPattern: '.*',
+
+ order: {
+ type: 'asc',
+ },
+ }],
+ },
+ },
+];
diff --git a/ext/action-popup.html b/ext/action-popup.html
index b60e7055e0..e7d69100e5 100644
--- a/ext/action-popup.html
+++ b/ext/action-popup.html
@@ -11,78 +11,54 @@
+
+
-
-