Compare commits

..

1 Commits

Author SHA1 Message Date
e18985fb14 Enhance AniList character dictionary sync and subtitle features (#15) 2026-03-07 18:30:59 -08:00
491 changed files with 1473 additions and 167535 deletions

View File

@@ -31,7 +31,8 @@ jobs:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-bun-

View File

@@ -31,22 +31,23 @@ jobs:
with:
node-version: 22.12.0
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build (TypeScript check)
run: bun run typecheck
- name: Test suite (source)
run: bun run test:fast
@@ -84,6 +85,11 @@ jobs:
with:
bun-version: 1.3.5
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.12.0
- name: Cache dependencies
uses: actions/cache@v4
with:
@@ -91,7 +97,8 @@ jobs:
~/.bun/install/cache
node_modules
vendor/texthooker-ui/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json') }}
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-bun-
@@ -140,6 +147,11 @@ jobs:
with:
bun-version: 1.3.5
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.12.0
- name: Cache dependencies
uses: actions/cache@v4
with:
@@ -147,7 +159,8 @@ jobs:
~/.bun/install/cache
node_modules
vendor/texthooker-ui/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json') }}
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-bun-

2
.gitignore vendored
View File

@@ -5,6 +5,7 @@ node_modules/
out/
dist/
release/
build/yomitan/
# Launcher build artifact (produced by make build-launcher)
/subminer
@@ -36,3 +37,4 @@ tests/*
.worktrees/
.codex/*
.agents/*
docs/*

6
.gitmodules vendored
View File

@@ -5,6 +5,6 @@
[submodule "vendor/yomitan-jlpt-vocab"]
path = vendor/yomitan-jlpt-vocab
url = https://github.com/stephenmk/yomitan-jlpt-vocab
[submodule "yomitan-jlpt-vocab"]
path = vendor/yomitan-jlpt-vocab
url = https://github.com/stephenmk/yomitan-jlpt-vocab
[submodule "vendor/subminer-yomitan"]
path = vendor/subminer-yomitan
url = https://github.com/ksyasuda/subminer-yomitan

View File

@@ -98,7 +98,7 @@ ensure-bun:
@command -v bun >/dev/null 2>&1 || { printf '%s\n' "[ERROR] bun not found"; exit 1; }
pretty: ensure-bun
@bun run format
@bun run format:src
build:
@printf '%s\n' "[INFO] Detected platform: $(PLATFORM)"

View File

@@ -54,7 +54,7 @@ chmod +x ~/.local/bin/subminer
> [!NOTE]
> The `subminer` wrapper uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
**From source** or **macOS**see the [installation guide](https://docs.subminer.moe/installation#from-source).
**From source** or **macOS**initialize submodules first (`git submodule update --init --recursive`). Source builds now also require Node.js 22 + npm because bundled Yomitan is built from the `vendor/subminer-yomitan` submodule into `build/yomitan` during `bun run build`. Full install guide: [docs.subminer.moe/installation#from-source](https://docs.subminer.moe/installation#from-source).
### 2. Launch the app once
@@ -92,7 +92,7 @@ subminer --start video.mkv # optional explicit overlay start when plugin auto_st
| Required | Optional |
| ------------------------------------------ | -------------------------------------------------- |
| `bun` | |
| `bun`, `node` 22, `npm` | |
| `mpv` with IPC socket | `yt-dlp` |
| `ffmpeg` | `guessit` (better AniSkip title/episode detection) |
| `mecab` + `mecab-ipadic` | `fzf` / `rofi` |

View File

@@ -4,15 +4,15 @@ title: Index AniList character alternative names in the character dictionary
status: Done
assignee: []
created_date: '2026-03-07 00:00'
updated_date: '2026-03-07 00:00'
updated_date: '2026-03-08 00:11'
labels:
- dictionary
- anilist
priority: high
dependencies: []
references:
- /home/sudacode/projects/japanese/SubMiner/src/main/character-dictionary-runtime.ts
- /home/sudacode/projects/japanese/SubMiner/src/main/character-dictionary-runtime.test.ts
- src/main/character-dictionary-runtime.ts
- src/main/character-dictionary-runtime.test.ts
priority: high
---
## Description

View File

@@ -0,0 +1,51 @@
---
id: TASK-110
title: Replace vendored Yomitan with submodule-built Chrome artifact workflow
status: Done
assignee: []
created_date: '2026-03-07 11:05'
updated_date: '2026-03-07 11:22'
labels:
- yomitan
- build
- release
dependencies: []
priority: high
ordinal: 9010
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Replace the checked-in `vendor/yomitan` release tree with a `subminer-yomitan` git submodule. Build Yomitan from source, extract the Chromium zip artifact into a stable local build directory, and make SubMiner dev/runtime/tests/release packaging load that extracted extension instead of the source tree or vendored files.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Repo tracks Yomitan as a git submodule instead of committed extension files under `vendor/yomitan`.
- [x] #2 SubMiner has a reproducible build/extract step that produces a local Chromium extension directory from `subminer-yomitan`.
- [x] #3 Dev/runtime/tests resolve the extracted build output as the default Yomitan extension path.
- [x] #4 Release packaging includes the extracted Chromium extension files instead of the old vendored tree.
- [x] #5 Docs and verification commands reflect the new workflow.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Replaced the checked-in `vendor/yomitan` extension tree with a `vendor/subminer-yomitan` git submodule and added a reproducible `bun run build:yomitan` workflow that builds `yomitan-chrome.zip`, extracts it into `build/yomitan`, and reuses a source-state stamp to skip redundant rebuilds. Runtime path resolution, helper CLIs, Yomitan integration tests, packaging, CI cache keys, and README source-build notes now all target that generated artifact instead of the old vendored files.
Verification:
- `bun run build:yomitan`
- `bun test src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-structured-content-generator.test.ts src/yomitan-translator-sort.test.ts`
- `bun run typecheck`
- `bun run build`
- `bun run test:core:src`
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -0,0 +1,71 @@
---
id: TASK-111
title: Fix subtitle-cycle OSD labels for J keybindings
status: Done
assignee:
- Codex
created_date: '2026-03-07 23:45'
updated_date: '2026-03-08 00:06'
labels: []
dependencies: []
references:
- /Users/sudacode/projects/japanese/SubMiner/src/core/services/ipc-command.ts
- /Users/sudacode/projects/japanese/SubMiner/src/core/services/mpv.ts
- >-
/Users/sudacode/projects/japanese/SubMiner/src/core/services/ipc-command.test.ts
- >-
/Users/sudacode/projects/japanese/SubMiner/src/core/services/mpv-control.test.ts
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
When cycling subtitle tracks with the default J/Shift+J keybindings, the mpv OSD currently shows raw template text like `${sid}` instead of a resolved subtitle label. Update the keybinding OSD behavior so users see the active subtitle selection clearly when cycling tracks, and ensure placeholder-based OSD messages sent through the mpv client API render correctly.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Pressing the primary subtitle cycle keybinding shows a resolved subtitle label on the OSD instead of a raw `${sid}` placeholder.
- [x] #2 Pressing the secondary subtitle cycle keybinding shows a resolved subtitle label on the OSD instead of a raw `${secondary-sid}` placeholder.
- [x] #3 Proxy OSD messages that rely on mpv property expansion render resolved values when sent through the mpv client API.
- [x] #4 Regression tests cover the subtitle-cycle OSD behavior and the placeholder-expansion OSD path.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add focused failing tests for subtitle-cycle OSD labels and mpv placeholder-expansion behavior.
2. Update the IPC mpv command handler to resolve primary and secondary subtitle track labels from mpv `track-list` data after cycling subtitle tracks.
3. Update the mpv OSD runtime path so placeholder-based `show-text` messages sent through the client API opt into property expansion.
4. Run focused tests, then the relevant core test lane, and record results in the task notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Initial triage: `ipc-command.ts` emits raw `${sid}`/`${secondary-sid}` placeholder strings, and `showMpvOsdRuntime` sends `show-text` via mpv client API without enabling property expansion.
User approved implementation plan on 2026-03-07.
Implementation: proxy mpv command OSD now supports an async resolver so subtitle track cycling can show human-readable labels instead of raw `${sid}` placeholders.
Implementation: `showMpvOsdRuntime` now prefixes placeholder-based messages with mpv client-api `expand-properties`, which fixes raw `${...}` OSD output for subtitle delay/position messages.
Testing: `bun test src/core/services/ipc-command.test.ts src/core/services/mpv-control.test.ts src/main/runtime/mpv-proxy-osd.test.ts src/main/runtime/ipc-mpv-command-main-deps.test.ts src/main/runtime/ipc-bridge-actions.test.ts src/main/runtime/ipc-bridge-actions-main-deps.test.ts src/main/runtime/composers/ipc-runtime-composer.test.ts` passed.
Testing: `bun x tsc --noEmit` passed.
Testing: `bun run test:core:src` passed (423 pass, 6 skip, 0 fail).
Docs: no update required because no checked-in docs or help text describe the J/Shift+J OSD output behavior.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed subtitle-cycle OSD handling for the default J/Shift+J keybindings. The IPC mpv command path now supports resolving proxy OSD text asynchronously, and the main-runtime resolver reads mpv `track-list` state so primary and secondary subtitle cycling show human-readable track labels instead of raw `${sid}` / `${secondary-sid}` placeholders.
Also fixed the lower-level mpv OSD transport so placeholder-based `show-text` messages sent through the client API opt into `expand-properties`. That preserves existing template-based OSD messages like subtitle delay and subtitle position without leaking the raw `${...}` syntax.
Added regression coverage for the async proxy OSD path, the placeholder-expansion `showMpvOsdRuntime` path, and the runtime subtitle-track label resolver. Verification run: `bun x tsc --noEmit`; focused mpv/IPC tests; and the maintained `bun run test:core:src` lane (423 pass, 6 skip, 0 fail).
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -0,0 +1,61 @@
---
id: TASK-112
title: Address Claude review items on PR 15
status: Done
assignee:
- codex
created_date: '2026-03-08 00:11'
updated_date: '2026-03-08 00:12'
labels:
- pr-review
- ci
dependencies: []
references:
- .github/workflows/release.yml
- .github/workflows/ci.yml
- .gitmodules
- >-
backlog/tasks/task-101 -
Index-AniList-character-alternative-names-in-the-character-dictionary.md
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Review Claude's PR feedback on PR #15, implement only the technically valid fixes on the current branch, and document which comments are non-actionable or already acceptable.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Validated Claude's concrete PR review items against current branch state and repo conventions
- [x] #2 Implemented the accepted fixes with regression coverage or verification where applicable
- [x] #3 Documented which review items are non-blocking or intentionally left unchanged
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Validate each Claude review item against current branch files and repo workflow.
2. Patch release quality-gate to match CI ordering and add explicit typecheck.
3. Remove duplicate .gitmodules stanza and normalize the TASK-101 reference path through Backlog MCP.
4. Run relevant verification for workflow/config metadata changes and record which review items remain non-actionable.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
User asked to address Claude PR comments on PR #15 and assess whether any action items remain. Treat review suggestions skeptically; only fix validated defects.
Validated Claude's five review items. Fixed release workflow ordering/typecheck, removed the duplicate .gitmodules entry, and normalized TASK-101 references to repo-relative paths via Backlog MCP.
Left the vendor/subminer-yomitan branch-pin suggestion unchanged. The committed submodule SHA already controls reproducibility; adding a branch would only affect update ergonomics and was not required to address a concrete defect.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Validated Claude's PR #15 review summary against the current branch and applied the actionable fixes. In `.github/workflows/release.yml`, the release `quality-gate` job now restores the dependency cache before installation, no longer installs twice, and runs `bun run typecheck` before the fast test suite to match CI expectations. In `.gitmodules`, removed the duplicate `vendor/yomitan-jlpt-vocab` stanza with the conflicting duplicate path. Through Backlog MCP, updated `TASK-101` references from an absolute local path to repo-relative paths so the task metadata is portable across contributors.
Verification: `git diff --check`, `git config -f .gitmodules --get-regexp '^submodule\..*\.path$'`, `bun run typecheck`, and `bun run test:fast` all passed. `bun run format:check` still fails on many pre-existing unrelated files already present on the branch, including multiple backlog task files and existing source/docs files; this review patch did not attempt a repo-wide formatting sweep.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -0,0 +1,59 @@
---
id: TASK-113
title: Scope make pretty to maintained source files
status: Done
assignee:
- codex
created_date: '2026-03-08 00:20'
updated_date: '2026-03-08 00:22'
labels:
- tooling
- formatting
dependencies: []
references:
- Makefile
- package.json
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Change the `make pretty` workflow so it formats only the maintained source/config files we intentionally keep under Prettier, instead of sweeping backlog/docs/generated content across the whole repository.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `make pretty` formats only the approved maintained source/config paths
- [x] #2 The allowlist is reusable for check/write flows instead of duplicating path logic
- [x] #3 Verification shows the scoped formatting command targets the intended files without touching backlog or vendored content
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Inspect current Prettier config/ignore behavior and keep the broad repo-wide format command unchanged.
2. Add a reusable scoped Prettier script that targets maintained source/config paths only.
3. Update `make pretty` to call the scoped script.
4. Verify the scoped command resolves only intended files and does not traverse backlog or vendor paths.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
User approved the allowlist approach: keep repo-wide `format` intact, make `make pretty` use a maintained-path formatter scope.
Added `scripts/prettier-scope.sh` as the single allowlist for scoped Prettier paths and wired `format:src` / `format:check:src` to it.
Updated `make pretty` to call `bun run format:src`. Verified with `make -n pretty` and shell tracing that the helper only targets the maintained allowlist and does not traverse `backlog/` or `vendor/`.
Excluded `Makefile` and `.prettierignore` from the allowlist after verification showed Prettier cannot infer parsers for them.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Scoped the repo's day-to-day formatting entrypoint without changing the existing broad repo-wide Prettier scripts. Added `scripts/prettier-scope.sh` as the shared allowlist for maintained source/config paths (`.github`, `build`, `launcher`, `scripts`, `src`, plus selected root JSON config files), added `format:src` and `format:check:src` in `package.json`, and updated `make pretty` to run the scoped formatter.
Verification: `make -n pretty` now resolves to `bun run format:src`. `bash -n scripts/prettier-scope.sh` passed, and shell-traced `bash -x scripts/prettier-scope.sh --check` confirmed the exact allowlist passed to Prettier. `bun run format:check:src` fails only because existing files inside the allowed source scope are not currently formatted; it no longer touches `backlog/` or `vendor/`.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -0,0 +1,62 @@
---
id: TASK-114
title: Fix failing CI checks on PR 15
status: Done
assignee:
- codex
created_date: '2026-03-08 00:34'
updated_date: '2026-03-08 00:37'
labels:
- ci
- test
dependencies: []
references:
- src/renderer/subtitle-render.test.ts
- src/renderer/style.css
- .github/workflows/ci.yml
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Investigate the failing GitHub Actions CI run for PR #15 on branch `yomitan-fork`, fix the underlying test or code regression, and verify the affected local test/CI lane passes.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Identified the concrete failing CI job and captured the relevant failure context
- [x] #2 Implemented the minimal code or test change needed to resolve the CI failure
- [x] #3 Verified the affected local test target and the broader fast CI test lane pass
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Inspect the failing GitHub Actions run and confirm the exact failing test/assertion.
2. Reproduce the failing renderer stylesheet test locally and compare the assertion against current CSS.
3. Apply the minimal test or stylesheet fix needed to restore the intended hover/selection behavior.
4. Re-run the targeted renderer test, then re-run `bun run test` to verify the fast CI lane is green.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
GitHub Actions run 22810400921 failed in job build-test-audit, step `Test suite (source)`, with a single failing test: `JLPT CSS rules use underline-only styling in renderer stylesheet` in src/renderer/subtitle-render.test.ts.
Reproduced the failing test locally with `bun test src/renderer/subtitle-render.test.ts`. The failure was a brittle stylesheet assertion, not a renderer behavior regression.
Updated the renderer stylesheet test helper to split selectors safely across `:is(...)` commas and normalize multiline selector whitespace, then switched the failing hover/JLPT assertions to inspect extracted rule blocks instead of matching the entire CSS file text.
Verification passed with `bun test src/renderer/subtitle-render.test.ts` and `bun run test`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Investigated GitHub Actions CI run `22810400921` for PR #15 and confirmed the only failing job was `build-test-audit`, step `Test suite (source)`, with a single failure in `src/renderer/subtitle-render.test.ts` (`JLPT CSS rules use underline-only styling in renderer stylesheet`).
The renderer CSS itself was still correct; the regression was in the test helper. `extractClassBlock` was splitting selector lists on every comma, which breaks selectors containing `:is(...)`, and the affected assertions fell back to brittle whole-file regex matching against a multiline selector. Fixed the test by teaching the helper to split selectors only at top-level commas, normalizing selector whitespace around multiline `:not(...)` / `:is(...)` clauses, and asserting on extracted rule blocks for the plain-word hover and JLPT-only hover/selection rules.
Verification: `bun test src/renderer/subtitle-render.test.ts` passed, and `bun run test` passed end to end (the same fast lane that failed in CI).
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -0,0 +1,59 @@
---
id: TASK-115
title: Refresh subminer-docs contributor docs for current repo workflow
status: Done
assignee:
- codex
created_date: '2026-03-08 00:40'
updated_date: '2026-03-08 00:42'
labels:
- docs
dependencies: []
references:
- ../subminer-docs/development.md
- ../subminer-docs/README.md
- Makefile
- package.json
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Update the sibling `subminer-docs` repo so contributor/development docs match the current SubMiner repo workflow after the docs split and recent tooling changes, including removing stale in-repo docs build steps and documenting the scoped formatting command.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Contributor docs in `subminer-docs` no longer reference stale in-repo docs build commands for the app repo
- [x] #2 Contributor docs mention the current scoped formatting workflow (`make pretty` / `format:src`) where relevant
- [x] #3 Removed stale or no-longer-needed instructions that no longer match the current repo layout
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Inspect `subminer-docs` for contributor/development instructions that drifted after the docs repo split and recent tooling changes.
2. Update contributor docs to remove stale app-repo docs commands and document the current scoped formatting workflow.
3. Verify the modified docs page and build the docs site from the sibling docs repo when local dependencies are available.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Detected concrete doc drift in `subminer-docs/development.md`: stale in-repo docs build commands and no mention of the scoped `make pretty` formatter.
Updated `../subminer-docs/development.md` to remove stale app-repo docs build steps from the local gate, document `make pretty` / `format:check:src`, and point docs-site work to the sibling docs repo explicitly.
Installed docs repo dependencies locally with `bun install` and verified the docs site with `bun run docs:build` in `../subminer-docs`.
Did not change `../subminer-docs/README.md`; it was already accurate for the docs repo itself.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Refreshed the contributor/development docs in the sibling `subminer-docs` repo to match the current SubMiner workflow. In `development.md`, removed the stale app-repo `bun run docs:build` step from the local CI-equivalent gate, added an explicit note to run docs builds from `../subminer-docs` when docs change, documented the scoped formatting workflow (`make pretty` and `bun run format:check:src`), and replaced the old in-repo `make docs*` instructions with the correct sibling-repo `bun run docs:*` commands. Also updated the Makefile reference to include `make pretty` and removed the obsolete `make docs-dev` entry.
Verification: installed docs repo dependencies with `bun install` in `../subminer-docs` and ran `bun run docs:build` successfully. Left `README.md` unchanged because it was already accurate for the standalone docs repo.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -0,0 +1,53 @@
---
id: TASK-116
title: Audit branch commits for remaining subminer-docs updates
status: Done
assignee:
- codex
created_date: '2026-03-08 00:46'
updated_date: '2026-03-08 00:48'
labels:
- docs
dependencies: []
references:
- ../subminer-docs/installation.md
- ../subminer-docs/troubleshooting.md
- src/core/services/yomitan-extension-paths.ts
- scripts/build-yomitan.mjs
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Review recent `yomitan-fork` commits against the sibling `subminer-docs` repo, identify any concrete documentation drift that remains after the earlier contributor-doc updates, and patch the docs for behavior/tooling changes that are now outdated or misleading.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Reviewed recent branch commits for user-facing or contributor-facing changes that may require docs updates
- [x] #2 Updated `subminer-docs` pages where branch changes introduced concrete doc drift
- [x] #3 Verified the docs site still builds after the updates
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Review branch commit themes against `subminer-docs` and identify only concrete drift introduced by recent workflow/runtime changes.
2. Patch docs for the Yomitan submodule build workflow, updated source-build prerequisites, and current runtime Yomitan search paths/manual fallback path.
3. Rebuild the docs site to verify the updated pages render cleanly.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Concrete remaining drift after commit audit: installation/development docs still understate the Node/npm + submodule requirements for the Yomitan build flow, and troubleshooting still points at obsolete `vendor/yomitan` / `extensions/yomitan` paths.
Audited branch commits against subminer-docs coverage. Existing docs already cover first-run setup, texthooker startup/annotated websocket config, AniList merged character dictionaries, configurable collapsible sections, and subtitle name highlighting. Patched remaining drift around source-build prerequisites and Yomitan build/install paths in installation.md, development.md, and troubleshooting.md. Verified with `bun run docs:build` in ../subminer-docs.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Audited branch commits for missing documentation updates in ../subminer-docs. Updated installation, development, and troubleshooting docs to match the current Yomitan submodule build flow, source-build prerequisites, and runtime extension search/manual fallback paths. Confirmed other recent branch features were already documented and rebuilt the docs site successfully.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -1,28 +0,0 @@
# Anki Integration
read_when:
- changing `src/anki-integration.ts`
- changing Anki transport/config hot-reload behavior
- tracing note update, field grouping, or proxy ownership
## Ownership
- `src/anki-integration.ts`: thin facade; wires dependencies; exposes public Anki API used by runtime/services.
- `src/anki-integration/runtime.ts`: normalized config state, polling-vs-proxy transport lifecycle, runtime config patch handling.
- `src/anki-integration/card-creation.ts`: sentence/audio card creation and clipboard update flow.
- `src/anki-integration/note-update-workflow.ts`: enrich newly added notes.
- `src/anki-integration/field-grouping.ts`: preview/build helpers for Kiku field grouping.
- `src/anki-integration/field-grouping-workflow.ts`: auto/manual merge execution.
- `src/anki-integration/anki-connect-proxy.ts`: local proxy transport for post-add enrichment.
- `src/anki-integration/known-word-cache.ts`: known-word cache lifecycle and persistence.
## Refactor seam
`AnkiIntegrationRuntime` owns the cluster that previously mixed:
- config normalization/defaulting
- polling vs proxy startup/shutdown
- transport restart decisions during runtime patches
- known-word cache lifecycle toggles tied to config changes
Keep new orchestration work in `runtime.ts` when it changes process-level Anki state. Keep note/card behavior in the workflow/service modules.

View File

@@ -1,50 +0,0 @@
# Character Name Gating Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Disable subtitle character-name lookup/highlighting when the AniList character dictionary feature is disabled, while keeping tokenization and all other annotations working.
**Architecture:** Gate `getNameMatchEnabled` at the runtime-deps boundary used by subtitle tokenization. Keep the tokenizer pipeline intact and only suppress character-name metadata requests when `anilist.characterDictionary.enabled` is false, regardless of `subtitleStyle.nameMatchEnabled`.
**Tech Stack:** TypeScript, Bun test runner, Electron main/runtime wiring.
---
### Task 1: Add runtime gating coverage
**Files:**
- Modify: `src/main/runtime/subtitle-tokenization-main-deps.test.ts`
**Step 1: Write the failing test**
Add a test proving `getNameMatchEnabled()` resolves to `false` when `getCharacterDictionaryEnabled()` is `false` even if `getNameMatchEnabled()` is `true`.
**Step 2: Run test to verify it fails**
Run: `bun test src/main/runtime/subtitle-tokenization-main-deps.test.ts`
Expected: FAIL because the deps builder does not yet combine the two flags.
### Task 2: Implement minimal runtime gate
**Files:**
- Modify: `src/main/runtime/subtitle-tokenization-main-deps.ts`
- Modify: `src/main.ts`
**Step 3: Write minimal implementation**
Add `getCharacterDictionaryEnabled` to the main handler deps and make the built `getNameMatchEnabled` return true only when both the subtitle setting and the character dictionary setting are enabled.
**Step 4: Run tests to verify green**
Run: `bun test src/main/runtime/subtitle-tokenization-main-deps.test.ts`
Expected: PASS.
### Task 3: Verify no regressions in related tokenization seams
**Files:**
- Modify: none unless failures reveal drift
**Step 5: Run focused verification**
Run: `bun test src/core/services/subtitle-processing-controller.test.ts src/main/runtime/subtitle-tokenization-main-deps.test.ts`
Expected: PASS.

View File

@@ -1,155 +0,0 @@
# Immersion SQLite Verification Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Make the SQLite-backed immersion tracking persistence tests visible in the repo's verification surface and reproducible through at least one documented automated command.
**Architecture:** Keep the existing Bun fast lane intact for routine local verification, but add an explicit SQLite verification lane that runs the database-backed immersion tests under a runtime with `node:sqlite` support. Surface unsupported-runtime behavior clearly in the source tests and contributor docs so skipped or omitted coverage is no longer mistaken for a fully green persistence lane.
**Tech Stack:** TypeScript, Bun scripts in `package.json`, Node's built-in `node:test` and `node:sqlite`, GitHub Actions workflows, Markdown docs in `README.md`.
---
### Task 1: Audit and expose the SQLite-backed immersion test surface
**Files:**
- Modify: `src/core/services/immersion-tracker-service.test.ts`
- Modify: `src/core/services/immersion-tracker/storage-session.test.ts`
- Reference: `src/main/runtime/registry.test.ts`
**Step 1: Write the failing test**
Refactor the SQLite-gated immersion tests so missing `node:sqlite` support is reported with an explicit skip reason instead of a silent top-level `test.skip` alias.
**Step 2: Run test to verify it fails**
Run: `bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts`
Expected: the current output shows generic skips or hides the storage-session suite from normal scripted verification, which is too opaque for contributors.
**Step 3: Write minimal implementation**
Mirror the `src/main/runtime/registry.test.ts` pattern: add a helper that either loads `DatabaseSync` or skips with a message like `requires node:sqlite support in this runtime`, then wrap each SQLite-backed test through that helper.
**Step 4: Run test to verify it passes**
Run: `bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts`
Expected: PASS, with explicit skip messages in unsupported runtimes.
### Task 2: Add a reproducible SQLite verification command
**Files:**
- Modify: `package.json`
- Reference: `src/core/services/immersion-tracker-service.test.ts`
- Reference: `src/core/services/immersion-tracker/storage-session.test.ts`
**Step 1: Write the failing test**
Add a dedicated script contract for the SQLite-backed immersion verification lane so both persistence-heavy suites are intentionally grouped and runnable together.
**Step 2: Run test to verify it fails**
Run: `bun run test:immersion:sqlite`
Expected: FAIL because no such reproducible lane exists yet.
**Step 3: Write minimal implementation**
Update `package.json` with explicit scripts for the SQLite lane. Prefer a command shape that actually executes the built JS tests under Node with `node:sqlite` support, for example:
- `test:immersion:sqlite:dist`: `node --test dist/core/services/immersion-tracker-service.test.js dist/core/services/immersion-tracker/storage-session.test.js`
- `test:immersion:sqlite`: `bun run build && bun run test:immersion:sqlite:dist`
If build cost or runtime behavior requires a small adjustment, keep the core contract the same: one documented command must run both SQLite-backed immersion suites end-to-end.
**Step 4: Run test to verify it passes**
Run: `bun run test:immersion:sqlite`
Expected: PASS in a Node runtime with `node:sqlite`, executing both persistence suites without Bun-only skips.
### Task 3: Wire the SQLite lane into automated verification
**Files:**
- Modify: `.github/workflows/ci.yml`
- Modify: `.github/workflows/release.yml`
- Reference: `package.json`
**Step 1: Write the failing test**
Add the new SQLite immersion lane to the repo's automated verification so contributors and CI can rely on a real persistence check rather than the Bun fast lane alone.
**Step 2: Run test to verify it fails**
Run: `bun run test:immersion:sqlite`
Expected: local command may pass, but CI/release workflows still omit the lane entirely.
**Step 3: Write minimal implementation**
Update both workflows to provision a Node version with `node:sqlite` support before the SQLite lane runs, then execute `bun run test:immersion:sqlite` in the quality gate after the bundle build produces `dist/**` test files.
**Step 4: Run test to verify it passes**
Run: `bun run test:immersion:sqlite`
Expected: PASS locally, and workflow definitions clearly show the SQLite lane as part of automated verification.
### Task 4: Document contributor-facing prerequisites and commands
**Files:**
- Modify: `README.md`
- Reference: `package.json`
- Reference: `.github/workflows/ci.yml`
**Step 1: Write the failing test**
Extend the verification docs so contributors can discover the SQLite lane, know why the Bun source lane may skip those cases, and understand which command reproduces the persistence coverage.
**Step 2: Run test to verify it fails**
Run: `grep -n "test:immersion:sqlite" README.md`
Expected: FAIL because the dedicated immersion SQLite lane is undocumented.
**Step 3: Write minimal implementation**
Update `README.md` to document:
- the Bun fast/default lane versus the SQLite persistence lane
- the `node:sqlite` prerequisite for the reproducible command
- that the dedicated lane covers session persistence/finalization behavior beyond seam tests
**Step 4: Run test to verify it passes**
Run: `grep -n "test:immersion:sqlite" README.md && grep -n "node:sqlite" README.md`
Expected: PASS, with clear contributor guidance.
### Task 5: Verify persistence coverage end-to-end
**Files:**
- Test: `src/core/services/immersion-tracker-service.test.ts`
- Test: `src/core/services/immersion-tracker/storage-session.test.ts`
- Reference: `README.md`
- Reference: `package.json`
**Step 1: Write the failing test**
Prove the final lane exercises real DB-backed persistence/finalization paths, not just the seam tests.
**Step 2: Run test to verify it fails**
Run: `bun run test:immersion:sqlite`
Expected: before implementation, the command does not exist or does not cover both SQLite-backed suites.
**Step 3: Write minimal implementation**
Keep the dedicated lane pointed at both existing SQLite-backed test files so it covers representative finalization and persistence behavior such as:
- `destroy finalizes active session and persists final telemetry`
- `start/finalize session updates ended_at and status`
- `executeQueuedWrite inserts event and telemetry rows`
**Step 4: Run test to verify it passes**
Run: `bun run test:immersion:sqlite`
Expected: PASS, with those DB-backed persistence/finalization cases executing successfully under Node.

View File

@@ -1,92 +0,0 @@
# Merged Character Dictionary Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Replace per-anime character dictionary imports with one merged Yomitan dictionary driven by MRU usage retention.
**Architecture:** Persist normalized per-media character dictionary snapshots locally, maintain MRU retained media ids in auto-sync state, and rebuild a single merged Yomitan zip only when the retained set changes. Keep external AniList fetches only for media without a local snapshot; normal revisits stay local.
**Tech Stack:** TypeScript, Bun test, Node fs/path, existing Yomitan zip generation helpers.
---
### Task 1: Lock in merged auto-sync behavior
**Files:**
- Modify: `src/main/runtime/character-dictionary-auto-sync.test.ts`
- Test: `src/main/runtime/character-dictionary-auto-sync.test.ts`
**Step 1: Write the failing test**
Add tests for:
- single merged dictionary title/import replacing per-media imports
- MRU reorder causing rebuild only when order changes
- unchanged revisit skipping rebuild/import
- capped retained set evicting least-recently-used media
**Step 2: Run test to verify it fails**
Run: `bun test src/main/runtime/character-dictionary-auto-sync.test.ts`
Expected: FAIL on old per-media import assumptions / missing merged behavior
**Step 3: Write minimal implementation**
Update auto-sync runtime to track retained media ids and merged revision/hash, call merged zip builder, and replace one imported Yomitan dictionary.
**Step 4: Run test to verify it passes**
Run: `bun test src/main/runtime/character-dictionary-auto-sync.test.ts`
Expected: PASS
### Task 2: Add snapshot + merged-zip runtime support
**Files:**
- Modify: `src/main/character-dictionary-runtime.ts`
- Modify: `src/main/character-dictionary-runtime.test.ts`
- Test: `src/main/character-dictionary-runtime.test.ts`
**Step 1: Write the failing test**
Add tests for:
- saving/loading normalized per-media snapshots without per-media zip cache
- building merged zip from retained media snapshots with stable dictionary title
- preserving images/terms from multiple media in merged output
**Step 2: Run test to verify it fails**
Run: `bun test src/main/character-dictionary-runtime.test.ts`
Expected: FAIL because snapshot/merged APIs do not exist yet
**Step 3: Write minimal implementation**
Refactor dictionary runtime to expose snapshot generation/loading and merged zip building from stored metadata/images.
**Step 4: Run test to verify it passes**
Run: `bun test src/main/character-dictionary-runtime.test.ts`
Expected: PASS
### Task 3: Wire app/runtime config and docs
**Files:**
- Modify: `src/main.ts`
- Modify: `src/config/definitions/options-integrations.ts`
- Modify: `README.md`
**Step 1: Write the failing test**
Add or update tests if needed for new dependency wiring / docs-adjacent config description expectations.
**Step 2: Run test to verify it fails**
Run: `bun test src/main/runtime/character-dictionary-auto-sync.test.ts src/main/character-dictionary-runtime.test.ts`
Expected: FAIL until wiring matches merged flow
**Step 3: Write minimal implementation**
Swap app wiring to new snapshot + merged build API, update config/docs text from TTL semantics to usage-based merged retention.
**Step 4: Run test to verify it passes**
Run: `bun test src/main/runtime/character-dictionary-auto-sync.test.ts src/main/character-dictionary-runtime.test.ts && bun run tsc --noEmit`
Expected: PASS

View File

@@ -1,121 +0,0 @@
# Subtitle Sync Verification Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Replace the no-op `test:subtitle` lane with real automated subtitle-sync verification that reuses the maintained subsync tests and documents the real contributor workflow.
**Architecture:** Repoint the subtitle verification command at the existing source-level subsync tests instead of inventing a second hidden suite. Add one focused ffsubsync failure-path test so the subtitle lane explicitly covers both engines plus a non-happy path, then update contributor docs to describe the dedicated subtitle lane and how it relates to `test:core`.
**Tech Stack:** TypeScript, Bun test, Node test/assert, npm package scripts, Markdown docs.
---
### Task 1: Lock subtitle lane to real subsync tests
**Files:**
- Modify: `package.json`
**Step 1: Write the failing test**
Define the intended command shape first: `test:subtitle:src` should run `src/core/services/subsync.test.ts` and `src/subsync/utils.test.ts`, `test:subtitle` should invoke that real source lane, and no placeholder echo should remain.
**Step 2: Run test to verify it fails**
Run: `bun run test:subtitle`
Expected: It performs a build and prints `Subtitle tests are currently not configured`, proving the lane is still a no-op.
**Step 3: Write minimal implementation**
Update `package.json` so:
- `test:subtitle:src` runs `bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts`
- `test:subtitle` runs the new source lane directly
- `test:subtitle:dist` is removed if it is no longer the real verification path
**Step 4: Run test to verify it passes**
Run: `bun run test:subtitle`
Expected: PASS with Bun executing the real subtitle-sync test files.
### Task 2: Add explicit ffsubsync non-happy-path coverage
**Files:**
- Modify: `src/core/services/subsync.test.ts`
- Test: `src/core/services/subsync.test.ts`
**Step 1: Write the failing test**
Add a test that runs `runSubsyncManual({ engine: 'ffsubsync' })` with a stub ffsubsync executable that exits non-zero and writes stderr, then assert:
- `result.ok === false`
- `result.message` starts with `ffsubsync synchronization failed`
- the failure message includes command details surfaced to the user
**Step 2: Run test to verify it fails**
Run: `bun test src/core/services/subsync.test.ts`
Expected: FAIL because ffsubsync failure propagation is not asserted yet.
**Step 3: Write minimal implementation**
Keep production code unchanged unless the new test exposes a real bug. If needed, tighten failure assertions or message propagation in `src/core/services/subsync.ts` without changing successful behavior.
**Step 4: Run test to verify it passes**
Run: `bun test src/core/services/subsync.test.ts`
Expected: PASS with both alass and ffsubsync paths covered, including a non-happy path.
### Task 3: Make contributor docs match the real verification path
**Files:**
- Modify: `README.md`
- Modify: `package.json`
**Step 1: Write the failing test**
Use the repository state as the failure signal: README currently advertises subtitle sync as a feature but does not tell contributors that `bun run test:subtitle` is the real verification lane.
**Step 2: Run test to verify it fails**
Run: `bun run test:subtitle && bun test src/subsync/utils.test.ts`
Expected: Tests pass, but docs still do not explain the lane; this is the remaining acceptance-criteria gap.
**Step 3: Write minimal implementation**
Update `README.md` with a short contributor-facing verification note that:
- points to `bun run test:subtitle` for subtitle-sync coverage
- states that the lane reuses the maintained subsync tests already included in broader core coverage
- avoids implying there is a separate hidden subtitle test harness beyond those tests
**Step 4: Run test to verify it passes**
Run: `bun run test:subtitle`
Expected: PASS, with docs and scripts now aligned around the same subtitle verification strategy.
### Task 4: Verify matrix integration stays clean
**Files:**
- Modify: `package.json` (only if Task 1/3 exposed cleanup needs)
**Step 1: Write the failing test**
Treat duplication as the failure condition: confirm the dedicated subtitle lane reuses the same maintained files already present in `test:core:src` rather than creating a second divergent suite.
**Step 2: Run test to verify it fails**
Run: `bun run test:subtitle && bun run test:core:src`
Expected: If file lists diverge unexpectedly, this review step exposes it before handoff.
**Step 3: Write minimal implementation**
If needed, do the smallest script cleanup necessary so subtitle coverage remains explicit without hiding or duplicating existing core coverage.
**Step 4: Run test to verify it passes**
Run: `bun run test:subtitle && bun run test:core:src`
Expected: PASS, confirming the dedicated lane and the broader core suite agree on subtitle coverage.

View File

@@ -1,169 +0,0 @@
# Testing Workflow Test Matrix Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Make the standard test commands reflect the maintained test surface so newly added tests are discovered automatically or intentionally documented outside the default lane.
**Architecture:** Replace the current hand-maintained file allowlists in `package.json` with directory-based Bun test lanes that map to maintained test surfaces. Keep the default developer lane fast, move slower or environment-specific checks into explicit commands, and document the resulting matrix in `README.md` so contributors know exactly which command to run.
**Tech Stack:** TypeScript, Bun test, npm-style package scripts in `package.json`, Markdown docs in `README.md`.
---
### Task 1: Lock in the desired script matrix with failing tests/audit checks
**Files:**
- Modify: `package.json`
- Test: `package.json`
- Reference: `src/main-entry-runtime.test.ts`
- Reference: `src/anki-integration/anki-connect-proxy.test.ts`
- Reference: `src/main/runtime/registry.test.ts`
**Step 1: Write the failing test**
Add a new script structure in `package.json` expectations by editing the script map so these lanes exist conceptually:
- `test:fast` for default fast verification
- `test:full` for the maintained source test surface
- `test:env` for environment-specific checks
The fast lane should stay selective and intentional. The full lane should use directory-based discovery rather than file-by-file allowlists, with representative coverage from:
- `src/main-entry-runtime.test.ts`
- `src/anki-integration/**/*.test.ts`
- `src/main/**/*.test.ts`
- `launcher/**/*.test.ts`
**Step 2: Run test to verify it fails**
Run: `bun run test:full`
Expected: FAIL because `test:full` does not exist yet, and previously omitted maintained tests are still outside the standard matrix.
**Step 3: Write minimal implementation**
Update `package.json` scripts so:
- `test` points at `test:fast`
- `test:fast` runs the fast default lane only
- `test:full` runs directory-based maintained suites instead of file allowlists
- `test:env` runs environment-specific verification (for example launcher/plugin and sqlite-gated suites)
- subsystem scripts use stable path globs or directory arguments so new tests are discovered automatically
Prefer commands like these, adjusted only as needed for Bun behavior in this repo:
- `bun test src/config/**/*.test.ts`
- `bun test src/{cli,core,renderer,subtitle,subsync,main,anki-integration}/*.test.ts ...` only if Bun cannot take the broader directory directly
- `bun test launcher/**/*.test.ts`
Do not keep large hand-maintained file enumerations for maintained unit/integration lanes.
**Step 4: Run test to verify it passes**
Run: `bun run test:full`
Expected: PASS, including automated execution of representative tests that were previously omitted from the standard matrix.
### Task 2: Separate environment-specific verification from the maintained default/full lanes
**Files:**
- Modify: `package.json`
- Test: `src/main/runtime/registry.test.ts`
- Test: `launcher/smoke.e2e.test.ts`
- Test: `src/core/services/immersion-tracker-service.test.ts`
**Step 1: Write the failing test**
Refine the package scripts so environment-specific checks are explicitly grouped outside the default fast lane. Treat these as the primary environment-specific examples unless repo behavior proves a better split during execution:
- launcher smoke/plugin checks that rely on local process or Lua execution
- sqlite-dependent checks that may skip when `node:sqlite` is unavailable
**Step 2: Run test to verify it fails**
Run: `bun run test:env`
Expected: FAIL because the environment-specific lane is not defined yet.
**Step 3: Write minimal implementation**
Add explicit environment-specific scripts in `package.json`, such as:
- a launcher/plugin lane that runs `launcher/smoke.e2e.test.ts` plus `lua scripts/test-plugin-start-gate.lua`
- a sqlite lane for tests that require `node:sqlite` support or otherwise need environment notes
- an aggregate `test:env` command that runs all environment-specific lanes
Keep these lanes documented and reproducible rather than silently excluded.
**Step 4: Run test to verify it passes**
Run: `bun run test:env`
Expected: PASS in supported environments, or clear documented skip behavior where the tests themselves intentionally gate on missing runtime support.
### Task 3: Document contributor-facing test commands and matrix
**Files:**
- Modify: `README.md`
- Reference: `package.json`
**Step 1: Write the failing test**
Add a contributor-focused testing section requirement in `README.md` expectations:
- fast verification command
- full verification command
- environment-specific verification command
- plain-language explanation of which suites each lane covers and why
**Step 2: Run test to verify it fails**
Run: `grep -n "Testing" README.md`
Expected: no contributor testing matrix section exists yet.
**Step 3: Write minimal implementation**
Update `README.md` with a concise `Testing` section that documents:
- `bun run test` / `bun run test:fast` for fast local verification
- `bun run test:full` for the maintained source test surface
- `bun run test:env` for environment-specific verification
- any important notes about sqlite-gated tests and launcher/plugin checks
Keep the matrix concrete and reproducible.
**Step 4: Run test to verify it passes**
Run: `grep -n "Testing" README.md && grep -n "test:full" README.md && grep -n "test:env" README.md`
Expected: PASS with the new contributor-facing matrix present.
### Task 4: Verify representative omitted suites now belong to automated lanes
**Files:**
- Test: `src/main-entry-runtime.test.ts`
- Test: `src/anki-integration/anki-connect-proxy.test.ts`
- Test: `src/main/runtime/registry.test.ts`
- Reference: `package.json`
- Reference: `README.md`
**Step 1: Write the failing test**
Use targeted command checks to prove these previously omitted surfaces are now in the matrix:
- entry/runtime: `src/main-entry-runtime.test.ts`
- Anki integration: `src/anki-integration/anki-connect-proxy.test.ts`
- main runtime: `src/main/runtime/registry.test.ts`
**Step 2: Run test to verify it fails**
Run: `bun run test:full src/main-entry-runtime.test.ts`
Expected: either unsupported invocation or evidence that the current matrix still does not include these surfaces automatically.
**Step 3: Write minimal implementation**
Adjust the final script paths/globs until the full matrix includes those representative surfaces without file-by-file script maintenance.
**Step 4: Run test to verify it passes**
Run: `bun test src/main-entry-runtime.test.ts src/anki-integration/anki-connect-proxy.test.ts src/main/runtime/registry.test.ts && bun run test:fast && bun run test:full`
Expected: PASS, with at least one representative test from each required surface executing through the documented automated lanes.

View File

@@ -16,7 +16,11 @@ import { generateYoutubeSubtitles } from '../youtube.js';
import type { Args } from '../types.js';
import type { LauncherCommandContext } from './context.js';
import { ensureLauncherSetupReady } from '../setup-gate.js';
import { getDefaultConfigDir, getSetupStatePath, readSetupState } from '../../src/shared/setup-state.js';
import {
getDefaultConfigDir,
getSetupStatePath,
readSetupState,
} from '../../src/shared/setup-state.js';
const SETUP_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
const SETUP_POLL_INTERVAL_MS = 500;

View File

@@ -4,6 +4,13 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn, spawnSync } from 'node:child_process';
import {
createDefaultSetupState,
getDefaultConfigDir,
getSetupStatePath,
readSetupState,
writeSetupState,
} from '../src/shared/setup-state.js';
type RunResult = {
status: number | null;
@@ -58,6 +65,13 @@ function createSmokeCase(name: string): SmokeCase {
`socket_path=${socketPath}\n`,
);
const configDir = getDefaultConfigDir({ xdgConfigHome, homeDir });
const setupState = createDefaultSetupState();
setupState.status = 'completed';
setupState.completedAt = '2026-03-07T00:00:00.000Z';
setupState.completionSource = 'user';
writeSetupState(getSetupStatePath(configDir), setupState);
const fakeMpvLogPath = path.join(artifactsDir, 'fake-mpv.log');
const fakeAppLogPath = path.join(artifactsDir, 'fake-app.log');
const fakeAppStartLogPath = path.join(artifactsDir, 'fake-app-start.log');
@@ -224,6 +238,22 @@ async function waitForJsonLines(
}
}
test('launcher smoke fixture seeds completed setup state', () => {
const smokeCase = createSmokeCase('setup-state');
try {
const configDir = getDefaultConfigDir({
xdgConfigHome: smokeCase.xdgConfigHome,
homeDir: smokeCase.homeDir,
});
const statePath = getSetupStatePath(configDir);
assert.equal(readSetupState(statePath)?.status, 'completed');
} finally {
fs.rmSync(smokeCase.root, { recursive: true, force: true });
fs.rmSync(smokeCase.socketDir, { recursive: true, force: true });
}
});
test('launcher mpv status returns ready when socket is connectable', async () => {
await withSmokeCase('mpv-status', async (smokeCase) => {
const env = makeTestEnv(smokeCase);

View File

@@ -1,6 +1,6 @@
{
"name": "subminer",
"version": "0.3.0",
"version": "0.4.0",
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
"packageManager": "bun@1.3.5",
"main": "dist/main-entry.js",
@@ -8,21 +8,24 @@
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
"typecheck:watch": "tsc --watch --preserveWatchOutput -p tsconfig.typecheck.json",
"get-frequency": "bun run scripts/get_frequency.ts --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
"get-frequency:electron": "bun build scripts/get_frequency.ts --format=cjs --target=node --outfile dist/scripts/get_frequency.js --external electron && electron dist/scripts/get_frequency.js --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
"get-frequency:electron": "bun run build:yomitan && bun build scripts/get_frequency.ts --format=cjs --target=node --outfile dist/scripts/get_frequency.js --external electron && electron dist/scripts/get_frequency.js --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
"test-yomitan-parser": "bun run scripts/test-yomitan-parser.ts",
"test-yomitan-parser:electron": "bun build scripts/test-yomitan-parser.ts --format=cjs --target=node --outfile dist/scripts/test-yomitan-parser.js --external electron && electron dist/scripts/test-yomitan-parser.js",
"build": "tsc -p tsconfig.json && bun run build:renderer && cp src/renderer/index.html src/renderer/style.css dist/renderer/ && cp -r src/renderer/fonts dist/renderer/ && bash scripts/build-macos-helper.sh",
"test-yomitan-parser:electron": "bun run build:yomitan && bun build scripts/test-yomitan-parser.ts --format=cjs --target=node --outfile dist/scripts/test-yomitan-parser.js --external electron && electron dist/scripts/test-yomitan-parser.js",
"build:yomitan": "node scripts/build-yomitan.mjs",
"build": "bun run build:yomitan && tsc -p tsconfig.json && bun run build:renderer && cp src/renderer/index.html src/renderer/style.css dist/renderer/ && cp -r src/renderer/fonts dist/renderer/ && bash scripts/build-macos-helper.sh",
"build:renderer": "esbuild src/renderer/renderer.ts --bundle --platform=browser --format=esm --target=es2022 --outfile=dist/renderer/renderer.js --sourcemap",
"format": "prettier --write .",
"format:check": "prettier --check .",
"format:src": "bash scripts/prettier-scope.sh --write",
"format:check:src": "bash scripts/prettier-scope.sh --check",
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts",
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js",
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua",
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/setup-gate.test.ts",
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/setup-gate.test.ts",
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
@@ -117,7 +120,7 @@
],
"extraResources": [
{
"from": "vendor/yomitan",
"from": "build/yomitan",
"to": "yomitan"
},
{

144
scripts/build-yomitan.mjs Normal file
View File

@@ -0,0 +1,144 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(dirname, '..');
const submoduleDir = path.join(repoRoot, 'vendor', 'subminer-yomitan');
const submodulePackagePath = path.join(submoduleDir, 'package.json');
const submodulePackageLockPath = path.join(submoduleDir, 'package-lock.json');
const buildOutputDir = path.join(repoRoot, 'build', 'yomitan');
const stampPath = path.join(buildOutputDir, '.subminer-build.json');
const zipPath = path.join(submoduleDir, 'builds', 'yomitan-chrome.zip');
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const dependencyStampPath = path.join(submoduleDir, 'node_modules', '.subminer-package-lock-hash');
function run(command, args, cwd) {
execFileSync(command, args, { cwd, stdio: 'inherit' });
}
function readCommand(command, args, cwd) {
return execFileSync(command, args, { cwd, encoding: 'utf8' }).trim();
}
function readStamp() {
try {
return JSON.parse(fs.readFileSync(stampPath, 'utf8'));
} catch {
return null;
}
}
function hashFile(filePath) {
const hash = createHash('sha256');
hash.update(fs.readFileSync(filePath));
return hash.digest('hex');
}
function ensureSubmodulePresent() {
if (!fs.existsSync(submodulePackagePath)) {
throw new Error(
'Missing vendor/subminer-yomitan submodule. Run `git submodule update --init --recursive`.',
);
}
}
function getSourceState() {
const revision = readCommand('git', ['rev-parse', 'HEAD'], submoduleDir);
const dirty = readCommand('git', ['status', '--short', '--untracked-files=no'], submoduleDir);
return { revision, dirty };
}
function isBuildCurrent(force) {
if (force) {
return false;
}
if (!fs.existsSync(path.join(buildOutputDir, 'manifest.json'))) {
return false;
}
const stamp = readStamp();
if (!stamp) {
return false;
}
const currentState = getSourceState();
return stamp.revision === currentState.revision && stamp.dirty === currentState.dirty;
}
function ensureDependenciesInstalled() {
const nodeModulesDir = path.join(submoduleDir, 'node_modules');
const currentLockHash = hashFile(submodulePackageLockPath);
let installedLockHash = '';
try {
installedLockHash = fs.readFileSync(dependencyStampPath, 'utf8').trim();
} catch {}
if (!fs.existsSync(nodeModulesDir) || installedLockHash !== currentLockHash) {
run(npmCommand, ['ci'], submoduleDir);
fs.mkdirSync(nodeModulesDir, { recursive: true });
fs.writeFileSync(dependencyStampPath, `${currentLockHash}\n`, 'utf8');
}
}
function installAndBuild() {
ensureDependenciesInstalled();
run(npmCommand, ['run', 'build', '--', '--target', 'chrome'], submoduleDir);
}
function extractBuild() {
if (!fs.existsSync(zipPath)) {
throw new Error(`Expected Yomitan build artifact at ${zipPath}`);
}
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-yomitan-'));
try {
run('unzip', ['-qo', zipPath, '-d', tempDir], repoRoot);
fs.rmSync(buildOutputDir, { recursive: true, force: true });
fs.mkdirSync(path.dirname(buildOutputDir), { recursive: true });
fs.cpSync(tempDir, buildOutputDir, { recursive: true });
if (!fs.existsSync(path.join(buildOutputDir, 'manifest.json'))) {
throw new Error(`Extracted Yomitan build missing manifest.json in ${buildOutputDir}`);
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
function writeStamp() {
const state = getSourceState();
fs.writeFileSync(
stampPath,
`${JSON.stringify(
{
revision: state.revision,
dirty: state.dirty,
builtAt: new Date().toISOString(),
},
null,
2,
)}\n`,
'utf8',
);
}
function main() {
const force = process.argv.includes('--force');
ensureSubmodulePresent();
if (isBuildCurrent(force)) {
process.stdout.write(`Yomitan build current: ${buildOutputDir}\n`);
return;
}
process.stdout.write('Building Yomitan Chrome artifact...\n');
installAndBuild();
extractBuild();
writeStamp();
process.stdout.write(`Yomitan extracted to ${buildOutputDir}\n`);
}
main();

View File

@@ -4,6 +4,7 @@ import process from 'node:process';
import { createTokenizerDepsRuntime, tokenizeSubtitle } from '../src/core/services/tokenizer.js';
import { createFrequencyDictionaryLookup } from '../src/core/services/frequency-dictionary.js';
import { resolveYomitanExtensionPath as resolveBuiltYomitanExtensionPath } from '../src/core/services/yomitan-extension-paths.js';
import { MecabTokenizer } from '../src/mecab-tokenizer.js';
import type { MergedToken, FrequencyDictionaryLookup } from '../src/types.js';
@@ -94,7 +95,7 @@ function parseCliArgs(argv: string[]): CliOptions {
if (!next) {
throw new Error('Missing value for --yomitan-extension');
}
yomitanExtensionPath = path.resolve(next);
yomitanExtensionPath = next;
continue;
}
@@ -103,7 +104,7 @@ function parseCliArgs(argv: string[]): CliOptions {
if (!next) {
throw new Error('Missing value for --yomitan-user-data');
}
yomitanUserDataPath = path.resolve(next);
yomitanUserDataPath = next;
continue;
}
@@ -225,12 +226,12 @@ function parseCliArgs(argv: string[]): CliOptions {
}
if (arg.startsWith('--yomitan-extension=')) {
yomitanExtensionPath = path.resolve(arg.slice('--yomitan-extension='.length));
yomitanExtensionPath = arg.slice('--yomitan-extension='.length);
continue;
}
if (arg.startsWith('--yomitan-user-data=')) {
yomitanUserDataPath = path.resolve(arg.slice('--yomitan-user-data='.length));
yomitanUserDataPath = arg.slice('--yomitan-user-data='.length);
continue;
}
@@ -524,7 +525,10 @@ function destroyUnknownParserWindow(window: unknown): void {
}
}
async function createYomitanRuntimeState(userDataPath: string): Promise<YomitanRuntimeState> {
async function createYomitanRuntimeState(
userDataPath: string,
extensionPath?: string,
): Promise<YomitanRuntimeState> {
const state: YomitanRuntimeState = {
yomitanExt: null,
parserWindow: null,
@@ -547,6 +551,7 @@ async function createYomitanRuntimeState(userDataPath: string): Promise<YomitanR
const loadYomitanExtension = (await import('../src/core/services/yomitan-extension-loader.js'))
.loadYomitanExtension as (options: {
userDataPath: string;
extensionPath?: string;
getYomitanParserWindow: () => unknown;
setYomitanParserWindow: (window: unknown) => void;
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
@@ -556,6 +561,7 @@ async function createYomitanRuntimeState(userDataPath: string): Promise<YomitanR
const extension = await loadYomitanExtension({
userDataPath,
extensionPath,
getYomitanParserWindow: () => state.parserWindow,
setYomitanParserWindow: (window) => {
state.parserWindow = window;
@@ -589,17 +595,16 @@ async function createYomitanRuntimeStateWithSearch(
userDataPath: string,
extensionPath?: string,
): Promise<YomitanRuntimeState> {
const preferredPath = extensionPath ? path.resolve(extensionPath) : undefined;
const defaultVendorPath = path.resolve(process.cwd(), 'vendor', 'yomitan');
const candidates = [...(preferredPath ? [preferredPath] : []), defaultVendorPath];
const resolvedExtensionPath = resolveBuiltYomitanExtensionPath({
explicitPath: extensionPath,
cwd: process.cwd(),
});
const candidates = resolvedExtensionPath ? [resolvedExtensionPath] : [];
for (const candidate of candidates) {
if (!candidate) {
continue;
}
try {
if (fs.existsSync(path.join(candidate, 'manifest.json'))) {
const state = await createYomitanRuntimeState(userDataPath);
const state = await createYomitanRuntimeState(userDataPath, candidate);
if (state.available) {
return state;
}
@@ -613,7 +618,7 @@ async function createYomitanRuntimeStateWithSearch(
}
}
return createYomitanRuntimeState(userDataPath);
return createYomitanRuntimeState(userDataPath, resolvedExtensionPath ?? undefined);
}
async function getFrequencyLookup(dictionaryPath: string): Promise<FrequencyDictionaryLookup> {

View File

@@ -1,287 +1,16 @@
#!/bin/bash
#
# SubMiner - All-in-one sentence mining overlay
# Copyright (C) 2024 sudacode
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# patch-yomitan.sh - Apply Electron compatibility patches to Yomitan
#
# This script applies the necessary patches to make Yomitan work in Electron
# after upgrading to a new version. Run this after extracting a fresh Yomitan release.
#
# Usage: ./patch-yomitan.sh [yomitan_dir]
# yomitan_dir: Path to the Yomitan directory (default: vendor/yomitan)
#
set -e
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
YOMITAN_DIR="${1:-$SCRIPT_DIR/../vendor/yomitan}"
YOMITAN_MANIFEST_PATH="$YOMITAN_DIR/manifest.json"
cat <<'EOF'
patch-yomitan.sh is retired.
if [ ! -d "$YOMITAN_DIR" ]; then
echo "Error: Yomitan directory not found: $YOMITAN_DIR"
exit 1
fi
SubMiner now uses the forked source submodule at vendor/subminer-yomitan and builds the
Chromium extension artifact into build/yomitan.
if [ ! -f "$YOMITAN_MANIFEST_PATH" ]; then
echo "Error: manifest.json not found at $YOMITAN_MANIFEST_PATH"
exit 1
fi
Use:
git submodule update --init --recursive
bun run build:yomitan
echo "Patching manifest.json..."
if node - "$YOMITAN_MANIFEST_PATH" <<'PATCH_EOF'
const fs = require('node:fs');
const path = process.argv[2];
const manifest = JSON.parse(fs.readFileSync(path, 'utf8'));
const stableKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxclvOy2sunfRa2UeSV/L9xyuMR9V65z85mbKCy0XvSLUkTBXM8BnvnrDu1DHhLjpidA3cBtetVt7rzwsJSA6/CzlMmtG6L6//3MOAH5Mhng8tXXWXbuNuJobLv/7MORPqoqYKZuoL1bnUvjdrf4Pb3BBDZtHN8LcDz13gOO4dnEFQbSE4F5RQ4mIQAGMkmbmlJkwFk5I022XyX+cWm/+9VvwPuEDA1Qf7X1G+4use3hGYWVPcRb6xTp7swXsO/fP7auE51gYQD0Ht36wr32UR6lfRmsahbHOX4RLe36S8B4ee74kk5C8iCsZf2fidWmevzLk7kK0GW15pv3dpGFpPQIDAQAB';
if (manifest.key === stableKey) {
process.exit(0);
}
manifest.key = stableKey;
fs.writeFileSync(path, `${JSON.stringify(manifest, null, 4)}\n`, 'utf8');
process.exit(0);
PATCH_EOF
then
echo " - Set stable manifest key in manifest.json"
else
echo " - Failed to patch manifest.json"
exit 1
fi
echo "Patching Yomitan in: $YOMITAN_DIR"
PERMISSIONS_UTIL="$YOMITAN_DIR/js/data/permissions-util.js"
if [ ! -f "$PERMISSIONS_UTIL" ]; then
echo "Error: permissions-util.js not found at $PERMISSIONS_UTIL"
exit 1
fi
echo "Patching permissions-util.js..."
if grep -q "Electron workaround" "$PERMISSIONS_UTIL"; then
echo " - Already patched, skipping"
else
cat > "$PERMISSIONS_UTIL.tmp" << 'PATCH_EOF'
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2021-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {getFieldMarkers} from './anki-util.js';
/**
* This function returns whether an Anki field marker might require clipboard permissions.
* This is speculative and may not guarantee that the field marker actually does require the permission,
* as the custom handlebars template is not deeply inspected.
* @param {string} marker
* @returns {boolean}
*/
function ankiFieldMarkerMayUseClipboard(marker) {
switch (marker) {
case 'clipboard-image':
case 'clipboard-text':
return true;
default:
return false;
}
}
/**
* @param {chrome.permissions.Permissions} permissions
* @returns {Promise<boolean>}
*/
export function hasPermissions(permissions) {
return new Promise((resolve, reject) => {
chrome.permissions.contains(permissions, (result) => {
const e = chrome.runtime.lastError;
if (e) {
reject(new Error(e.message));
} else {
resolve(result);
}
});
});
}
/**
* @param {chrome.permissions.Permissions} permissions
* @param {boolean} shouldHave
* @returns {Promise<boolean>}
*/
export function setPermissionsGranted(permissions, shouldHave) {
return (
shouldHave ?
new Promise((resolve, reject) => {
chrome.permissions.request(permissions, (result) => {
const e = chrome.runtime.lastError;
if (e) {
reject(new Error(e.message));
} else {
resolve(result);
}
});
}) :
new Promise((resolve, reject) => {
chrome.permissions.remove(permissions, (result) => {
const e = chrome.runtime.lastError;
if (e) {
reject(new Error(e.message));
} else {
resolve(!result);
}
});
})
);
}
/**
* @returns {Promise<chrome.permissions.Permissions>}
*/
export function getAllPermissions() {
// Electron workaround - chrome.permissions.getAll() not available
return Promise.resolve({
origins: ["<all_urls>"],
permissions: ["clipboardWrite", "storage", "unlimitedStorage", "scripting", "contextMenus"]
});
}
/**
* @param {string} fieldValue
* @returns {string[]}
*/
export function getRequiredPermissionsForAnkiFieldValue(fieldValue) {
const markers = getFieldMarkers(fieldValue);
for (const marker of markers) {
if (ankiFieldMarkerMayUseClipboard(marker)) {
return ['clipboardRead'];
}
}
return [];
}
/**
* @param {chrome.permissions.Permissions} permissions
* @param {import('settings').ProfileOptions} options
* @returns {boolean}
*/
export function hasRequiredPermissionsForOptions(permissions, options) {
const permissionsSet = new Set(permissions.permissions);
if (!permissionsSet.has('nativeMessaging') && (options.parsing.enableMecabParser || options.general.enableYomitanApi)) {
return false;
}
if (!permissionsSet.has('clipboardRead')) {
if (options.clipboard.enableBackgroundMonitor || options.clipboard.enableSearchPageMonitor) {
return false;
}
const fieldsList = options.anki.cardFormats.map((cardFormat) => cardFormat.fields);
for (const fields of fieldsList) {
for (const {value: fieldValue} of Object.values(fields)) {
const markers = getFieldMarkers(fieldValue);
for (const marker of markers) {
if (ankiFieldMarkerMayUseClipboard(marker)) {
return false;
}
}
}
}
}
return true;
}
PATCH_EOF
mv "$PERMISSIONS_UTIL.tmp" "$PERMISSIONS_UTIL"
echo " - Patched successfully"
fi
OPTIONS_SCHEMA="$YOMITAN_DIR/data/schemas/options-schema.json"
if [ ! -f "$OPTIONS_SCHEMA" ]; then
echo "Error: options-schema.json not found at $OPTIONS_SCHEMA"
exit 1
fi
echo "Patching options-schema.json..."
if grep -q '"selectText".*"default": true' "$OPTIONS_SCHEMA"; then
sed -i '/"selectText": {/,/"default":/{s/"default": true/"default": false/}' "$OPTIONS_SCHEMA"
echo " - Changed selectText default to false"
elif grep -q '"selectText".*"default": false' "$OPTIONS_SCHEMA"; then
echo " - selectText already set to false, skipping"
else
echo " - Warning: Could not find selectText setting"
fi
if grep -q '"layoutAwareScan".*"default": true' "$OPTIONS_SCHEMA"; then
sed -i '/"layoutAwareScan": {/,/"default":/{s/"default": true/"default": false/}' "$OPTIONS_SCHEMA"
echo " - Changed layoutAwareScan default to false"
elif grep -q '"layoutAwareScan".*"default": false' "$OPTIONS_SCHEMA"; then
echo " - layoutAwareScan already set to false, skipping"
else
echo " - Warning: Could not find layoutAwareScan setting"
fi
POPUP_JS="$YOMITAN_DIR/js/app/popup.js"
if [ ! -f "$POPUP_JS" ]; then
echo "Error: popup.js not found at $POPUP_JS"
exit 1
fi
echo "Patching popup.js..."
if grep -q "yomitan-popup-shown" "$POPUP_JS"; then
echo " - Already patched, skipping"
else
# Add the visibility event dispatch after the existing _onVisibleChange code
# We need to add it after: void this._invokeSafe('displayVisibilityChanged', {value});
sed -i "/void this._invokeSafe('displayVisibilityChanged', {value});/a\\
\\
// Dispatch custom events for popup visibility (Electron integration)\\
if (value) {\\
window.dispatchEvent(new CustomEvent('yomitan-popup-shown'));\\
} else {\\
window.dispatchEvent(new CustomEvent('yomitan-popup-hidden'));\\
}" "$POPUP_JS"
echo " - Added visibility events"
fi
echo ""
echo "Yomitan patching complete!"
echo ""
echo "Changes applied:"
echo " 1. permissions-util.js: Hardcoded permissions (Electron workaround)"
echo " 2. options-schema.json: selectText=false, layoutAwareScan=false"
echo " 3. popup.js: Added yomitan-popup-shown/hidden events"
echo ""
echo "To verify: Run 'bun run dev' and check for 'Yomitan extension loaded successfully'"
If you need to change Electron compatibility behavior, patch the forked source repo and rebuild.
EOF

20
scripts/prettier-scope.sh Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
paths=(
"package.json"
"tsconfig.json"
"tsconfig.renderer.json"
"tsconfig.typecheck.json"
".prettierrc.json"
".github"
"build"
"launcher"
"scripts"
"src"
)
exec bunx prettier "$@" "${paths[@]}"

View File

@@ -4,6 +4,7 @@ import path from 'node:path';
import process from 'node:process';
import { createTokenizerDepsRuntime, tokenizeSubtitle } from '../src/core/services/tokenizer.js';
import { resolveYomitanExtensionPath as resolveBuiltYomitanExtensionPath } from '../src/core/services/yomitan-extension-paths.js';
import { MecabTokenizer } from '../src/mecab-tokenizer.js';
import type { MergedToken } from '../src/types.js';
@@ -112,12 +113,12 @@ function parseCliArgs(argv: string[]): CliOptions {
if (!next) {
throw new Error('Missing value for --yomitan-extension');
}
yomitanExtensionPath = path.resolve(next);
yomitanExtensionPath = next;
continue;
}
if (arg.startsWith('--yomitan-extension=')) {
yomitanExtensionPath = path.resolve(arg.slice('--yomitan-extension='.length));
yomitanExtensionPath = arg.slice('--yomitan-extension='.length);
continue;
}
@@ -126,12 +127,12 @@ function parseCliArgs(argv: string[]): CliOptions {
if (!next) {
throw new Error('Missing value for --yomitan-user-data');
}
yomitanUserDataPath = path.resolve(next);
yomitanUserDataPath = next;
continue;
}
if (arg.startsWith('--yomitan-user-data=')) {
yomitanUserDataPath = path.resolve(arg.slice('--yomitan-user-data='.length));
yomitanUserDataPath = arg.slice('--yomitan-user-data='.length);
continue;
}
@@ -372,21 +373,10 @@ function findSelectedCandidateIndexes(
}
function resolveYomitanExtensionPath(explicitPath?: string): string | null {
const candidates = [
explicitPath ? path.resolve(explicitPath) : null,
path.resolve(process.cwd(), 'vendor', 'yomitan'),
];
for (const candidate of candidates) {
if (!candidate) {
continue;
}
if (fs.existsSync(path.join(candidate, 'manifest.json'))) {
return candidate;
}
}
return null;
return resolveBuiltYomitanExtensionPath({
explicitPath,
cwd: process.cwd(),
});
}
async function setupYomitanRuntime(options: CliOptions): Promise<YomitanRuntimeState> {
@@ -420,7 +410,7 @@ async function setupYomitanRuntime(options: CliOptions): Promise<YomitanRuntimeS
const extensionPath = resolveYomitanExtensionPath(options.yomitanExtensionPath);
if (!extensionPath) {
state.note = 'no Yomitan extension directory found';
state.note = 'no built Yomitan extension directory found; run `bun run build:yomitan`';
return state;
}

View File

@@ -55,7 +55,10 @@ test('AnkiIntegrationRuntime normalizes url and proxy defaults', () => {
assert.equal(normalized.proxy?.host, '0.0.0.0');
assert.equal(normalized.proxy?.port, 7001);
assert.equal(normalized.proxy?.upstreamUrl, 'http://anki.local:8765');
assert.equal(normalized.media?.fallbackDuration, DEFAULT_ANKI_CONNECT_CONFIG.media.fallbackDuration);
assert.equal(
normalized.media?.fallbackDuration,
DEFAULT_ANKI_CONNECT_CONFIG.media.fallbackDuration,
);
});
test('AnkiIntegrationRuntime starts proxy transport when proxy mode is enabled', () => {
@@ -70,10 +73,7 @@ test('AnkiIntegrationRuntime starts proxy transport when proxy mode is enabled',
runtime.start();
assert.deepEqual(calls, [
'known:start',
'proxy:start:127.0.0.1:9999:http://upstream:8765',
]);
assert.deepEqual(calls, ['known:start', 'proxy:start:127.0.0.1:9999:http://upstream:8765']);
});
test('AnkiIntegrationRuntime switches transports and clears known words when runtime patch disables highlighting', () => {

View File

@@ -31,8 +31,7 @@ function trimToNonEmptyString(value: unknown): string | null {
}
export function normalizeAnkiIntegrationConfig(config: AnkiConnectConfig): AnkiConnectConfig {
const resolvedUrl =
trimToNonEmptyString(config.url) ?? DEFAULT_ANKI_CONNECT_CONFIG.url;
const resolvedUrl = trimToNonEmptyString(config.url) ?? DEFAULT_ANKI_CONNECT_CONFIG.url;
const proxySource =
config.proxy && typeof config.proxy === 'object'
? (config.proxy as NonNullable<AnkiConnectConfig['proxy']>)

View File

@@ -166,9 +166,7 @@ test('parses texthooker.launchAtStartup and warns on invalid values', () => {
DEFAULT_CONFIG.texthooker.launchAtStartup,
);
assert.ok(
invalidService
.getWarnings()
.some((warning) => warning.path === 'texthooker.launchAtStartup'),
invalidService.getWarnings().some((warning) => warning.path === 'texthooker.launchAtStartup'),
);
});
@@ -211,14 +209,10 @@ test('parses annotationWebsocket settings and warns on invalid values', () => {
DEFAULT_CONFIG.annotationWebsocket.port,
);
assert.ok(
invalidService
.getWarnings()
.some((warning) => warning.path === 'annotationWebsocket.enabled'),
invalidService.getWarnings().some((warning) => warning.path === 'annotationWebsocket.enabled'),
);
assert.ok(
invalidService
.getWarnings()
.some((warning) => warning.path === 'annotationWebsocket.port'),
invalidService.getWarnings().some((warning) => warning.path === 'annotationWebsocket.port'),
);
});
@@ -350,8 +344,8 @@ test('parses subtitleStyle.nameMatchColor and warns on invalid values', () => {
const validService = new ConfigService(validDir);
assert.equal(
((validService.getConfig().subtitleStyle as unknown as Record<string, unknown>).nameMatchColor ??
null) as string | null,
((validService.getConfig().subtitleStyle as unknown as Record<string, unknown>)
.nameMatchColor ?? null) as string | null,
'#eed49f',
);
@@ -373,9 +367,7 @@ test('parses subtitleStyle.nameMatchColor and warns on invalid values', () => {
'#f5bde6',
);
assert.ok(
invalidService
.getWarnings()
.some((warning) => warning.path === 'subtitleStyle.nameMatchColor'),
invalidService.getWarnings().some((warning) => warning.path === 'subtitleStyle.nameMatchColor'),
);
});
@@ -505,10 +497,16 @@ test('parses anilist.characterDictionary config with clamping and enum validatio
assert.equal(config.anilist.characterDictionary.maxLoaded, 20);
assert.equal(config.anilist.characterDictionary.evictionPolicy, 'delete');
assert.equal(config.anilist.characterDictionary.profileScope, 'all');
assert.ok(warnings.some((warning) => warning.path === 'anilist.characterDictionary.refreshTtlHours'));
assert.ok(
warnings.some((warning) => warning.path === 'anilist.characterDictionary.refreshTtlHours'),
);
assert.ok(warnings.some((warning) => warning.path === 'anilist.characterDictionary.maxLoaded'));
assert.ok(warnings.some((warning) => warning.path === 'anilist.characterDictionary.evictionPolicy'));
assert.ok(warnings.some((warning) => warning.path === 'anilist.characterDictionary.profileScope'));
assert.ok(
warnings.some((warning) => warning.path === 'anilist.characterDictionary.evictionPolicy'),
);
assert.ok(
warnings.some((warning) => warning.path === 'anilist.characterDictionary.profileScope'),
);
});
test('parses anilist.characterDictionary.collapsibleSections booleans and warns on invalid values', () => {

View File

@@ -175,7 +175,8 @@ export function buildIntegrationConfigOptionRegistry(
path: 'anilist.characterDictionary.collapsibleSections.description',
kind: 'boolean',
defaultValue: defaultConfig.anilist.characterDictionary.collapsibleSections.description,
description: 'Open the Description section by default in character dictionary glossary entries.',
description:
'Open the Description section by default in character dictionary glossary entries.',
},
{
path: 'anilist.characterDictionary.collapsibleSections.characterInformation',
@@ -189,7 +190,8 @@ export function buildIntegrationConfigOptionRegistry(
path: 'anilist.characterDictionary.collapsibleSections.voicedBy',
kind: 'boolean',
defaultValue: defaultConfig.anilist.characterDictionary.collapsibleSections.voicedBy,
description: 'Open the Voiced by section by default in character dictionary glossary entries.',
description:
'Open the Voiced by section by default in character dictionary glossary entries.',
},
{
path: 'jellyfin.enabled',

View File

@@ -238,7 +238,9 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
);
if (nameMatchEnabled !== undefined) {
resolved.subtitleStyle.nameMatchEnabled = nameMatchEnabled;
} else if ((src.subtitleStyle as { nameMatchEnabled?: unknown }).nameMatchEnabled !== undefined) {
} else if (
(src.subtitleStyle as { nameMatchEnabled?: unknown }).nameMatchEnabled !== undefined
) {
resolved.subtitleStyle.nameMatchEnabled = fallbackSubtitleStyleNameMatchEnabled;
warn(
'subtitleStyle.nameMatchEnabled',

View File

@@ -99,8 +99,7 @@ test('runAppReadyRuntime starts texthooker on startup when enabled in config', a
calls.indexOf('createMpvClient') < calls.indexOf('startTexthooker:5174:ws://127.0.0.1:6678'),
);
assert.ok(
calls.indexOf('startTexthooker:5174:ws://127.0.0.1:6678') <
calls.indexOf('handleInitialArgs'),
calls.indexOf('startTexthooker:5174:ws://127.0.0.1:6678') < calls.indexOf('handleInitialArgs'),
);
});

View File

@@ -261,7 +261,8 @@ export function handleCliCommand(
const ignoreSecondInstanceStart =
source === 'second-instance' && args.start && deps.isOverlayRuntimeInitialized();
const shouldStart = (!ignoreSecondInstanceStart && args.start) || args.toggle || args.toggleVisibleOverlay;
const shouldStart =
(!ignoreSecondInstanceStart && args.start) || args.toggle || args.toggleVisibleOverlay;
const needsOverlayRuntime = commandNeedsOverlayRuntime(args);
const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start;

View File

@@ -38,6 +38,7 @@ function createOptions(overrides: Partial<Parameters<typeof handleMpvCommandFrom
mpvSendCommand: (command) => {
sentCommands.push(command);
},
resolveProxyCommandOsd: async () => null,
isMpvConnected: () => true,
hasRuntimeOptionsManager: () => true,
...overrides,
@@ -52,30 +53,39 @@ test('handleMpvCommandFromIpc forwards regular mpv commands', () => {
assert.deepEqual(osd, []);
});
test('handleMpvCommandFromIpc emits osd for subtitle position keybinding proxies', () => {
test('handleMpvCommandFromIpc emits osd for subtitle position keybinding proxies', async () => {
const { options, sentCommands, osd } = createOptions();
handleMpvCommandFromIpc(['add', 'sub-pos', 1], options);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(sentCommands, [['add', 'sub-pos', 1]]);
assert.deepEqual(osd, ['Subtitle position: ${sub-pos}']);
});
test('handleMpvCommandFromIpc emits osd for primary subtitle track keybinding proxies', () => {
const { options, sentCommands, osd } = createOptions();
test('handleMpvCommandFromIpc emits resolved osd for primary subtitle track keybinding proxies', async () => {
const { options, sentCommands, osd } = createOptions({
resolveProxyCommandOsd: async () => 'Subtitle track: Internal #3 - Japanese (active)',
});
handleMpvCommandFromIpc(['cycle', 'sid'], options);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(sentCommands, [['cycle', 'sid']]);
assert.deepEqual(osd, ['Subtitle track: ${sid}']);
assert.deepEqual(osd, ['Subtitle track: Internal #3 - Japanese (active)']);
});
test('handleMpvCommandFromIpc emits osd for secondary subtitle track keybinding proxies', () => {
const { options, sentCommands, osd } = createOptions();
test('handleMpvCommandFromIpc emits resolved osd for secondary subtitle track keybinding proxies', async () => {
const { options, sentCommands, osd } = createOptions({
resolveProxyCommandOsd: async () =>
'Secondary subtitle track: External #8 - English Commentary',
});
handleMpvCommandFromIpc(['set_property', 'secondary-sid', 'auto'], options);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(sentCommands, [['set_property', 'secondary-sid', 'auto']]);
assert.deepEqual(osd, ['Secondary subtitle track: ${secondary-sid}']);
assert.deepEqual(osd, ['Secondary subtitle track: External #8 - English Commentary']);
});
test('handleMpvCommandFromIpc emits osd for subtitle delay keybinding proxies', () => {
test('handleMpvCommandFromIpc emits osd for subtitle delay keybinding proxies', async () => {
const { options, sentCommands, osd } = createOptions();
handleMpvCommandFromIpc(['add', 'sub-delay', 0.1], options);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(sentCommands, [['add', 'sub-delay', 0.1]]);
assert.deepEqual(osd, ['Subtitle delay: ${sub-delay}']);
});

View File

@@ -23,6 +23,7 @@ export interface HandleMpvCommandFromIpcOptions {
mpvPlayNextSubtitle: () => void;
shiftSubDelayToAdjacentSubtitle: (direction: 'next' | 'previous') => Promise<void>;
mpvSendCommand: (command: (string | number)[]) => void;
resolveProxyCommandOsd?: (command: (string | number)[]) => Promise<string | null>;
isMpvConnected: () => boolean;
hasRuntimeOptionsManager: () => boolean;
}
@@ -36,7 +37,7 @@ const MPV_PROPERTY_COMMANDS = new Set([
'multiply',
]);
function resolveProxyCommandOsd(command: (string | number)[]): string | null {
function resolveProxyCommandOsdTemplate(command: (string | number)[]): string | null {
const operation = typeof command[0] === 'string' ? command[0] : '';
const property = typeof command[1] === 'string' ? command[1] : '';
if (!MPV_PROPERTY_COMMANDS.has(operation)) return null;
@@ -55,6 +56,25 @@ function resolveProxyCommandOsd(command: (string | number)[]): string | null {
return null;
}
function showResolvedProxyCommandOsd(
command: (string | number)[],
options: HandleMpvCommandFromIpcOptions,
): void {
const template = resolveProxyCommandOsdTemplate(command);
if (!template) return;
const emit = async () => {
try {
const resolved = await options.resolveProxyCommandOsd?.(command);
options.showMpvOsd(resolved || template);
} catch {
options.showMpvOsd(template);
}
};
void emit();
}
export function handleMpvCommandFromIpc(
command: (string | number)[],
options: HandleMpvCommandFromIpcOptions,
@@ -103,10 +123,7 @@ export function handleMpvCommandFromIpc(
options.mpvPlayNextSubtitle();
} else {
options.mpvSendCommand(command);
const osd = resolveProxyCommandOsd(command);
if (osd) {
options.showMpvOsd(osd);
}
showResolvedProxyCommandOsd(command, options);
}
}
}

View File

@@ -22,6 +22,22 @@ test('showMpvOsdRuntime sends show-text when connected', () => {
assert.deepEqual(commands, [['show-text', 'hello', '3000']]);
});
test('showMpvOsdRuntime enables property expansion for placeholder-based messages', () => {
const commands: (string | number)[][] = [];
showMpvOsdRuntime(
{
connected: true,
send: ({ command }) => {
commands.push(command);
},
},
'Subtitle delay: ${sub-delay}',
);
assert.deepEqual(commands, [
['expand-properties', 'show-text', 'Subtitle delay: ${sub-delay}', '3000'],
]);
});
test('showMpvOsdRuntime logs fallback when disconnected', () => {
const logs: string[] = [];
showMpvOsdRuntime(

View File

@@ -53,7 +53,10 @@ export function showMpvOsdRuntime(
fallbackLog: (text: string) => void = (line) => logger.info(line),
): void {
if (mpvClient && mpvClient.connected) {
mpvClient.send({ command: ['show-text', text, '3000'] });
const command = text.includes('${')
? ['expand-properties', 'show-text', text, '3000']
: ['show-text', text, '3000'];
mpvClient.send({ command });
return;
}
fallbackLog(`OSD (MPV not connected): ${text}`);

View File

@@ -12,7 +12,7 @@ test('injectTexthookerBootstrapHtml injects websocket bootstrap before head clos
/window\.localStorage\.setItem\('bannou-texthooker-websocketUrl', "ws:\/\/127\.0\.0\.1:6678"\)/,
);
assert.ok(actual.indexOf('</script></head>') !== -1);
assert.ok(actual.includes("bannou-texthooker-websocketUrl"));
assert.ok(actual.includes('bannou-texthooker-websocketUrl'));
assert.ok(!actual.includes('bannou-texthooker-enableKnownWordColoring'));
assert.ok(!actual.includes('bannou-texthooker-enableNPlusOneColoring'));
assert.ok(!actual.includes('bannou-texthooker-enableNameMatchColoring'));

View File

@@ -764,11 +764,9 @@ test('requestYomitanScanTokens skips fallback fragments without exact primary so
});
});
const result = await requestYomitanScanTokens(
'だが それでも届かぬ高みがあった',
deps,
{ error: () => undefined },
);
const result = await requestYomitanScanTokens('だが それでも届かぬ高みがあった', deps, {
error: () => undefined,
});
assert.deepEqual(
result?.map((token) => ({
@@ -875,7 +873,8 @@ test('dictionary settings helpers upsert and remove dictionary entries without r
const upsertScript = scripts.find(
(script) =>
script.includes('setAllSettings') && script.includes('"SubMiner Character Dictionary (AniList 1)"'),
script.includes('setAllSettings') &&
script.includes('"SubMiner Character Dictionary (AniList 1)"'),
);
assert.ok(upsertScript);
const jitendexOffset = upsertScript?.indexOf('"Jitendex"') ?? -1;
@@ -915,9 +914,18 @@ test('importYomitanDictionaryFromZip uses settings automation bridge instead of
});
assert.equal(imported, true);
assert.equal(scripts.some((script) => script.includes('__subminerYomitanSettingsAutomation')), true);
assert.equal(scripts.some((script) => script.includes('importDictionaryArchiveBase64')), true);
assert.equal(scripts.some((script) => script.includes('subminerImportDictionary')), false);
assert.equal(
scripts.some((script) => script.includes('__subminerYomitanSettingsAutomation')),
true,
);
assert.equal(
scripts.some((script) => script.includes('importDictionaryArchiveBase64')),
true,
);
assert.equal(
scripts.some((script) => script.includes('subminerImportDictionary')),
false,
);
});
test('deleteYomitanDictionaryByTitle uses settings automation bridge instead of custom backend action', async () => {
@@ -947,7 +955,16 @@ test('deleteYomitanDictionaryByTitle uses settings automation bridge instead of
);
assert.equal(deleted, true);
assert.equal(scripts.some((script) => script.includes('__subminerYomitanSettingsAutomation')), true);
assert.equal(scripts.some((script) => script.includes('deleteDictionary')), true);
assert.equal(scripts.some((script) => script.includes('subminerDeleteDictionary')), false);
assert.equal(
scripts.some((script) => script.includes('__subminerYomitanSettingsAutomation')),
true,
);
assert.equal(
scripts.some((script) => script.includes('deleteDictionary')),
true,
);
assert.equal(
scripts.some((script) => script.includes('subminerDeleteDictionary')),
false,
);
});

View File

@@ -562,9 +562,7 @@ async function createYomitanExtensionWindow(
});
return window;
} catch (err) {
logger.error(
`Failed to create hidden Yomitan ${pageName} window: ${(err as Error).message}`,
);
logger.error(`Failed to create hidden Yomitan ${pageName} window: ${(err as Error).message}`);
if (!window.isDestroyed()) {
window.destroy();
}
@@ -1043,13 +1041,15 @@ export async function requestYomitanScanTokens(
}
if (Array.isArray(rawResult)) {
const selectedTokens = selectYomitanParseTokens(rawResult, () => false, 'headword');
return selectedTokens?.map((token) => ({
surface: token.surface,
reading: token.reading,
headword: token.headword,
startPos: token.startPos,
endPos: token.endPos,
})) ?? null;
return (
selectedTokens?.map((token) => ({
surface: token.surface,
reading: token.reading,
headword: token.headword,
startPos: token.startPos,
endPos: token.endPos,
})) ?? null
);
}
return null;
} catch (err) {
@@ -1523,7 +1523,12 @@ export async function getYomitanDictionaryInfo(
deps: YomitanParserRuntimeDeps,
logger: LoggerLike,
): Promise<YomitanDictionaryInfo[]> {
const result = await invokeYomitanBackendAction<unknown>('getDictionaryInfo', undefined, deps, logger);
const result = await invokeYomitanBackendAction<unknown>(
'getDictionaryInfo',
undefined,
deps,
logger,
);
if (!Array.isArray(result)) {
return [];
}
@@ -1546,7 +1551,12 @@ export async function getYomitanSettingsFull(
deps: YomitanParserRuntimeDeps,
logger: LoggerLike,
): Promise<Record<string, unknown> | null> {
const result = await invokeYomitanBackendAction<unknown>('optionsGetFull', undefined, deps, logger);
const result = await invokeYomitanBackendAction<unknown>(
'optionsGetFull',
undefined,
deps,
logger,
);
return isObject(result) ? result : null;
}
@@ -1653,7 +1663,7 @@ export async function upsertYomitanDictionarySettings(
(entry) =>
isObject(entry) &&
typeof (entry as { name?: unknown }).name === 'string' &&
((entry as { name: string }).name.trim() === normalizedTitle),
(entry as { name: string }).name.trim() === normalizedTitle,
);
if (existingIndex >= 0) {

View File

@@ -90,7 +90,10 @@ export function shouldCopyYomitanExtension(sourceDir: string, targetDir: string)
return sourceHash === null || targetHash === null || sourceHash !== targetHash;
}
export function ensureExtensionCopy(sourceDir: string, userDataPath: string): {
export function ensureExtensionCopy(
sourceDir: string,
userDataPath: string,
): {
targetDir: string;
copied: boolean;
} {

View File

@@ -75,7 +75,10 @@ test('ensureExtensionCopy refreshes copied extension when display files change',
assert.equal(result.targetDir, targetDir);
assert.equal(result.copied, true);
assert.equal(
fs.readFileSync(path.join(targetDir, 'js', 'display', 'structured-content-generator.js'), 'utf8'),
fs.readFileSync(
path.join(targetDir, 'js', 'display', 'structured-content-generator.js'),
'utf8',
),
'new display code',
);
});

View File

@@ -1,13 +1,17 @@
import { BrowserWindow, Extension, session } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { createLogger } from '../../logger';
import { ensureExtensionCopy } from './yomitan-extension-copy';
import {
getYomitanExtensionSearchPaths,
resolveExistingYomitanExtensionPath,
} from './yomitan-extension-paths';
const logger = createLogger('main:yomitan-extension-loader');
export interface YomitanExtensionLoaderDeps {
userDataPath: string;
extensionPath?: string;
getYomitanParserWindow: () => BrowserWindow | null;
setYomitanParserWindow: (window: BrowserWindow | null) => void;
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
@@ -18,25 +22,17 @@ export interface YomitanExtensionLoaderDeps {
export async function loadYomitanExtension(
deps: YomitanExtensionLoaderDeps,
): Promise<Extension | null> {
const searchPaths = [
path.join(__dirname, '..', '..', 'vendor', 'yomitan'),
path.join(__dirname, '..', '..', '..', 'vendor', 'yomitan'),
path.join(process.resourcesPath, 'yomitan'),
'/usr/share/SubMiner/yomitan',
path.join(deps.userDataPath, 'yomitan'),
];
let extPath: string | null = null;
for (const p of searchPaths) {
if (fs.existsSync(p)) {
extPath = p;
break;
}
}
const searchPaths = getYomitanExtensionSearchPaths({
explicitPath: deps.extensionPath,
moduleDir: __dirname,
resourcesPath: process.resourcesPath,
userDataPath: deps.userDataPath,
});
let extPath = resolveExistingYomitanExtensionPath(searchPaths, fs.existsSync);
if (!extPath) {
logger.error('Yomitan extension not found in any search path');
logger.error('Install Yomitan to one of:', searchPaths);
logger.error('Run `bun run build:yomitan` or install Yomitan to one of:', searchPaths);
return null;
}

View File

@@ -0,0 +1,50 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import test from 'node:test';
import {
getYomitanExtensionSearchPaths,
resolveExistingYomitanExtensionPath,
} from './yomitan-extension-paths';
test('getYomitanExtensionSearchPaths prioritizes generated build output before packaged fallbacks', () => {
const searchPaths = getYomitanExtensionSearchPaths({
cwd: '/repo',
moduleDir: '/repo/dist/core/services',
resourcesPath: '/opt/SubMiner/resources',
userDataPath: '/Users/kyle/.config/SubMiner',
});
assert.deepEqual(searchPaths, [
path.join('/repo', 'build', 'yomitan'),
path.join('/opt/SubMiner/resources', 'yomitan'),
'/usr/share/SubMiner/yomitan',
path.join('/Users/kyle/.config/SubMiner', 'yomitan'),
]);
});
test('resolveExistingYomitanExtensionPath returns first manifest-backed candidate', () => {
const existing = new Set<string>([
path.join('/repo', 'build', 'yomitan', 'manifest.json'),
path.join('/repo', 'vendor', 'subminer-yomitan', 'ext', 'manifest.json'),
]);
const resolved = resolveExistingYomitanExtensionPath(
[
path.join('/repo', 'build', 'yomitan'),
path.join('/repo', 'vendor', 'subminer-yomitan', 'ext'),
],
(candidate) => existing.has(candidate),
);
assert.equal(resolved, path.join('/repo', 'build', 'yomitan'));
});
test('resolveExistingYomitanExtensionPath ignores source tree without built manifest', () => {
const resolved = resolveExistingYomitanExtensionPath(
[path.join('/repo', 'vendor', 'subminer-yomitan', 'ext')],
() => false,
);
assert.equal(resolved, null);
});

View File

@@ -0,0 +1,60 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
export interface YomitanExtensionPathOptions {
explicitPath?: string;
cwd?: string;
moduleDir?: string;
resourcesPath?: string;
userDataPath?: string;
}
function pushUnique(values: string[], candidate: string | null | undefined): void {
if (!candidate || values.includes(candidate)) {
return;
}
values.push(candidate);
}
export function getYomitanExtensionSearchPaths(
options: YomitanExtensionPathOptions = {},
): string[] {
const searchPaths: string[] = [];
pushUnique(searchPaths, options.explicitPath ? path.resolve(options.explicitPath) : null);
pushUnique(searchPaths, options.cwd ? path.resolve(options.cwd, 'build', 'yomitan') : null);
pushUnique(
searchPaths,
options.moduleDir
? path.resolve(options.moduleDir, '..', '..', '..', 'build', 'yomitan')
: null,
);
pushUnique(
searchPaths,
options.resourcesPath ? path.join(options.resourcesPath, 'yomitan') : null,
);
pushUnique(searchPaths, '/usr/share/SubMiner/yomitan');
pushUnique(searchPaths, options.userDataPath ? path.join(options.userDataPath, 'yomitan') : null);
return searchPaths;
}
export function resolveExistingYomitanExtensionPath(
searchPaths: string[],
existsSync: (path: string) => boolean = fs.existsSync,
): string | null {
for (const candidate of searchPaths) {
if (existsSync(path.join(candidate, 'manifest.json'))) {
return candidate;
}
}
return null;
}
export function resolveYomitanExtensionPath(
options: YomitanExtensionPathOptions = {},
existsSync: (path: string) => boolean = fs.existsSync,
): string | null {
return resolveExistingYomitanExtensionPath(getYomitanExtensionSearchPaths(options), existsSync);
}

View File

@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import path from 'node:path';
import test from 'node:test';
import { pathToFileURL } from 'node:url';
import { resolveYomitanExtensionPath } from './yomitan-extension-paths';
class FakeStyle {
private values = new Map<string, string>();
@@ -155,15 +156,14 @@ function findFirstByClass(node: FakeNode, className: string): FakeNode | null {
}
test('StructuredContentGenerator uses direct img loading for popup glossary images', async () => {
const yomitanRoot = resolveYomitanExtensionPath({ cwd: process.cwd() });
assert.ok(yomitanRoot, 'Run `bun run build:yomitan` before Yomitan integration tests.');
const { DisplayContentManager } = await import(
pathToFileURL(
path.join(process.cwd(), 'vendor/yomitan/js/display/display-content-manager.js'),
).href
pathToFileURL(path.join(yomitanRoot, 'js', 'display', 'display-content-manager.js')).href
);
const { StructuredContentGenerator } = await import(
pathToFileURL(
path.join(process.cwd(), 'vendor/yomitan/js/display/structured-content-generator.js'),
).href
pathToFileURL(path.join(yomitanRoot, 'js', 'display', 'structured-content-generator.js')).href
);
const createObjectURLCalls: string[] = [];
@@ -197,14 +197,10 @@ test('StructuredContentGenerator uses direct img loading for popup glossary imag
},
});
const generator = new StructuredContentGenerator(
manager,
new FakeDocument(),
{
devicePixelRatio: 1,
navigator: { userAgent: 'Mozilla/5.0' },
},
);
const generator = new StructuredContentGenerator(manager, new FakeDocument(), {
devicePixelRatio: 1,
navigator: { userAgent: 'Mozilla/5.0' },
});
const node = generator.createDefinitionImage(
{

View File

@@ -16,10 +16,7 @@ test('normalizeStartupArgv defaults no-arg startup to --start --background', ()
'--background',
]);
assert.deepEqual(
normalizeStartupArgv(
['SubMiner.AppImage', '--password-store', 'gnome-libsecret'],
{},
),
normalizeStartupArgv(['SubMiner.AppImage', '--password-store', 'gnome-libsecret'], {}),
['SubMiner.AppImage', '--password-store', 'gnome-libsecret', '--start', '--background'],
);
assert.deepEqual(normalizeStartupArgv(['SubMiner.AppImage', '--background'], {}), [

View File

@@ -1657,10 +1657,9 @@ const {
},
});
const maybeFocusExistingFirstRunSetupWindow =
createMaybeFocusExistingFirstRunSetupWindowHandler({
getSetupWindow: () => appState.firstRunSetupWindow,
});
const maybeFocusExistingFirstRunSetupWindow = createMaybeFocusExistingFirstRunSetupWindowHandler({
getSetupWindow: () => appState.firstRunSetupWindow,
});
const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
maybeFocusExistingSetupWindow: maybeFocusExistingFirstRunSetupWindow,
createSetupWindow: () =>
@@ -2404,9 +2403,9 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
shouldSkipHeavyStartup: () =>
Boolean(
appState.initialArgs &&
(shouldRunSettingsOnlyStartup(appState.initialArgs) ||
appState.initialArgs.dictionary ||
appState.initialArgs.setup),
(shouldRunSettingsOnlyStartup(appState.initialArgs) ||
appState.initialArgs.dictionary ||
appState.initialArgs.setup),
),
createImmersionTracker: () => {
ensureImmersionTrackerStarted();
@@ -2419,65 +2418,64 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
immersionTrackerStartupMainDeps,
});
const { runAndApplyStartupState } =
runtimeRegistry.startup.createStartupRuntimeHandlers<
CliArgs,
StartupState,
ReturnType<typeof createStartupBootstrapRuntimeDeps>
>({
appLifecycleRuntimeRunnerMainDeps: {
app,
platform: process.platform,
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
parseArgs: (argv: string[]) => parseArgs(argv),
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
handleCliCommand(nextArgs, source),
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
onReady: appReadyRuntimeRunner,
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
const { runAndApplyStartupState } = runtimeRegistry.startup.createStartupRuntimeHandlers<
CliArgs,
StartupState,
ReturnType<typeof createStartupBootstrapRuntimeDeps>
>({
appLifecycleRuntimeRunnerMainDeps: {
app,
platform: process.platform,
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
parseArgs: (argv: string[]) => parseArgs(argv),
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
handleCliCommand(nextArgs, source),
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
onReady: appReadyRuntimeRunner,
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
},
createAppLifecycleRuntimeRunner: (params) => createAppLifecycleRuntimeRunner(params),
buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
argv: process.argv,
parseArgs: (argv: string[]) => parseArgs(argv),
setLogLevel: (level: string, source: LogLevelSource) => {
setLogLevel(level, source);
},
createAppLifecycleRuntimeRunner: (params) => createAppLifecycleRuntimeRunner(params),
buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
argv: process.argv,
parseArgs: (argv: string[]) => parseArgs(argv),
setLogLevel: (level: string, source: LogLevelSource) => {
setLogLevel(level, source);
forceX11Backend: (args: CliArgs) => {
forceX11Backend(args);
},
enforceUnsupportedWaylandMode: (args: CliArgs) => {
enforceUnsupportedWaylandMode(args);
},
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
getDefaultSocketPath: () => getDefaultSocketPath(),
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
configDir: CONFIG_DIR,
defaultConfig: DEFAULT_CONFIG,
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
generateDefaultConfigFile: (
args: CliArgs,
options: {
configDir: string;
defaultConfig: unknown;
generateTemplate: (config: unknown) => string;
},
forceX11Backend: (args: CliArgs) => {
forceX11Backend(args);
},
enforceUnsupportedWaylandMode: (args: CliArgs) => {
enforceUnsupportedWaylandMode(args);
},
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
getDefaultSocketPath: () => getDefaultSocketPath(),
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
configDir: CONFIG_DIR,
defaultConfig: DEFAULT_CONFIG,
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
generateDefaultConfigFile: (
args: CliArgs,
options: {
configDir: string;
defaultConfig: unknown;
generateTemplate: (config: unknown) => string;
},
) => generateDefaultConfigFile(args, options),
setExitCode: (code) => {
process.exitCode = code;
},
quitApp: () => app.quit(),
logGenerateConfigError: (message) => logger.error(message),
startAppLifecycle,
}),
createStartupBootstrapRuntimeDeps: (deps) => createStartupBootstrapRuntimeDeps(deps),
runStartupBootstrapRuntime,
applyStartupState: (startupState) => applyStartupState(appState, startupState),
});
) => generateDefaultConfigFile(args, options),
setExitCode: (code) => {
process.exitCode = code;
},
quitApp: () => app.quit(),
logGenerateConfigError: (message) => logger.error(message),
startAppLifecycle,
}),
createStartupBootstrapRuntimeDeps: (deps) => createStartupBootstrapRuntimeDeps(deps),
runStartupBootstrapRuntime,
applyStartupState: (startupState) => applyStartupState(appState, startupState),
});
runAndApplyStartupState();
if (isAnilistTrackingEnabled(getResolvedConfig())) {
@@ -3203,6 +3201,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
shiftSubtitleDelayToAdjacentCueHandler(direction),
sendMpvCommand: (rawCommand: (string | number)[]) =>
sendMpvCommandRuntime(appState.mpvClient, rawCommand),
getMpvClient: () => appState.mpvClient,
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
},
@@ -3341,74 +3340,75 @@ const createCliCommandContextHandler = createCliCommandContextFactory({
});
const { createMainWindow: createMainWindowHandler, createModalWindow: createModalWindowHandler } =
createOverlayWindowRuntimeHandlers<BrowserWindow>({
createOverlayWindowDeps: {
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
isDev,
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled),
isOverlayVisible: (windowKind) =>
windowKind === 'visible' ? overlayManager.getVisibleOverlayVisible() : false,
tryHandleOverlayShortcutLocalFallback: (input) =>
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
onWindowClosed: (windowKind) => {
if (windowKind === 'visible') {
overlayManager.setMainWindow(null);
} else {
overlayManager.setModalWindow(null);
}
createOverlayWindowDeps: {
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
isDev,
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
setOverlayDebugVisualizationEnabled: (enabled) =>
setOverlayDebugVisualizationEnabled(enabled),
isOverlayVisible: (windowKind) =>
windowKind === 'visible' ? overlayManager.getVisibleOverlayVisible() : false,
tryHandleOverlayShortcutLocalFallback: (input) =>
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
onWindowClosed: (windowKind) => {
if (windowKind === 'visible') {
overlayManager.setMainWindow(null);
} else {
overlayManager.setModalWindow(null);
}
},
},
},
setMainWindow: (window) => overlayManager.setMainWindow(window),
setModalWindow: (window) => overlayManager.setModalWindow(window),
});
setMainWindow: (window) => overlayManager.setMainWindow(window),
setModalWindow: (window) => overlayManager.setModalWindow(window),
});
const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
createTrayRuntimeHandlers({
resolveTrayIconPathDeps: {
resolveTrayIconPathRuntime,
platform: process.platform,
resourcesPath: process.resourcesPath,
appPath: app.getAppPath(),
dirname: __dirname,
joinPath: (...parts) => path.join(...parts),
fileExists: (candidate) => fs.existsSync(candidate),
},
buildTrayMenuTemplateDeps: {
buildTrayMenuTemplateRuntime,
initializeOverlayRuntime: () => initializeOverlayRuntime(),
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
openFirstRunSetupWindow: () => openFirstRunSetupWindow(),
openYomitanSettings: () => openYomitanSettings(),
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
openJellyfinSetupWindow: () => openJellyfinSetupWindow(),
openAnilistSetupWindow: () => openAnilistSetupWindow(),
quitApp: () => app.quit(),
},
ensureTrayDeps: {
getTray: () => appTray,
setTray: (tray) => {
appTray = tray as Tray | null;
resolveTrayIconPathDeps: {
resolveTrayIconPathRuntime,
platform: process.platform,
resourcesPath: process.resourcesPath,
appPath: app.getAppPath(),
dirname: __dirname,
joinPath: (...parts) => path.join(...parts),
fileExists: (candidate) => fs.existsSync(candidate),
},
createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath),
createEmptyImage: () => nativeImage.createEmpty(),
createTray: (icon) => new Tray(icon as ConstructorParameters<typeof Tray>[0]),
trayTooltip: TRAY_TOOLTIP,
platform: process.platform,
logWarn: (message) => logger.warn(message),
initializeOverlayRuntime: () => initializeOverlayRuntime(),
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
},
destroyTrayDeps: {
getTray: () => appTray,
setTray: (tray) => {
appTray = tray as Tray | null;
buildTrayMenuTemplateDeps: {
buildTrayMenuTemplateRuntime,
initializeOverlayRuntime: () => initializeOverlayRuntime(),
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
openFirstRunSetupWindow: () => openFirstRunSetupWindow(),
openYomitanSettings: () => openYomitanSettings(),
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
openJellyfinSetupWindow: () => openJellyfinSetupWindow(),
openAnilistSetupWindow: () => openAnilistSetupWindow(),
quitApp: () => app.quit(),
},
},
buildMenuFromTemplate: (template) => Menu.buildFromTemplate(template),
});
ensureTrayDeps: {
getTray: () => appTray,
setTray: (tray) => {
appTray = tray as Tray | null;
},
createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath),
createEmptyImage: () => nativeImage.createEmpty(),
createTray: (icon) => new Tray(icon as ConstructorParameters<typeof Tray>[0]),
trayTooltip: TRAY_TOOLTIP,
platform: process.platform,
logWarn: (message) => logger.warn(message),
initializeOverlayRuntime: () => initializeOverlayRuntime(),
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
},
destroyTrayDeps: {
getTray: () => appTray,
setTray: (tray) => {
appTray = tray as Tray | null;
},
},
buildMenuFromTemplate: (template) => Menu.buildFromTemplate(template),
});
const yomitanExtensionRuntime = createYomitanExtensionRuntime({
loadYomitanExtensionCore,
userDataPath: USER_DATA_PATH,

View File

@@ -563,7 +563,9 @@ test('generateForCurrentMedia reapplies collapsible open states when using cache
content: { content: Array<Record<string, unknown>> };
}
).content.content;
const sections = children.filter((item) => (item as { tag?: string }).tag === 'details') as Array<{
const sections = children.filter(
(item) => (item as { tag?: string }).tag === 'details',
) as Array<{
open?: boolean;
}>;
assert.ok(sections.length >= 2);
@@ -1889,7 +1891,9 @@ test('buildMergedDictionary reapplies collapsible open states from current confi
content: { content: Array<Record<string, unknown>> };
}
).content.content;
const sections = children.filter((item) => (item as { tag?: string }).tag === 'details') as Array<{
const sections = children.filter(
(item) => (item as { tag?: string }).tag === 'details',
) as Array<{
open?: boolean;
}>;
assert.ok(sections.length >= 1);

View File

@@ -502,7 +502,10 @@ function expandRawNameVariants(rawName: string): string[] {
if (!trimmed) return [];
const variants = new Set<string>([trimmed]);
const outer = trimmed.replace(/[(][^()]+[)]/g, ' ').replace(/\s+/g, ' ').trim();
const outer = trimmed
.replace(/[(][^()]+[)]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (outer && outer !== trimmed) {
variants.add(outer);
}
@@ -1286,12 +1289,14 @@ async function fetchCharactersForMedia(
if (!node || typeof node.id !== 'number') continue;
const fullName = node.name?.full?.trim() || '';
const nativeName = node.name?.native?.trim() || '';
const alternativeNames = [...new Set(
(node.name?.alternative ?? [])
.filter((value): value is string => typeof value === 'string')
.map((value) => value.trim())
.filter((value) => value.length > 0),
)];
const alternativeNames = [
...new Set(
(node.name?.alternative ?? [])
.filter((value): value is string => typeof value === 'string')
.map((value) => value.trim())
.filter((value) => value.length > 0),
),
];
if (!fullName && !nativeName && alternativeNames.length === 0) continue;
const voiceActors: VoiceActorRecord[] = [];
for (const va of edge?.voiceActors ?? []) {

View File

@@ -186,6 +186,7 @@ export interface MpvCommandRuntimeServiceDepsParams {
mpvPlayNextSubtitle: HandleMpvCommandFromIpcOptions['mpvPlayNextSubtitle'];
shiftSubDelayToAdjacentSubtitle: HandleMpvCommandFromIpcOptions['shiftSubDelayToAdjacentSubtitle'];
mpvSendCommand: HandleMpvCommandFromIpcOptions['mpvSendCommand'];
resolveProxyCommandOsd?: HandleMpvCommandFromIpcOptions['resolveProxyCommandOsd'];
isMpvConnected: HandleMpvCommandFromIpcOptions['isMpvConnected'];
hasRuntimeOptionsManager: HandleMpvCommandFromIpcOptions['hasRuntimeOptionsManager'];
}
@@ -339,6 +340,7 @@ export function createMpvCommandRuntimeServiceDeps(
mpvPlayNextSubtitle: params.mpvPlayNextSubtitle,
shiftSubDelayToAdjacentSubtitle: params.shiftSubDelayToAdjacentSubtitle,
mpvSendCommand: params.mpvSendCommand,
resolveProxyCommandOsd: params.resolveProxyCommandOsd,
isMpvConnected: params.isMpvConnected,
hasRuntimeOptionsManager: params.hasRuntimeOptionsManager,
};

View File

@@ -2,6 +2,12 @@ import type { RuntimeOptionApplyResult, RuntimeOptionId } from '../types';
import { handleMpvCommandFromIpc } from '../core/services';
import { createMpvCommandRuntimeServiceDeps } from './dependencies';
import { SPECIAL_COMMANDS } from '../config';
import { resolveProxyCommandOsdRuntime } from './runtime/mpv-proxy-osd';
type MpvPropertyClientLike = {
connected: boolean;
requestProperty: (name: string) => Promise<unknown>;
};
export interface MpvCommandFromIpcRuntimeDeps {
triggerSubsyncFromConfig: () => void;
@@ -12,6 +18,7 @@ export interface MpvCommandFromIpcRuntimeDeps {
playNextSubtitle: () => void;
shiftSubDelayToAdjacentSubtitle: (direction: 'next' | 'previous') => Promise<void>;
sendMpvCommand: (command: (string | number)[]) => void;
getMpvClient: () => MpvPropertyClientLike | null;
isMpvConnected: () => boolean;
hasRuntimeOptionsManager: () => boolean;
}
@@ -33,6 +40,8 @@ export function handleMpvCommandFromIpcRuntime(
shiftSubDelayToAdjacentSubtitle: (direction) =>
deps.shiftSubDelayToAdjacentSubtitle(direction),
mpvSendCommand: deps.sendMpvCommand,
resolveProxyCommandOsd: (nextCommand) =>
resolveProxyCommandOsdRuntime(nextCommand, deps.getMpvClient),
isMpvConnected: deps.isMpvConnected,
hasRuntimeOptionsManager: deps.hasRuntimeOptionsManager,
}),

View File

@@ -75,5 +75,7 @@ test('createRegisterSubminerProtocolClientHandler keeps unsupported registration
});
register();
assert.deepEqual(calls, ['debug:Failed to register default protocol handler for subminer:// URLs']);
assert.deepEqual(calls, [
'debug:Failed to register default protocol handler for subminer:// URLs',
]);
});

View File

@@ -172,7 +172,10 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
? String(existing.revision)
: null;
const shouldImport =
merged !== null || existing === null || existingRevision === null || existingRevision !== revision;
merged !== null ||
existing === null ||
existingRevision === null ||
existingRevision !== revision;
if (shouldImport) {
if (existing !== null) {

View File

@@ -16,6 +16,7 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
playNextSubtitle: () => {},
shiftSubDelayToAdjacentSubtitle: async () => {},
sendMpvCommand: () => {},
getMpvClient: () => null,
isMpvConnected: () => false,
hasRuntimeOptionsManager: () => true,
},

View File

@@ -79,7 +79,10 @@ test('installFirstRunPluginToDefaultLocation installs plugin and backs up existi
const scriptsDirEntries = fs.readdirSync(installPaths.scriptsDir);
const scriptOptsEntries = fs.readdirSync(installPaths.scriptOptsDir);
assert.equal(scriptsDirEntries.some((entry) => entry.startsWith('subminer.bak.')), true);
assert.equal(
scriptsDirEntries.some((entry) => entry.startsWith('subminer.bak.')),
true,
);
assert.equal(
scriptOptsEntries.some((entry) => entry.startsWith('subminer.conf.bak.')),
true,

View File

@@ -3,10 +3,7 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
createFirstRunSetupService,
shouldAutoOpenFirstRunSetup,
} from './first-run-setup-service';
import { createFirstRunSetupService, shouldAutoOpenFirstRunSetup } from './first-run-setup-service';
import type { CliArgs } from '../../cli/args';
function withTempDir(fn: (dir: string) => Promise<void> | void): Promise<void> | void {

View File

@@ -43,39 +43,39 @@ export interface FirstRunSetupService {
function hasAnyStartupCommandBeyondSetup(args: CliArgs): boolean {
return Boolean(
args.toggle ||
args.toggleVisibleOverlay ||
args.settings ||
args.show ||
args.hide ||
args.showVisibleOverlay ||
args.hideVisibleOverlay ||
args.copySubtitle ||
args.copySubtitleMultiple ||
args.mineSentence ||
args.mineSentenceMultiple ||
args.updateLastCardFromClipboard ||
args.refreshKnownWords ||
args.toggleSecondarySub ||
args.triggerFieldGrouping ||
args.triggerSubsync ||
args.markAudioCard ||
args.openRuntimeOptions ||
args.anilistStatus ||
args.anilistLogout ||
args.anilistSetup ||
args.anilistRetryQueue ||
args.dictionary ||
args.jellyfin ||
args.jellyfinLogin ||
args.jellyfinLogout ||
args.jellyfinLibraries ||
args.jellyfinItems ||
args.jellyfinSubtitles ||
args.jellyfinPlay ||
args.jellyfinRemoteAnnounce ||
args.jellyfinPreviewAuth ||
args.texthooker ||
args.help
args.toggleVisibleOverlay ||
args.settings ||
args.show ||
args.hide ||
args.showVisibleOverlay ||
args.hideVisibleOverlay ||
args.copySubtitle ||
args.copySubtitleMultiple ||
args.mineSentence ||
args.mineSentenceMultiple ||
args.updateLastCardFromClipboard ||
args.refreshKnownWords ||
args.toggleSecondarySub ||
args.triggerFieldGrouping ||
args.triggerSubsync ||
args.markAudioCard ||
args.openRuntimeOptions ||
args.anilistStatus ||
args.anilistLogout ||
args.anilistSetup ||
args.anilistRetryQueue ||
args.dictionary ||
args.jellyfin ||
args.jellyfinLogin ||
args.jellyfinLogout ||
args.jellyfinLibraries ||
args.jellyfinItems ||
args.jellyfinSubtitles ||
args.jellyfinPlay ||
args.jellyfinRemoteAnnounce ||
args.jellyfinPreviewAuth ||
args.texthooker ||
args.help,
);
}
@@ -85,7 +85,10 @@ export function shouldAutoOpenFirstRunSetup(args: CliArgs): boolean {
return !hasAnyStartupCommandBeyondSetup(args);
}
function getPluginStatus(state: SetupState, pluginInstalled: boolean): SetupStatusSnapshot['pluginStatus'] {
function getPluginStatus(
state: SetupState,
pluginInstalled: boolean,
): SetupStatusSnapshot['pluginStatus'] {
if (pluginInstalled) return 'installed';
if (state.pluginInstallStatus === 'skipped') return 'skipped';
if (state.pluginInstallStatus === 'failed') return 'failed';

View File

@@ -253,7 +253,9 @@ export function createHandleFirstRunSetupNavigationHandler(deps: {
};
}
export function createOpenFirstRunSetupWindowHandler<TWindow extends FirstRunSetupWindowLike>(deps: {
export function createOpenFirstRunSetupWindowHandler<
TWindow extends FirstRunSetupWindowLike,
>(deps: {
maybeFocusExistingSetupWindow: () => boolean;
createSetupWindow: () => TWindow;
getSetupSnapshot: () => Promise<FirstRunSetupHtmlModel>;
@@ -279,9 +281,7 @@ export function createOpenFirstRunSetupWindowHandler<TWindow extends FirstRunSet
const render = async (): Promise<void> => {
const model = await deps.getSetupSnapshot();
const html = deps.buildSetupHtml(model);
await setupWindow.loadURL(
`data:text/html;charset=utf-8,${deps.encodeURIComponent(html)}`,
);
await setupWindow.loadURL(`data:text/html;charset=utf-8,${deps.encodeURIComponent(html)}`);
};
const handleNavigation = createHandleFirstRunSetupNavigationHandler({

View File

@@ -19,6 +19,7 @@ test('ipc bridge action main deps builders map callbacks', async () => {
playNextSubtitle: () => {},
shiftSubDelayToAdjacentSubtitle: async () => {},
sendMpvCommand: () => {},
getMpvClient: () => null,
isMpvConnected: () => true,
hasRuntimeOptionsManager: () => true,
}),

View File

@@ -16,6 +16,7 @@ test('handle mpv command handler forwards command and built deps', () => {
playNextSubtitle: () => {},
shiftSubDelayToAdjacentSubtitle: async () => {},
sendMpvCommand: () => {},
getMpvClient: () => null,
isMpvConnected: () => true,
hasRuntimeOptionsManager: () => true,
};

View File

@@ -15,6 +15,7 @@ test('ipc mpv command main deps builder maps callbacks', () => {
calls.push(`shift:${direction}`);
},
sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`),
getMpvClient: () => ({ connected: true, requestProperty: async () => null }),
isMpvConnected: () => true,
hasRuntimeOptionsManager: () => false,
})();
@@ -27,6 +28,7 @@ test('ipc mpv command main deps builder maps callbacks', () => {
deps.playNextSubtitle();
void deps.shiftSubDelayToAdjacentSubtitle('next');
deps.sendMpvCommand(['show-text', 'ok']);
assert.equal(typeof deps.getMpvClient()?.requestProperty, 'function');
assert.equal(deps.isMpvConnected(), true);
assert.equal(deps.hasRuntimeOptionsManager(), false);
assert.deepEqual(calls, [

View File

@@ -12,6 +12,7 @@ export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(
playNextSubtitle: () => deps.playNextSubtitle(),
shiftSubDelayToAdjacentSubtitle: (direction) => deps.shiftSubDelayToAdjacentSubtitle(direction),
sendMpvCommand: (command: (string | number)[]) => deps.sendMpvCommand(command),
getMpvClient: () => deps.getMpvClient(),
isMpvConnected: () => deps.isMpvConnected(),
hasRuntimeOptionsManager: () => deps.hasRuntimeOptionsManager(),
});

View File

@@ -0,0 +1,33 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolveProxyCommandOsdRuntime } from './mpv-proxy-osd';
function createClient() {
return {
connected: true,
requestProperty: async (name: string) => {
if (name === 'sid') return 3;
if (name === 'secondary-sid') return 8;
if (name === 'track-list') {
return [
{ id: 3, type: 'sub', title: 'Japanese', selected: true, external: false },
{ id: 8, type: 'sub', title: 'English Commentary', external: true },
];
}
return null;
},
};
}
test('resolveProxyCommandOsdRuntime formats the active primary subtitle track label', async () => {
const result = await resolveProxyCommandOsdRuntime(['cycle', 'sid'], () => createClient());
assert.equal(result, 'Subtitle track: Internal #3 - Japanese (active)');
});
test('resolveProxyCommandOsdRuntime formats the active secondary subtitle track label', async () => {
const result = await resolveProxyCommandOsdRuntime(
['set_property', 'secondary-sid', 'auto'],
() => createClient(),
);
assert.equal(result, 'Secondary subtitle track: External #8 - English Commentary');
});

View File

@@ -0,0 +1,100 @@
type MpvPropertyClientLike = {
connected: boolean;
requestProperty: (name: string) => Promise<unknown>;
};
type MpvSubtitleTrack = {
id?: number;
type?: string;
selected?: boolean;
external?: boolean;
lang?: string;
title?: string;
};
function getTrackOsdPrefix(command: (string | number)[]): string | null {
const operation = typeof command[0] === 'string' ? command[0] : '';
const property = typeof command[1] === 'string' ? command[1] : '';
const modifiesProperty =
operation === 'add' ||
operation === 'set' ||
operation === 'set_property' ||
operation === 'cycle' ||
operation === 'cycle-values' ||
operation === 'multiply';
if (!modifiesProperty) return null;
if (property === 'sid') return 'Subtitle track';
if (property === 'secondary-sid') return 'Secondary subtitle track';
return null;
}
function parseTrackId(value: unknown): number | null {
if (typeof value === 'number' && Number.isInteger(value)) {
return value;
}
if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed.length || trimmed === 'no' || trimmed === 'auto') {
return null;
}
const parsed = Number(trimmed);
if (Number.isInteger(parsed)) {
return parsed;
}
}
return null;
}
function normalizeTrackList(trackListRaw: unknown): MpvSubtitleTrack[] {
if (!Array.isArray(trackListRaw)) return [];
return trackListRaw
.filter(
(track): track is Record<string, unknown> => Boolean(track) && typeof track === 'object',
)
.map((track) => {
const id = parseTrackId(track.id);
return {
...track,
id: id === null ? undefined : id,
} as MpvSubtitleTrack;
});
}
function formatSubtitleTrackLabel(track: MpvSubtitleTrack): string {
const trackId = typeof track.id === 'number' ? track.id : -1;
const source = track.external ? 'External' : 'Internal';
const label = track.lang || track.title || 'unknown';
const active = track.selected ? ' (active)' : '';
return `${source} #${trackId} - ${label}${active}`;
}
export async function resolveProxyCommandOsdRuntime(
command: (string | number)[],
getMpvClient: () => MpvPropertyClientLike | null,
): Promise<string | null> {
const prefix = getTrackOsdPrefix(command);
if (!prefix) return null;
const client = getMpvClient();
if (!client?.connected) return null;
const property = prefix === 'Subtitle track' ? 'sid' : 'secondary-sid';
const [trackListRaw, trackIdRaw] = await Promise.all([
client.requestProperty('track-list'),
client.requestProperty(property),
]);
const trackId = parseTrackId(trackIdRaw);
if (trackId === null) {
return `${prefix}: none`;
}
const track = normalizeTrackList(trackListRaw).find(
(entry) => entry.type === 'sub' && entry.id === trackId,
);
if (!track) {
return `${prefix}: #${trackId}`;
}
return `${prefix}: ${formatSubtitleTrackLabel(track)}`;
}

View File

@@ -516,11 +516,11 @@ body.settings-modal-open #subtitleContainer {
}
#subtitleRoot
.word:not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(
.word-frequency-band-1
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
.word-frequency-band-5
):hover {
.word:not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(
.word-frequency-single
):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(
.word-frequency-band-4
):not(.word-frequency-band-5):hover {
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
border-radius: 3px;
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
@@ -558,9 +558,11 @@ body.settings-modal-open #subtitleContainer {
#subtitleRoot
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
.word-known
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(
.word-frequency-band-2
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5):hover {
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(
.word-frequency-band-1
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
.word-frequency-band-5
):hover {
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
}
@@ -636,15 +638,19 @@ body.settings-modal-open #subtitleContainer {
#subtitleRoot
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
.word-known
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(
.word-frequency-band-2
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)::selection,
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(
.word-frequency-band-1
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
.word-frequency-band-5
)::selection,
#subtitleRoot
.word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(
.word-known
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(
.word-frequency-band-2
):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)
):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(
.word-frequency-band-1
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
.word-frequency-band-5
)
.c::selection {
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;

View File

@@ -114,7 +114,8 @@ function installFakeDocument() {
function collectWordNodes(root: FakeElement): FakeElement[] {
return root.childNodes.filter(
(child): child is FakeElement => child instanceof FakeElement && child.className.includes('word'),
(child): child is FakeElement =>
child instanceof FakeElement && child.className.includes('word'),
);
}
@@ -137,17 +138,15 @@ function extractClassBlock(cssText: string, selector: string): string {
const ruleRegex = /([^{}]+)\{([^}]*)\}/g;
let match: RegExpExecArray | null = null;
let fallbackBlock = '';
const normalizedSelector = normalizeCssSelector(selector);
while ((match = ruleRegex.exec(cssText)) !== null) {
const selectorsBlock = match[1]?.trim() ?? '';
const selectorBlock = match[2] ?? '';
const selectors = selectorsBlock
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
const selectors = splitCssSelectors(selectorsBlock);
if (selectors.includes(selector)) {
if (selectors.some((entry) => normalizeCssSelector(entry) === normalizedSelector)) {
if (selectors.length === 1) {
return selectorBlock;
}
@@ -165,6 +164,53 @@ function extractClassBlock(cssText: string, selector: string): string {
return '';
}
function splitCssSelectors(selectorsBlock: string): string[] {
const selectors: string[] = [];
let current = '';
let parenDepth = 0;
for (const char of selectorsBlock) {
if (char === '(') {
parenDepth += 1;
current += char;
continue;
}
if (char === ')') {
parenDepth = Math.max(0, parenDepth - 1);
current += char;
continue;
}
if (char === ',' && parenDepth === 0) {
const trimmed = current.trim();
if (trimmed.length > 0) {
selectors.push(trimmed);
}
current = '';
continue;
}
current += char;
}
const trimmed = current.trim();
if (trimmed.length > 0) {
selectors.push(trimmed);
}
return selectors;
}
function normalizeCssSelector(selector: string): string {
return selector
.replace(/\s+/g, ' ')
.replace(/\(\s+/g, '(')
.replace(/\s+\)/g, ')')
.replace(/\s*,\s*/g, ', ')
.trim();
}
test('computeWordClass preserves known and n+1 classes while adding JLPT classes', () => {
const knownJlpt = createToken({
isKnown: true,
@@ -668,9 +714,21 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
);
assert.match(jlptTooltipKeyboardSelectedBlock, /opacity:\s*1;/);
assert.match(
const plainWordHoverBlock = extractClassBlock(
cssText,
/#subtitleRoot\s+\.word:not\(\.word-known\):not\(\.word-n-plus-one\):not\(\.word-name-match\):not\(\.word-frequency-single\):not\(\s*\.word-frequency-band-1\s*\):not\(\.word-frequency-band-2\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\s*\.word-frequency-band-5\s*\):hover\s*\{[\s\S]*?background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
'#subtitleRoot .word:not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5):hover',
);
assert.match(
plainWordHoverBlock,
/background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);/,
);
assert.match(
plainWordHoverBlock,
/color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
);
assert.match(
plainWordHoverBlock,
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
);
const coloredWordHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known:hover');
@@ -706,13 +764,31 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
assert.match(coloredCharHoverBlock, /background:\s*transparent;/);
assert.match(coloredCharHoverBlock, /color:\s*inherit\s*!important;/);
assert.match(
const jlptOnlyHoverBlock = extractClassBlock(
cssText,
/\.word:is\(\.word-jlpt-n1,\s*\.word-jlpt-n2,\s*\.word-jlpt-n3,\s*\.word-jlpt-n4,\s*\.word-jlpt-n5\):not\(\s*\.word-known\s*\):not\(\.word-n-plus-one\):not\(\.word-name-match\):not\(\.word-frequency-single\):not\(\.word-frequency-band-1\):not\(\s*\.word-frequency-band-2\s*\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\.word-frequency-band-5\):hover\s*\{[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
'#subtitleRoot .word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5):hover',
);
assert.match(
cssText,
/\.word:is\(\.word-jlpt-n1,\s*\.word-jlpt-n2,\s*\.word-jlpt-n3,\s*\.word-jlpt-n4,\s*\.word-jlpt-n5\):not\(\s*\.word-known\s*\):not\(\.word-n-plus-one\):not\(\.word-name-match\):not\(\.word-frequency-single\):not\(\.word-frequency-band-1\):not\(\s*\.word-frequency-band-2\s*\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\.word-frequency-band-5\)::selection[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
jlptOnlyHoverBlock,
/color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
);
assert.match(
jlptOnlyHoverBlock,
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
);
assert.match(
extractClassBlock(
cssText,
'#subtitleRoot .word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)::selection',
),
/color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
);
assert.match(
extractClassBlock(
cssText,
'#subtitleRoot .word:is(.word-jlpt-n1, .word-jlpt-n2, .word-jlpt-n3, .word-jlpt-n4, .word-jlpt-n5):not(.word-known):not(.word-n-plus-one):not(.word-name-match):not(.word-frequency-single):not(.word-frequency-band-1):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(.word-frequency-band-5)::selection',
),
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
);
const selectionBlock = extractClassBlock(cssText, '#subtitleRoot::selection');

View File

@@ -265,10 +265,7 @@ function renderWithTokens(
span.dataset.tokenIndex = String(segment.tokenIndex);
if (token.reading) span.dataset.reading = token.reading;
if (token.headword) span.dataset.headword = token.headword;
const frequencyRankLabel = getFrequencyRankLabelForToken(
token,
resolvedTokenRenderSettings,
);
const frequencyRankLabel = getFrequencyRankLabelForToken(token, resolvedTokenRenderSettings);
if (frequencyRankLabel) {
span.dataset.frequencyRank = frequencyRankLabel;
}
@@ -304,10 +301,7 @@ function renderWithTokens(
span.dataset.tokenIndex = String(index);
if (token.reading) span.dataset.reading = token.reading;
if (token.headword) span.dataset.headword = token.headword;
const frequencyRankLabel = getFrequencyRankLabelForToken(
token,
resolvedTokenRenderSettings,
);
const frequencyRankLabel = getFrequencyRankLabelForToken(token, resolvedTokenRenderSettings);
if (frequencyRankLabel) {
span.dataset.frequencyRank = frequencyRankLabel;
}
@@ -413,10 +407,7 @@ export function computeWordClass(
tokenRenderSettings?.bandedColors,
DEFAULT_FREQUENCY_RENDER_SETTINGS.bandedColors,
),
topX: sanitizeFrequencyTopX(
tokenRenderSettings?.topX,
DEFAULT_FREQUENCY_RENDER_SETTINGS.topX,
),
topX: sanitizeFrequencyTopX(tokenRenderSettings?.topX, DEFAULT_FREQUENCY_RENDER_SETTINGS.topX),
singleColor: sanitizeHexColor(
tokenRenderSettings?.singleColor,
DEFAULT_FREQUENCY_RENDER_SETTINGS.singleColor,

View File

@@ -43,7 +43,10 @@ test('ensureDefaultConfigBootstrap creates config dir and default jsonc only whe
});
assert.equal(fs.existsSync(configDir), true);
assert.equal(fs.readFileSync(path.join(configDir, 'config.jsonc'), 'utf8'), '{\n "logging": {}\n}\n');
assert.equal(
fs.readFileSync(path.join(configDir, 'config.jsonc'), 'utf8'),
'{\n "logging": {}\n}\n',
);
fs.writeFileSync(path.join(configDir, 'config.json'), '{"keep":true}\n');
fs.rmSync(path.join(configDir, 'config.jsonc'));

View File

@@ -162,7 +162,10 @@ export function ensureDefaultConfigBootstrap(options: {
const writeFileSync = options.writeFileSync ?? fs.writeFileSync;
mkdirSync(options.configDir, { recursive: true });
if (existsSync(options.configFilePaths.jsoncPath) || existsSync(options.configFilePaths.jsonPath)) {
if (
existsSync(options.configFilePaths.jsoncPath) ||
existsSync(options.configFilePaths.jsonPath)
) {
return;
}
@@ -178,7 +181,7 @@ export function resolveDefaultMpvInstallPaths(
platform === 'darwin'
? path.join(homeDir, 'Library', 'Application Support', 'mpv')
: platform === 'linux'
? path.join((xdgConfigHome?.trim() || path.join(homeDir, '.config')), 'mpv')
? path.join(xdgConfigHome?.trim() || path.join(homeDir, '.config'), 'mpv')
: path.join(homeDir, 'AppData', 'Roaming', 'mpv');
return {

View File

@@ -1,8 +1,9 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import test from 'node:test';
import { pathToFileURL } from 'node:url';
// @ts-expect-error Vendored Yomitan translator has no local TypeScript declarations.
import { Translator } from '../vendor/yomitan/js/language/translator.js';
import { resolveYomitanExtensionPath } from './core/services/yomitan-extension-paths';
type SortableTermEntry = {
matchPrimaryReading: boolean;
@@ -28,8 +29,20 @@ type SortableDefinition = {
index: number;
};
test('Translator prioritizes SubMiner term entries without changing dictionary index order', () => {
const translator = new Translator({});
async function loadTranslator(): Promise<{ new (...args: unknown[]): { [key: string]: unknown } }> {
const yomitanRoot = resolveYomitanExtensionPath({ cwd: process.cwd() });
assert.ok(yomitanRoot, 'Run `bun run build:yomitan` before Yomitan integration tests.');
const module = await import(
pathToFileURL(path.join(yomitanRoot, 'js', 'language', 'translator.js')).href
);
return module.Translator as { new (...args: unknown[]): { [key: string]: unknown } };
}
test('Translator prioritizes SubMiner term entries without changing dictionary index order', async () => {
const Translator = await loadTranslator();
const translator = new Translator({}) as {
_sortTermDictionaryEntries: (entries: unknown[]) => void;
};
const entries: SortableTermEntry[] = [
{
matchPrimaryReading: true,
@@ -64,8 +77,11 @@ test('Translator prioritizes SubMiner term entries without changing dictionary i
assert.equal(entries[0]?.dictionaryAlias, 'SubMiner Character Dictionary');
});
test('Translator prioritizes SubMiner definitions without changing dictionary index order', () => {
const translator = new Translator({});
test('Translator prioritizes SubMiner definitions without changing dictionary index order', async () => {
const Translator = await loadTranslator();
const translator = new Translator({}) as {
_sortTermDictionaryEntryDefinitions: (definitions: unknown[]) => void;
};
const definitions: SortableDefinition[] = [
{
dictionary: 'JMdict',

1
vendor/subminer-yomitan vendored Submodule

Submodule vendor/subminer-yomitan added at 9863d865e1

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Yomitan Action Popup</title>
<link rel="icon" type="image/png" href="/images/icon16.png" sizes="16x16">
<link rel="icon" type="image/png" href="/images/icon19.png" sizes="19x19">
<link rel="icon" type="image/png" href="/images/icon32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/images/icon38.png" sizes="38x38">
<link rel="icon" type="image/png" href="/images/icon48.png" sizes="48x48">
<link rel="icon" type="image/png" href="/images/icon64.png" sizes="64x64">
<link rel="icon" type="image/png" href="/images/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/css/material.css">
<link rel="stylesheet" type="text/css" href="/css/action-popup.css">
<script src="/js/pages/action-popup-main.js" type="module"></script>
</head>
<body>
<div id="loading">
Loading...
</div>
<div id="action-popup">
<div class="action-container action-select-profile" hidden>
<div class="action-item-left">
<h2 class="action-title">Profile</h2>
</div>
<div class="action-item-right">
<select tabindex="0" class="profile-select" id="profile-select">
</select>
</div>
</div>
<div class="action-container">
<div class="action-item-center">
<label class="toggle">
<input tabindex="0" type="checkbox" class="enable-search">
<div class="toggle-group">
<span class="toggle-on">On</span>
<span class="toggle-off">Off</span>
<span class="toggle-handle"></span>
</div>
</label>
<p class="tooltip">Hover over text to scan</p>
</div>
</div>
<div class="action-container action-buttons">
<button tabindex="0" type="button" class="low-emphasis action-open-settings" title="Settings" data-hotkey='["global:openSettingsPage","title","Settings ({0})"]'>
<div class="action-icon">
<span class="icon" data-icon="cog"></span>
</div>
</button>
<button tabindex="0" type="button" class="low-emphasis action-open-search" title="Search&#010;Shift+click to open here" data-hotkey='["global:openSearchPage","title","Search ({0})\nShift+click to open here"]'>
<div class="action-icon">
<span class="icon" data-icon="magnifying-glass"></span>
</div>
</button>
<button tabindex="0" type="button" class="low-emphasis action-open-info" title="Information" data-hotkey='["global:openInfoPage","title","Information ({0})"]'>
<div class="action-icon">
<span class="icon" data-icon="question-mark-circle"></span>
</div>
</button>
</div>
</div>
</body>
</html>

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Yomitan Background</title>
</head>
<body>
<script type="module" src="js/background/background-main.js"></script>
</body>
</html>

View File

@@ -1,376 +0,0 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2020-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* Variables */
:root {
--font-size-no-units: 14;
--font-size: calc(1px * var(--font-size-no-units));
--line-height-no-units: 20;
--line-height: calc(var(--line-height-no-units) / var(--font-size-no-units));
--background-color: #f8f9fa;
--text-color: #333333;
}
:root[data-theme=dark] {
--background-color: #1e1e1e;
--text-color: #cccccc;
}
/* Initilization */
body[data-loaded=true] #loading {
display: none;
}
body:not([data-loaded=true]) #action-popup {
display: none;
}
:root[data-mode=full] #action-popup {
display: initial;
}
#action-popup {
display: flex;
flex-flow: column nowrap;
align-items: center;
}
body {
padding: 5px;
margin: 0;
font-family: 'Segoe UI', Tahoma, sans-serif;
font-size: var(--font-size);
width: max-content;
height: max-content;
background-color: var(--background-color);
}
/* Toggle */
body[data-loaded=true] .toggle-group {
transition: transform 0.35s;
}
.toggle>input[type=checkbox] {
width: 0;
height: 0;
opacity: 0;
display: block;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.toggle>input[type=checkbox]:not(:checked)~.toggle-group {
transform: translateX(-50%);
}
.toggle {
box-sizing: border-box;
width: 85px;
height: 43px;
position: relative;
overflow: hidden;
border: 1px solid #245580;
border-radius: 4px;
display: inline-block;
}
.toggle-group {
position: absolute;
width: 200%;
left: 0;
top: 0;
bottom: 0;
user-select: none;
}
.toggle-on,
.toggle-off,
.toggle-handle {
display: flex;
justify-content: center;
align-items: center;
padding: 6px 12px;
font-size: var(--font-size);
font-weight: bold;
line-height: var(--line-height);
text-align: center;
white-space: nowrap;
cursor: pointer;
}
.toggle-on,
.toggle-off {
position: absolute;
top: 0;
bottom: 0;
margin: 0;
border: 0;
}
.toggle-on {
padding-right: 24px;
left: 0;
right: 50%;
color: #ffffff;
border-color: #2e6da4;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 3px 5px rgba(0, 0, 0, 0);
background-image: linear-gradient(225deg, #bc00ff, #00eeff);
background-repeat: repeat-x;
}
input[type=checkbox]:focus~.toggle-group>.toggle-on,
input[type=checkbox]~.toggle-group>.toggle-on:hover {
filter: grayscale(30%);
}
input[type=checkbox]:focus:not(:focus-visible)~.toggle-group>.toggle-on:not(:hover) {
background-image: linear-gradient(225deg, #bc00ff, #00eeff);
}
input[type=checkbox]:focus-visible~.toggle-group>.toggle-on {
filter: grayscale(30%);
}
input[type=checkbox]~.toggle-group>.toggle-on:active,
input[type=checkbox]~.toggle-group>.toggle-on:active:focus {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
.toggle-off {
padding-left: 24px;
left: 50%;
right: 0;
}
.toggle-handle {
position: relative;
margin: 0 auto;
padding-top: 0;
padding-bottom: 0;
height: 100%;
width: 0;
border-style: solid;
border-width: 0 1px;
border-radius: 4px;
border-color: #cccccc;
}
.toggle-off,
.toggle-handle {
color: #333333;
text-shadow: 0 1px 0 #ffffff;
background-color: #ffffff;
background-repeat: repeat-x;
}
input[type=checkbox]:focus~.toggle-group>.toggle-off,
input[type=checkbox]~.toggle-group>.toggle-off:hover,
input[type=checkbox]~.toggle-group>.toggle-handle:hover {
background-color: #e6e6e6;
}
input[type=checkbox]:focus:not(:focus-visible)~.toggle-group>.toggle-off:not(:hover) {
background-color: #ffffff;
}
input[type=checkbox]:focus-visible~.toggle-group>.toggle-off,
input[type=checkbox]~.toggle-group>.toggle-off:hover {
background-color: #e6e6e6;
}
input[type=checkbox]~.toggle-group>.toggle-off:active,
input[type=checkbox]~.toggle-group>.toggle-handle:active,
input[type=checkbox]~.toggle-group>.toggle-off:active:focus,
input[type=checkbox]~.toggle-group>.toggle-handle:active:focus {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
/* Action Containers */
h2.action-title {
padding: 0;
margin: 0;
font-size: 1.125em;
line-height: 1.5;
}
.action-icon:hover {
cursor: pointer;
}
.action-icon {
flex: 0 0 auto;
align-self: center;
}
.action-icon>.icon {
display: block;
background-color: var(--button-default-icon-color);
width: 1.5em;
height: 1.5em;
}
.low-emphasis {
position: relative;
}
.action-item-left {
flex: 1 1 auto;
align-self: center;
position: relative;
display: flex;
justify-content: flex-start;
}
.action-item-center {
flex: 1 1 auto;
align-self: center;
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
}
.action-item-right {
flex: 0 1 auto;
align-self: stretch;
display: flex;
flex-flow: row nowrap;
align-items: center;
align-content: center;
justify-content: flex-end;
}
.action-container:not([hidden]) {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px;
}
.action-container button {
flex: 0 0 auto;
}
.action-container.action-buttons {
gap: 0.25em;
}
/* Actions */
.action-container.action-select-profile {
position: relative;
gap: 0.5em;
}
select.profile-select {
width: 7em;
height: 100%;
box-sizing: border-box;
border: 0;
margin: 0;
cursor: pointer;
font-size: var(--font-size);
overflow: hidden;
text-overflow: ellipsis;
}
/* Tooltip */
.tooltip {
color: #808080;
padding: 0;
margin: 0;
margin-top: 0.25em;
}
.enable-dictionary-tooltip {
color: #f0ad4e;
}
.tooltip>a:link, a:visited {
color: #f0ad4e;
}
/* Mobile overrides */
:root[data-mode=full] #action-popup {
display: initial;
}
:root[data-mode=full] body {
min-width: 95%;
width: max-content;
font-size: calc(var(--font-size) * 2);
text-align: center;
}
:root[data-mode=full] .toggle-on, :root[data-mode=full] .toggle-off {
font-size: calc(var(--font-size) * 4);
}
:root[data-mode=full] .toggle-handle {
padding-left: 65px;
padding-right: 65px;
border-radius: 10px;
}
:root[data-mode=full] .toggle {
width: 100%;
padding-top: 37.7%;
}
:root[data-mode=full] #extension-info {
max-width: 95vw;
overflow-wrap: break-word;
}
:root[data-mode=full] select.profile-select {
font-size: calc(var(--font-size) * 2);
}
/* Fallback Mobile overrides */
/* Treat devices that can't hover as mobile devices */
@media (hover: none) {
#action-popup {
display: initial;
}
body {
min-width: 95%;
width: max-content;
font-size: calc(var(--font-size) * 2);
text-align: center;
}
.toggle-on, .toggle-off {
font-size: calc(var(--font-size) * 4);
}
.toggle-handle {
padding-left: 65px;
padding-right: 65px;
border-radius: 10px;
}
.toggle {
width: 100%;
padding-top: 37.7%;
}
#extension-info {
max-width: 95vw;
overflow-wrap: break-word;
}
select.profile-select {
font-size: calc(var(--font-size) * 2);
}
}
/* Dark mode before themes are applied
DO NOT use this for normal theming */
@media (prefers-color-scheme: dark) {
body:not([data-loaded=true]) {
color: #cccccc;
background-color: #1e1e1e;
}
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* stylelint-disable declaration-no-important */
#clipboard-rich-content-paste-target * {
background-image: none !important;
list-style-image: none !important;
content: none !important;
cursor: auto !important;
border-image-source: none !important;
offset-path: none !important;
-webkit-mask-image: none !important;
mask-image: none !important;
}
/* stylelint-enable declaration-no-important */

View File

@@ -1,132 +0,0 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2021-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the entrys of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
:root {
--pronunciation-annotation-color: #000000;
}
:root[data-theme=dark] {
--pronunciation-annotation-color: #ffffff;
}
.pronunciation-downstep-notation {
display: inline;
}
.pronunciation-text {
display: inline;
}
.pronunciation-mora {
display: inline-block;
position: relative;
}
.pronunciation-mora-line {
border-color: var(--pronunciation-annotation-color);
}
.pronunciation-mora[data-pitch=high]>.pronunciation-mora-line {
display: block;
user-select: none;
pointer-events: none;
position: absolute;
top: 0.1em;
left: 0;
right: 0;
height: 0;
border-top-width: 0.1em;
border-top-style: solid;
}
.pronunciation-mora[data-pitch=high][data-pitch-next=low]>.pronunciation-mora-line {
right: -0.1em;
height: 0.4em;
border-right-width: 0.1em;
border-right-style: solid;
}
.pronunciation-mora[data-pitch=high][data-pitch-next=low] {
padding-right: 0.1em;
margin-right: 0.1em;
}
.pronunciation-devoice-indicator {
display: block;
position: absolute;
left: 50%;
top: 50%;
width: 1.125em;
height: 1.125em;
border: calc(1.5em / var(--font-size-no-units)) dotted var(--danger-color);
border-radius: 50%;
box-sizing: border-box;
z-index: 1;
transform: translate(-50%, -50%);
}
.pronunciation-nasal-indicator {
display: block;
position: absolute;
right: -0.125em;
top: 0.125em;
width: 0.375em;
height: 0.375em;
border: calc(1.5em / var(--font-size-no-units)) solid var(--danger-color);
border-radius: 50%;
box-sizing: border-box;
z-index: 1;
}
.pronunciation-nasal-diacritic {
position: absolute;
width: 0;
height: 0;
opacity: 0;
}
.pronunciation-character {
display: inline;
}
.pronunciation-character-group {
display: inline-block;
position: relative;
}
.pronunciation-graph {
display: inline-block;
vertical-align: middle;
height: 1.5em;
}
.pronunciation-graph-line,
.pronunciation-graph-line-tail {
fill: none;
stroke: var(--pronunciation-annotation-color);
stroke-width: 5;
}
.pronunciation-graph-line-tail {
stroke-dasharray: 5 5;
}
.pronunciation-graph-dot {
fill: var(--pronunciation-annotation-color);
stroke: var(--pronunciation-annotation-color);
stroke-width: 5;
}
.pronunciation-graph-dot-downstep1 {
fill: none;
stroke: var(--pronunciation-annotation-color);
stroke-width: 5;
}
.pronunciation-graph-dot-downstep2 {
fill: var(--pronunciation-annotation-color);
}
.pronunciation-graph-triangle {
fill: none;
stroke: var(--pronunciation-annotation-color);
stroke-width: 5;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2021-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#permissions-origin-list {
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-rows: auto;
align-items: center;
justify-content: center;
}
.permissions-origin-index {
margin: 0 0.5em;
}
input[type=text].permissions-origin-input {
width: auto;
justify-self: stretch;
}
.permissions-origin-button {
justify-self: center;
margin-left: 0.5em;
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2016-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
iframe.yomitan-popup {
all: initial;
font-size: 1px;
background-color: #ffffff;
border: 1em solid #999999;
box-shadow: 0 0 10em rgba(0, 0, 0, 0.5);
position: fixed;
resize: none;
visibility: hidden;
z-index: 2147483647;
box-sizing: border-box;
}
iframe.yomitan-popup[data-theme=dark] {
background-color: #1e1e1e;
border-color: #666666;
}
iframe.yomitan-popup[data-outer-theme=dark] {
box-shadow: 0 0 10em rgba(255, 255, 255, 0.5);
}
iframe.yomitan-popup[data-outer-theme=none] {
box-shadow: none;
}
iframe.yomitan-popup[data-popup-display-mode=full-width] {
border-left: none;
border-right: none;
}
iframe.yomitan-popup[data-popup-display-mode=full-width][data-below=true] {
border-bottom: none;
}
iframe.yomitan-popup[data-popup-display-mode=full-width]:not([data-below=true]) {
border-top: none;
}

View File

@@ -1,164 +0,0 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2020-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
:root {
--font-size-no-units: 14;
--font-size: calc(1px * var(--font-size-no-units));
--line-height-no-units: 20;
--line-height: calc(var(--line-height-no-units) / var(--font-size-no-units));
--animation-duration: 0s;
--example-text-color: #333333;
--background-color: rgba(0, 0, 0, 0);
}
:root[data-loaded=true] {
--animation-duration: 0.25s;
}
:root[data-theme=dark] {
--example-text-color: #d4d4d4;
--background-color: rgba(0, 0, 0, 0);
}
html {
background-color: var(--background-color);
}
html.dark {
background-color: var(--background-color);
}
html,
body {
margin: 0;
padding: 0;
border: 0;
overflow: hidden;
width: 100%;
height: 100%;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: var(--font-size);
line-height: var(--line-height);
}
.content {
display: flex;
min-width: 100%;
min-height: 100%;
box-sizing: border-box;
padding: 1em;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
}
.content-body {
max-width: 100%;
width: 400px;
}
.top-options {
max-width: 100%;
display: flex;
flex-flow: row nowrap;
align-items: center;
}
.top-options-left {
flex: 1 1 auto;
}
.top-options-right {
flex: 0 0 auto;
}
.example-text-container {
position: relative;
}
.example-text {
display: block;
width: 100%;
font-size: 24px;
line-height: 1.25em;
height: 1.25em;
box-sizing: border-box;
border: 1px solid rgba(221, 221, 221, 0);
margin: 0;
padding: 0;
outline: none;
color: var(--example-text-color);
background-color: transparent;
white-space: pre;
transition: background-color var(--animation-duration) linear 0s, border-color var(--animation-duration) linear 0s;
}
.example-text:hover,
.example-text-input {
border-color: #dddddd;
}
.example-text[hidden] {
display: none;
}
.example-text-input {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.example-text-input:not([hidden])+.example-text {
visibility: hidden;
}
.popup-placeholder {
display: flex;
width: 100%;
height: 250px;
padding-top: 10px;
border: 1px solid rgba(0, 0, 0, 0);
flex-flow: column nowrap;
justify-content: center;
}
.placeholder-info {
flex: 0 1 auto;
visibility: hidden;
opacity: 0;
transition: opacity var(--animation-duration) linear 0s, visibility 0s linear var(--animation-duration);
}
.placeholder-info.placeholder-info-visible {
color: var(--example-text-color);
visibility: visible;
opacity: 1;
transition: opacity var(--animation-duration) linear 0s, visibility 0s linear 0s;
}
.theme-button {
display: inline-block;
margin-left: 0.5em;
text-decoration: none;
cursor: pointer;
white-space: nowrap;
line-height: 0;
}
.theme-button>input {
vertical-align: middle;
margin: 0 0.25em 0 0;
padding: 0;
}
.theme-button>span {
vertical-align: middle;
}
.theme-button:hover>span {
text-decoration: underline;
}

View File

@@ -1,489 +0,0 @@
:root {
--padding: 10px;
--padding-negative: calc(var(--padding) * -1);
--content-width: 700px;
--shadow-vertical: 0 1px 4px 0 var(--shadow-color), 0 2px 2px 0 var(--shadow-color);
--shadow-left: -1px 0 4px 0 var(--shadow-color), -2px 0 2px 0 var(--shadow-color);
--settings-group-horizontal-margin: 0;
--settings-group-inner-vertical-padding: 0.85em;
--settings-group-inner-horizontal-padding: 1.5em;
--settings-group-inner-horizontal-padding-half: calc(var(--settings-group-inner-horizontal-padding) * 0.5);
--settings-group-inner-horizontal-padding-half-wrappable: var(--settings-group-inner-horizontal-padding-half);
--settings-group-inner-horizontal-padding-fourth: calc(var(--settings-group-inner-horizontal-padding) * 0.25);
--settings-group-border-radius: 0.3em;
--settings-group-right-max-height: 40px;
--settings-group-wrap: nowrap;
--show-preview-label-height: 40px;
--font-size-no-units: 14;
--font-size: calc(1px * var(--font-size-no-units));
--font-size-small: 12px;
--outline-item-height: 40px;
--outline-item-icon-size: 32px;
--input-short-width: calc(var(--input-width-large) / 2 - var(--padding) / 2);
--input-short-height: 24px;
--input-medium-width: calc(var(--input-width-large) * 0.75);
--fab-button-size: 56px;
--fab-button-padding: 16px;
--modal-width: 600px;
--modal-height: 400px;
--modal-width-small: 400px;
--modal-height-small: 200px;
--modal-width-medium: 600px;
--modal-height-medium: 400px;
--modal-transition-offset: -64px;
--badge-size: 16px;
--link-color: var(--accent-color);
--link-color-hover: var(--accent-color-dark);
--separator-color1: #cccccc;
--separator-color2: #eeeeee;
--outline-item-background-color: rgba(13, 13, 13, 0);
--outline-item-background-color-hover: rgba(13, 13, 13, 0.15);
--warning-color: #96751c;
--warning-color-light: #edc75e;
--dim-background-color: rgba(0, 0, 0, 0.5);
--content-dimmer-color: rgba(0, 0, 0, 0.1);
--advanced-color: #6640be;
--advanced-color-lighter: hsl(258, 50%, 75%);
--advanced-color-transparent25: rgba(102, 64, 190, 0.5);
--modal-padding-horizontal: 1em;
--modal-padding-vertical: 0.625em;
--modal-padding-vertical-half: calc(var(--modal-padding-vertical) * 0.5);
--modal-button-spacing: 0.625em;
}
:root:not([data-loaded=true]) {
--animation-duration: 0s;
}
:root[data-theme=dark] {
--separator-color1: #333333;
--separator-color2: #222222;
}
/* Modal */
.modal {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-items: center;
overscroll-behavior: contain;
background-color: var(--dim-background-color);
outline: none;
z-index: 10000;
opacity: 1;
visibility: visible;
transition:
opacity var(--animation-duration2) ease-out,
visibility 0s linear;
}
.modal[hidden] {
opacity: 0;
visibility: hidden;
transition:
opacity var(--animation-duration2) ease-in,
visibility 0s linear var(--animation-duration2);
}
.modal[hidden]:not(.hidden-animating) {
display: none;
}
.modal-content {
max-width: 100%;
max-height: 100%;
width: var(--modal-width);
height: var(--modal-height);
background-color: var(--background-color-light);
flex: 0 1 auto;
border-radius: 0.5em;
transform: translate(0, 0);
transition:
transform var(--animation-duration2) ease-out,
width var(--animation-duration2) ease-in-out,
height var(--animation-duration2) ease-in-out,
border-radius var(--animation-duration2) ease-in-out;
box-shadow: var(--shadow-vertical);
display: flex;
flex-flow: column nowrap;
overflow: hidden;
}
.modal[hidden] .modal-content {
pointer-events: none;
}
.modal-content.modal-content-small {
width: var(--modal-width-small);
min-height: var(--modal-height-small);
height: auto;
max-height: 100%;
}
.modal-content.modal-content-medium {
width: var(--modal-width-medium);
min-height: var(--modal-height-medium);
height: auto;
max-height: 100%;
}
.modal-content.modal-content-full {
width: var(--content-width);
height: 100%;
transform: translate(0, 0);
border-radius: 0;
}
.modal[hidden] .modal-content {
transform: translate(0, var(--modal-transition-offset));
transition:
transform 0s linear var(--animation-duration2),
width var(--animation-duration2) ease-in-out,
height var(--animation-duration2) ease-in-out,
border-radius var(--animation-duration2) ease-in-out;
}
.modal-header {
flex: 0 0 auto;
padding: var(--modal-padding-vertical) var(--modal-padding-horizontal) var(--modal-padding-vertical-half);
display: flex;
width: 100%;
align-items: center;
box-sizing: border-box;
}
.modal-title {
font-size: 1.125em;
flex: 1 1 auto;
}
.modal-footer {
flex: 0 0 auto;
padding: var(--modal-padding-vertical-half) var(--modal-padding-horizontal) var(--modal-padding-vertical);
margin-right: calc(var(--modal-button-spacing) * -1);
margin-top: calc(var(--modal-button-spacing) * -1);
display: flex;
flex-flow: row wrap;
align-items: flex-end;
justify-items: flex-end;
justify-content: flex-end;
}
.modal-footer>* {
margin-right: var(--modal-button-spacing);
margin-top: var(--modal-button-spacing);
}
.modal-body {
flex: 1 1 auto;
overflow: auto;
padding: var(--modal-padding-vertical-half) var(--modal-padding-horizontal);
}
.modal-body-addon {
flex: 0 0 auto;
padding: var(--modal-padding-vertical-half) var(--modal-padding-horizontal);
}
.modal-body>.settings-item,
.modal-settings-group>.settings-item {
margin-left: calc(var(--modal-padding-horizontal) * -1);
}
.modal-body .settings-item {
margin-right: calc(var(--modal-padding-horizontal) * -1);
}
.modal-body .settings-item+.settings-item {
border-top: none;
}
.modal-body .settings-item-left {
padding-left: var(--modal-padding-horizontal);
padding-top: var(--settings-group-inner-horizontal-padding-fourth);
padding-bottom: var(--settings-group-inner-horizontal-padding-fourth);
}
.modal-body .settings-item-right {
padding-right: var(--modal-padding-horizontal);
padding-top: var(--settings-group-inner-horizontal-padding-fourth);
padding-bottom: var(--settings-group-inner-horizontal-padding-fourth);
}
.modal-body .settings-item-children {
padding-left: var(--modal-padding-horizontal);
padding-right: var(--modal-padding-horizontal);
padding-bottom: var(--settings-group-inner-horizontal-padding-fourth);
}
.modal.modal-left {
display: flex;
flex-flow: row nowrap;
width: 100%;
height: 100%;
background-color: transparent;
pointer-events: none;
}
.modal-content-container {
pointer-events: none;
width: 100%;
height: 100%;
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-items: center;
}
.modal-content-container>.modal-content,
.modal-content-container>.modal-content-dimmer {
pointer-events: auto;
}
.modal-content-container>.modal-content-dimmer {
background: var(--custom-css-modal-background);
width: var(--custom-css-dim-size);
height: 100%;
margin-right: calc(100% - var(--custom-css-dim-size));
position: absolute;
}
.modal-header-button-container {
margin-top: calc(-1 * var(--modal-padding-vertical-half));
margin-bottom: calc(-1 * var(--modal-padding-vertical-half));
}
.modal-header-button-group {
display: block;
position: relative;
width: var(--icon-button-size);
height: var(--icon-button-size);
}
.modal-header-button {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
button.icon-button.modal-header-button {
--button-content-color: var(--button-default-icon-color-light);
--button-hover-content-color: var(--button-default-icon-color);
--button-active-content-color: var(--button-default-icon-color);
}
button.icon-button.modal-header-button>.icon-button-inner>.icon {
transition: background-color var(--animation-duration) ease-in-out;
}
.modal-header-button[data-modal-action=expand],
.modal-header-button[data-modal-action=collapse] {
visibility: visible;
opacity: 1;
z-index: 1;
transition:
opacity var(--animation-duration2) ease-in-out 0s,
visibility 0s ease-in-out 0s;
}
.modal-content.modal-content-full .modal-header-button[data-modal-action=expand],
.modal-content:not(.modal-content-full) .modal-header-button[data-modal-action=collapse] {
visibility: hidden;
opacity: 0;
pointer-events: none;
z-index: 0;
transition:
opacity var(--animation-duration2) ease-in-out 0s,
visibility 0s ease-in-out var(--animation-duration2);
}
.modal-separator-line {
border-top: var(--thin-border-size) solid var(--separator-color1);
margin: 0 calc(var(--modal-padding-horizontal) * -1);
}
.modal-separator-line-light {
border-top: var(--thin-border-size) solid var(--separator-color2);
margin: 0 calc(var(--modal-padding-horizontal) * -1);
}
/* Settings styles */
.settings-group {
margin: 0 var(--settings-group-horizontal-margin);
padding: 0;
box-sizing: border-box;
background-color: var(--background-color-light);
box-shadow: var(--shadow-vertical);
border-radius: var(--settings-group-border-radius);
overflow-x: hidden;
}
.settings-group.settings-group-top-margin {
margin-top: 1.0125em;
}
.settings-item {
position: relative;
}
.settings-item:not([hidden]) {
display: block;
}
.settings-item-outer {
display: block;
width: 100%;
}
.settings-item-inner {
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
align-content: stretch;
width: 100%;
}
.settings-item-inner.settings-item-inner-wrappable {
flex-wrap: var(--settings-group-wrap);
}
.settings-item-left {
padding: var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding-half) var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding);
flex: 1 1 auto;
align-self: center;
position: relative;
}
.settings-item-left:last-child {
padding-right: var(--settings-group-inner-horizontal-padding);
}
.settings-item-right {
padding: var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding) var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding-half);
flex: 0 1 auto;
align-self: stretch;
max-height: var(--settings-group-right-max-height);
display: flex;
flex-flow: row nowrap;
align-items: center;
align-content: center;
justify-content: flex-end;
}
.settings-item-inner.settings-item-inner-wrappable>.settings-item-left {
padding-right: var(--settings-group-inner-horizontal-padding-half-wrappable);
}
.settings-item-inner.settings-item-inner-wrappable>.settings-item-right {
padding-left: var(--settings-group-inner-horizontal-padding-half-wrappable);
}
.settings-item-center {
padding: var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding) var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding);
flex: 0 1 100%;
align-self: flex-start;
text-align: center;
}
.settings-item+.settings-item {
border-top: var(--thin-border-size) solid var(--separator-color2);
}
.settings-item-description {
color: var(--text-color-light2);
}
.settings-item-right.open-panel-button-container {
padding: 0.25em 1em 0.25em 0.75em;
max-height: calc(var(--settings-group-right-max-height) + var(--settings-group-inner-vertical-padding) * 2);
}
.settings-item-children {
padding: 0em var(--settings-group-inner-horizontal-padding-half) var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding);
margin-top: 0;
}
.settings-item-children.settings-item-children-group {
padding: 0 0 0 calc(var(--settings-group-inner-horizontal-padding) + var(--settings-group-inner-horizontal-padding));
}
.settings-item-children.settings-item-children-group .settings-item {
border-top: var(--thin-border-size) solid var(--separator-color2);
}
.settings-item-children.settings-item-children-group .settings-item-left {
padding-left: 0;
}
.settings-item-children.settings-item-children-group .settings-item-inner.settings-item-inner-wrappable>.settings-item-left:not(:last-child) {
padding-right: calc(var(--settings-group-inner-horizontal-padding-half-wrappable) * 2);
}
.settings-item-children.settings-item-children-group .settings-item-inner.settings-item-inner-wrappable>.settings-item-right {
padding-left: 0;
}
.settings-item-children.settings-item-children-group .settings-item-children {
padding-left: 0;
}
.settings-item.settings-item-button,
a.settings-item.settings-item-button {
cursor: pointer;
color: var(--text-color);
text-decoration: none;
background-color: transparent;
transition: background-color var(--animation-duration) ease-in-out;
}
.settings-item.settings-item-button>.settings-item-inner,
.settings-item.settings-item-button>.settings-item-inner>.settings-item-left,
.settings-item.settings-item-button>.settings-item-inner>.settings-item-right {
margin-top: 0;
}
.settings-item.settings-item-button:hover,
.settings-item.settings-item-button:active {
background-color: var(--background-color);
}
.settings-item.settings-item-button .icon-button>.icon-button-inner>.icon {
transition: background-color var(--animation-duration) ease-in-out;
}
.settings-item.settings-item-button:hover .icon-button>.icon-button-inner>.icon,
.settings-item.settings-item-button:active .icon-button>.icon-button-inner>.icon {
background-color: var(--accent-color);
}
.settings-item-invalid-indicator {
display: none;
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0.5em;
background-color: var(--danger-color);
}
.settings-item[data-invalid=true] .settings-item-invalid-indicator {
display: block;
}
/* Settings item groups */
.settings-item-group {
margin-right: var(--padding-negative);
display: flex;
flex-flow: row nowrap;
align-items: center;
align-content: center;
justify-content: flex-end;
}
.settings-item-group.settings-item-group-wrap {
flex-wrap: wrap;
}
.settings-item-group-item {
flex: 0 1 auto;
padding-right: var(--padding);
}
.settings-item-group-item-label {
font-size: var(--font-size-small);
line-height: 1;
}
input[type=text].short-width,
input[type=number].short-width,
select.short-width {
width: var(--input-short-width);
}
input[type=text].medium-width,
input[type=number].medium-width,
select.medium-width {
width: var(--input-medium-width);
}
input[type=text].short-height,
input[type=number].short-height,
select.short-height {
height: var(--input-short-height);
margin-top: calc(var(--settings-group-right-max-height) - var(--input-short-height) - var(--font-size-small));
line-height: var(--line-height);
}
.settings-item-button-group-container {
max-height: none;
width: 100%;
}
.settings-item-button-group {
display: flex;
width: 100%;
flex-flow: row wrap;
max-height: none;
justify-content: flex-start;
margin-top: var(--padding-negative);
margin-right: var(--padding-negative);
}
.settings-item-button-group-item {
flex: 0 1 auto;
padding-top: var(--padding);
padding-right: var(--padding);
}
.settings-item-progress-report {
display: none;
font-weight: bold;
color: #4169e1;
}
.settings-item-error-report {
display: none;
font-weight: bold;
color: #8b0000;
}

View File

@@ -1,245 +0,0 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2020-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* Variables */
:root {
--search-scroll-container-horizontal-padding: 0.72em;
--query-horizontal-padding: 0;
--padding: calc(10em / var(--font-size-no-units));
--content-width-search: 700;
--content-width: calc(1em * var(--content-width-search) / var(--font-size-no-units));
--background-color: #ffffff;
--separator-color1: #cccccc;
--search-textbox-height: calc(var(--textarea-line-height) + var(--textarea-padding) * 2);
--search-textbox-min-height: calc(var(--textarea-line-height) + var(--textarea-padding) * 2);
--search-textbox-max-height: calc(var(--textarea-line-height) * 5 + var(--textarea-padding) * 2);
--cog-icon-size: 26px;
}
:root:not([data-loaded=true]) {
--animation-duration: 0s;
}
:root[data-theme=dark] {
--separator-color1: #333333;
}
/* Common styles */
:root {
height: 100%;
}
body {
background-color: var(--background-color);
margin: 0;
padding: 0;
color: var(--text-color);
height: 100%;
overflow: hidden;
}
.search-header {
padding-left: var(--search-scroll-container-horizontal-padding);
padding-right: var(--search-scroll-container-horizontal-padding);
}
h1 {
font-size: 2em;
line-height: 1.5em;
margin: 0;
padding: 0.25em 0 0;
font-weight: normal;
box-sizing: border-box;
border-bottom: calc(1em / (var(--font-size-no-units) * 2)) solid var(--separator-color1);
}
/* Search bar */
.search-textbox-container {
display: flex;
flex-flow: row nowrap;
width: 100%;
align-items: center;
margin: 0;
padding: 0;
border: 0;
}
#search-textbox {
color: var(--text-color);
flex: 1 1 auto;
box-sizing: border-box;
padding: var(--textarea-padding);
background-color: var(--input-background-color);
border-radius: 0;
line-height: var(--textarea-line-height);
border: 0;
outline: none;
width: 100%;
height: var(--search-textbox-height);
min-height: var(--search-textbox-min-height);
max-height: var(--search-textbox-max-height);
resize: none;
font-size: var(--font-size);
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
white-space: pre-wrap;
z-index: 1;
}
.search-button {
flex: 0 0 auto;
position: relative;
width: 2.5em;
height: var(--search-textbox-height);
min-height: var(--search-textbox-min-height);
max-height: var(--search-textbox-max-height);
background-color: var(--input-background-color);
border: 0;
padding: 0;
margin: 0;
cursor: pointer;
outline: none;
transition: background-color var(--animation-duration) ease-in-out;
border-radius: 0;
}
.search-button:hover,
.search-button:focus {
background-color: var(--input-background-color-dark);
}
.search-button:focus:not(:focus-visible):not(:hover) {
background-color: var(--input-background-color);
}
.search-button:focus-visible {
background-color: var(--input-background-color-dark);
}
.search-button:active,
.search-button:active:focus {
background-color: var(--input-background-color-darker);
}
.search-button>.icon {
display: block;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: var(--button-default-icon-color);
--icon-size: 16px 16px;
}
.clear-button {
flex: 0 0 auto;
position: relative;
width: 2.5em;
height: var(--search-textbox-height);
min-height: var(--search-textbox-min-height);
max-height: var(--search-textbox-max-height);
background-color: var(--input-background-color);
border: 0;
padding: 0;
margin: 0;
cursor: pointer;
outline: none;
transition: background-color var(--animation-duration) ease-in-out;
border-radius: 0;
}
.clear-button:hover,
.clear-button:focus {
background-color: var(--input-background-color-dark);
}
.clear-button:focus:not(:focus-visible):not(:hover) {
background-color: var(--input-background-color);
}
.clear-button:focus-visible {
background-color: var(--input-background-color-dark);
}
.clear-button:active,
.clear-button:active:focus {
background-color: var(--input-background-color-darker);
}
.clear-button>.icon {
display: block;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: var(--button-default-icon-color);
--icon-size: 16px 16px;
}
/* Search options */
#search-settings-button>.icon {
display: block;
background-color: var(--button-default-icon-color);
width: var(--cog-icon-size);
height: var(--cog-icon-size);
transition: var(--animation-duration) filter ease-in-out;
}
#search-settings-button>.icon:hover,
#search-settings-button>.icon:focus {
filter: invert(0.5);
}
#search-settings-button {
margin-right: 0;
float: right;
}
.search-options-right {
flex: 1;
}
.search-options {
display: flex;
flex-flow: row wrap;
margin: 0.5em 0 0 0;
align-items: center;
}
.search-option {
flex: 0 1 auto;
margin: 0.5em 2em 0.5em 0;
align-items: center;
cursor: pointer;
}
.search-option:not([hidden]) {
display: flex;
}
.search-option-label {
padding-left: 0.5em;
}
/* Search styles */
#intro {
overflow: hidden;
}
#intro>p {
margin: 0;
}
:root[data-search-mode=action-popup] #intro,
:root[data-search-mode=action-popup] #search-option-clipboard-monitor-container {
display: none;
}
:root[data-search-mode=action-popup],
:root[data-search-mode=action-popup] body {
width: 640px;
height: 480px;
}
/* Dark mode before themes are applied
DO NOT use this for normal theming */
@media (prefers-color-scheme: dark) {
:root:not([data-loaded=true]) {
background-color: #1e1e1e;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,259 +0,0 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2021-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the entrys of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* Glossary images */
.gloss-image-container {
display: inline-block;
white-space: nowrap;
max-width: 100%;
max-height: 100vh;
position: relative;
vertical-align: top;
line-height: 0;
font-size: calc(1em / var(--font-size-no-units));
overflow: hidden;
}
.gloss-image-link[data-background=true]>.gloss-image-container {
background-color: var(--gloss-image-background-color);
}
.gloss-image-link {
cursor: inherit;
color: var(--accent-color);
display: inline-block;
position: relative;
line-height: 1;
max-width: 100%;
}
.gloss-image-link:hover {
color: var(--accent-color-dark);
cursor: pointer;
}
.gloss-image-container-overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: calc(1em * var(--font-size-no-units));
line-height: var(--line-height);
display: table;
table-layout: fixed;
white-space: normal;
color: var(--text-color-light3);
}
.gloss-image-link[data-has-image=true][data-image-load-state=load-error] .gloss-image-container-overlay::after {
content: 'Image failed to load';
display: table-cell;
width: 100%;
height: 100%;
vertical-align: middle;
text-align: center;
padding: 0.25em;
}
.gloss-image-background {
--image: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: var(--text-color);
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center center;
-webkit-mask-mode: alpha;
-webkit-mask-size: contain;
-webkit-mask-image: var(--image);
mask-repeat: no-repeat;
mask-position: center center;
mask-mode: alpha;
mask-size: contain;
mask-image: var(--image);
display: none;
}
.gloss-image {
display: inline-block;
vertical-align: top;
object-fit: contain;
border: none;
outline: none;
}
.gloss-image-link[data-has-aspect-ratio=true] .gloss-image {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.gloss-image-link[data-image-rendering=pixelated] .gloss-image,
.gloss-image-link[data-image-rendering=pixelated] .gloss-image-background {
image-rendering: auto;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image,
.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background {
image-rendering: auto;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
: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 {
image-rendering: auto;
}
.gloss-image-link[data-has-aspect-ratio=true] .gloss-image-sizer {
display: inline-block;
width: 0;
vertical-align: top;
font-size: 0;
}
.gloss-image-link-text {
display: none;
line-height: var(--line-height);
}
.gloss-image-link-text::before {
content: '[';
}
.gloss-image-link-text::after {
content: ']';
}
.gloss-image-description {
display: block;
white-space: pre-line;
}
.gloss-image-link[data-appearance=monochrome] .gloss-image {
/* Workaround for coloring monochrome gloss images due to issues with masking using a canvas without loading extra media */
/* drop-shadow with 0.01px blur is at minimum required for Firefox to render the shadow when used on a canvas */
--shadow-settings: 0 0 0.01px var(--text-color);
filter: grayscale(1) opacity(0.5) drop-shadow(var(--shadow-settings)) drop-shadow(var(--shadow-settings)) saturate(1000%) brightness(1000%);
}
.gloss-image-link[data-size-units=em] .gloss-image-container {
font-size: 1em;
}
.gloss-image-link[data-vertical-align=baseline] { vertical-align: baseline; }
.gloss-image-link[data-vertical-align=sub] { vertical-align: sub; }
.gloss-image-link[data-vertical-align=super] { vertical-align: super; }
.gloss-image-link[data-vertical-align=text-top] { vertical-align: top; }
.gloss-image-link[data-vertical-align=text-bottom] { vertical-align: bottom; }
.gloss-image-link[data-vertical-align=middle] { vertical-align: middle; }
.gloss-image-link[data-vertical-align=top] { vertical-align: top; }
.gloss-image-link[data-vertical-align=bottom] { vertical-align: bottom; }
.gloss-image-link[data-collapsed=true],
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true] {
vertical-align: baseline;
}
.gloss-image-link[data-collapsed=true] .gloss-image-container,
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true] .gloss-image-container {
display: none;
position: absolute;
left: 0;
top: 100%;
z-index: 1;
}
.entry:nth-last-of-type(1):not(:nth-of-type(1)) .gloss-image-link[data-collapsed=true] .gloss-image-container,
:root[data-glossary-layout-mode^=compact] .entry:nth-last-of-type(1):not(:nth-of-type(1)) .gloss-image-link[data-collapsible=true] .gloss-image-container,
:root[data-glossary-layout-mode^=compact] .definition-item:nth-last-of-type(1) .gloss-image-link[data-collapsible=true] .gloss-image-container {
bottom: 100%;
top: auto;
}
.gloss-image-link[data-collapsed=true]:hover .gloss-image-container,
.gloss-image-link[data-collapsed=true]:focus .gloss-image-container,
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true]:hover .gloss-image-container,
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true]:focus .gloss-image-container {
display: block;
}
.gloss-image-link[data-collapsed=true] .gloss-image-link-text,
:root[data-glossary-layout-mode^=compact] .gloss-image-link[data-collapsible=true] .gloss-image-link-text {
display: inline;
}
.gloss-image-link[data-collapsed=true]~.gloss-image-description,
:root[data-glossary-layout-mode^=compact] .gloss-image-description {
display: inline;
}
/* Links */
.gloss-link-text {
vertical-align: baseline;
}
.gloss-link-external-icon {
display: inline-block;
vertical-align: middle;
width: calc(16em / var(--font-size-no-units));
height: calc(16em / var(--font-size-no-units));
margin-left: 0.25em;
background-color: var(--link-color);
position: relative;
}
/* Structured content glossary styles */
.gloss-sc-table-container {
display: block;
}
.gloss-sc-table {
table-layout: auto;
border-collapse: collapse;
}
.gloss-sc-thead,
.gloss-sc-tfoot,
.gloss-sc-th {
font-weight: bold;
background-color: var(--background-color-dark1);
}
.gloss-sc-th,
.gloss-sc-td {
border-width: calc(1em / var(--font-size-no-units));
border-style: solid;
border-color: var(--text-color-light2);
padding: 0.25em;
vertical-align: top;
}
.gloss-sc-ol,
.gloss-sc-ul {
padding-left: var(--list-padding2);
}
:root[data-glossary-layout-mode^=compact] .gloss-sc-ul[data-sc-content=glossary] {
display: inline;
list-style: none;
padding-left: 0;
}
:root[data-glossary-layout-mode^=compact] .gloss-sc-ul[data-sc-content=glossary] .gloss-sc-li {
display: inline;
}
:root[data-glossary-layout-mode^=compact] .gloss-sc-ul[data-sc-content=glossary] .gloss-sc-li:not(:first-child)::before {
white-space: pre-wrap;
content: var(--compact-list-separator);
display: inline;
color: var(--text-color-light3);
}
.gloss-sc-details {
padding-left: var(--list-padding1);
}
.gloss-sc-summary {
list-style-position: outside;
}

View File

@@ -1,21 +0,0 @@
:root:not([data-debug=true]) .debug-only {
display: none;
}
:root:not([data-advanced=true]) .advanced-only {
display: none;
}
:root:not([data-advanced=false]) .basic-only {
display: none;
}
:root:not([data-language=ja]):not([data-language=zh]):not([data-language=yue]) .jpzhyue-only {
display: none;
}
:root:not([data-language=ja]):not([data-language=zh]):not([data-language=yue]):not([data-language=ko]) .jpzhyueko-only {
display: none;
}
:root:is([data-language=ja], [data-language=zh], [data-language=yue], [data-language=ko]) .not-jpzhyueko {
display: none;
}
:root:not([data-language=ja]) .jp-only {
display: none;
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2019-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const ANKI_COMPACT_GLOSS_STYLES = `
ul[data-sc-content="glossary"] > li:not(:first-child)::before {
white-space: pre-wrap;
content: ' | ';
display: inline;
color: #777777;
}
ul[data-sc-content="glossary"] > li {
display: inline;
}
ul[data-sc-content="glossary"] {
display: inline;
list-style: none;
padding-left: 0;
}
`;
/**
* @returns {string}
*/
export function getAnkiCompactGlossStyles() {
return ANKI_COMPACT_GLOSS_STYLES.trim();
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,166 +0,0 @@
[
{
"selectors": [".pronunciation-downstep-notation"],
"styles": [
["display", "inline"]
]
},
{
"selectors": [".pronunciation-text"],
"styles": [
["display", "inline"]
]
},
{
"selectors": [".pronunciation-mora"],
"styles": [
["display", "inline-block"],
["position", "relative"]
]
},
{
"selectors": [".pronunciation-mora-line"],
"styles": [
["border-color", "currentColor"]
]
},
{
"selectors": [".pronunciation-mora[data-pitch=high]>.pronunciation-mora-line"],
"styles": [
["display", "block"],
["user-select", "none"],
["pointer-events", "none"],
["position", "absolute"],
["top", "0.1em"],
["left", "0"],
["right", "0"],
["height", "0"],
["border-top-width", "0.1em"],
["border-top-style", "solid"]
]
},
{
"selectors": [".pronunciation-mora[data-pitch=high][data-pitch-next=low]>.pronunciation-mora-line"],
"styles": [
["right", "-0.1em"],
["height", "0.4em"],
["border-right-width", "0.1em"],
["border-right-style", "solid"]
]
},
{
"selectors": [".pronunciation-mora[data-pitch=high][data-pitch-next=low]"],
"styles": [
["padding-right", "0.1em"],
["margin-right", "0.1em"]
]
},
{
"selectors": [".pronunciation-devoice-indicator"],
"styles": [
["display", "block"],
["position", "absolute"],
["left", "50%"],
["top", "50%"],
["width", "1.125em"],
["height", "1.125em"],
["border-radius", "50%"],
["box-sizing", "border-box"],
["z-index", "1"],
["transform", "translate(-50%, -50%)"],
["border", "1.5px dotted #c83c28"]
]
},
{
"selectors": [".pronunciation-nasal-indicator"],
"styles": [
["display", "block"],
["position", "absolute"],
["right", "-0.125em"],
["top", "0.125em"],
["width", "0.375em"],
["height", "0.375em"],
["border-radius", "50%"],
["box-sizing", "border-box"],
["z-index", "1"],
["border", "1.5px solid #c83c28"]
]
},
{
"selectors": [".pronunciation-nasal-diacritic"],
"styles": [
["position", "absolute"],
["width", "0"],
["height", "0"],
["opacity", "0"]
]
},
{
"selectors": [".pronunciation-character"],
"styles": [
["display", "inline"]
]
},
{
"selectors": [".pronunciation-character-group"],
"styles": [
["display", "inline-block"],
["position", "relative"]
]
},
{
"selectors": [".pronunciation-graph"],
"styles": [
["display", "inline-block"],
["vertical-align", "middle"],
["height", "1.5em"]
]
},
{
"selectors": [
".pronunciation-graph-line",
".pronunciation-graph-line-tail"
],
"styles": [
["fill", "none"],
["stroke-width", "5"],
["stroke", "currentColor"]
]
},
{
"selectors": [".pronunciation-graph-line-tail"],
"styles": [
["stroke-dasharray", "5 5"]
]
},
{
"selectors": [".pronunciation-graph-dot"],
"styles": [
["stroke-width", "5"],
["fill", "currentColor"],
["stroke", "currentColor"]
]
},
{
"selectors": [".pronunciation-graph-dot-downstep1"],
"styles": [
["fill", "none"],
["stroke-width", "5"],
["stroke", "currentColor"]
]
},
{
"selectors": [".pronunciation-graph-dot-downstep2"],
"styles": [
["fill", "currentColor"]
]
},
{
"selectors": [".pronunciation-graph-triangle"],
"styles": [
["fill", "none"],
["stroke-width", "5"],
["stroke", "currentColor"]
]
}
]

View File

@@ -1,901 +0,0 @@
{
"afb": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-afb-en",
"description": "Gulf Arabic to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-afb-en.zip"
}
]
},
"aii": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [
{
"name": "kty-aii-en-ipa",
"description": "Assyrian Neo-Aramaic IPA dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-aii-en-ipa.zip"
}
],
"terms": [
{
"name": "kty-aii-en",
"description": "Assyrian Neo-Aramaic to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-aii-en.zip"
}
]
},
"ang": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-ang-en",
"description": "Old English to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ang-en.zip"
}
]
},
"ar": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [
{
"name": "kty-ar-en-ipa",
"description": "Arabic IPA dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ar-en-ipa.zip"
}
],
"terms": [
{
"name": "kty-ar-en",
"description": "Arabic to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ar-en.zip"
}
]
},
"arz": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [
{
"name": "kty-ar-en-ipa",
"description": "Arabic IPA dictionary created from Wiktionary data.",
"homepage": "https://github.com/yomidevs/kaikki-to-yomitan/blob/master/downloads.md",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ar-en-ipa.zip"
}
],
"terms": [
{
"name": "kty-ar-en",
"description": "Arabic to English dictionary created from Wiktionary data.",
"homepage": "https://github.com/yomidevs/kaikki-to-yomitan/blob/master/downloads.md",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ar-en.zip"
}
]
},
"cs": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-cs-en",
"description": "Czech to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-cs-en.zip"
}
]
},
"de": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-de-en",
"description": "German to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-de-en.zip"
}
]
},
"el": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-el-en",
"description": "Greek to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-el-en.zip"
}
]
},
"en": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-en-en",
"description": "English to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-en-en.zip"
}
]
},
"enm": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-enm-en",
"description": "Middle English to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-enm-en.zip"
}
]
},
"eo": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-eo-en",
"description": "Esperanto to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-eo-en.zip"
}
]
},
"es": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-es-en",
"description": "Spanish to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-es-en.zip"
}
]
},
"fa": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-fa-en",
"description": "Persian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-fa-en.zip"
}
]
},
"fi": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-fi-en",
"description": "Finnish to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-fi-en.zip"
}
]
},
"fr": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-fr-en",
"description": "French to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-fr-en.zip"
}
]
},
"grc": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-grc-en",
"description": "Ancient Greek to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-grc-en.zip"
}
]
},
"haw": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "pukui-elbert-1986",
"description": "English to Hawaiian Dictionary from Pukui-Elbert in 1986",
"homepage": "https://github.com/bee-san/awesome-hawaiian-language",
"downloadUrl": "https://github.com/bee-san/awesome-hawaiian-language/releases/download/0.0.1/Pukui-Elbert-1986-Deduped-Yomitan.zip"
},
{
"name": "combined-dictionary",
"description": "English to Hawaiian Dictionary from Stephen (Kepano) Trussel",
"homepage": "https://github.com/bee-san/awesome-hawaiian-language",
"downloadUrl": "https://github.com/bee-san/awesome-hawaiian-language/releases/download/0.0.1/Combined-Hawaiian-Dictionary-2020-Mitch-Cleaned.zip"
},
{
"name": "hawaiian-place-names-2002",
"description": "Hawaiian Place Names, published 2002",
"homepage": "https://github.com/bee-san/awesome-hawaiian-language",
"downloadUrl": "https://github.com/bee-san/awesome-hawaiian-language/releases/download/0.0.1/Hawaii-place-names-2002-yomitan.zip"
}
]
},
"he": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-he-en",
"description": "Hebrew to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-he-en.zip"
}
]
},
"hi": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-hi-en",
"description": "Hindi to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-hi-en.zip"
}
]
},
"hu": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-hu-en",
"description": "Hungarian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-hu-en.zip"
}
]
},
"id": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-id-en",
"description": "Indonesian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-id-en.zip"
}
]
},
"it": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-it-en",
"description": "Italian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-it-en.zip"
}
]
},
"ja": {
"frequency": [
{
"name": "BCCWJ",
"description": "Based on the Balanced Corpus of Contemporary Written Japanese covering books, magazines, newspapers, blogs, forums, textbooks, and legal documents among others.",
"homepage": "https://github.com/Kuuuube/yomitan-dictionaries?tab=readme-ov-file#bccwj-suw-luw-combined",
"downloadUrl": "https://github.com/Kuuuube/yomitan-dictionaries/releases/download/yomitan-permalink/BCCWJ_SUW_LUW_combined.zip"
},
{
"name": "JPDB",
"description": "A frequency dictionary based on the corpus from the online Japanese dictionary and SRS system at https://jpdb.io.",
"homepage": "https://github.com/Kuuuube/yomitan-dictionaries?tab=readme-ov-file#jpdb-v22-frequency",
"downloadUrl": "https://github.com/Kuuuube/yomitan-dictionaries/releases/download/yomitan-permalink/JPDB_v2.2_Frequency_Kana.zip"
},
{
"name": "Jiten",
"description": "A frequency dictionary based on the corpus from the media stats database at https://jiten.moe",
"homepage": "https://jiten.moe/other",
"downloadUrl": "https://api.jiten.moe/api/frequency-list/download?downloadType=yomitan"
}
],
"grammar": [],
"kanji": [
{
"name": "KANJIDIC",
"description": "An English dictionary with readings, meanings, stroke order diagrams, frequency, grade level, JLPT level and frequency of kanji characters.",
"homepage": "https://github.com/yomidevs/jmdict-yomitan?tab=readme-ov-file#kanjidic-for-yomitan",
"downloadUrl": "https://github.com/yomidevs/jmdict-yomitan/releases/latest/download/KANJIDIC_english.zip"
}
],
"pronunciation": [],
"terms": [
{
"name": "Jitendex",
"description": "A free and openly licensed Japanese-to-English dictionary with example sentences, usage notes, etymology notes, cross references, antonyms, definition notes.",
"homepage": "https://jitendex.org",
"downloadUrl": "https://github.com/stephenmk/stephenmk.github.io/releases/latest/download/jitendex-yomitan.zip"
},
{
"name": "JMnedict",
"description": "A dictionary of Japanese proper names maintained by the Electronic Dictionary Research and Development Group.",
"homepage": "https://github.com/yomidevs/jmdict-yomitan?tab=readme-ov-file#jmnedict-for-yomitan",
"downloadUrl": "https://github.com/yomidevs/jmdict-yomitan/releases/latest/download/JMnedict.zip"
}
]
},
"km": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-km-en",
"description": "Khmer to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-km-en.zip"
}
]
},
"kn": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-kn-en",
"description": "Kannada to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-kn-en.zip"
}
]
},
"ko": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-ko-en",
"description": "Korean to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ko-en.zip"
}
]
},
"la": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-la-en",
"description": "Latin to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-la-en.zip"
}
]
},
"lv": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-lv-en",
"description": "Latvian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-lv-en.zip"
}
]
},
"mn": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-mn-en",
"description": "Mongolian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-mn-en.zip"
}
]
},
"mt": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [
{
"name": "kty-mt-en-ipa",
"description": "Maltese IPA dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-mt-en-ipa.zip"
}
],
"terms": [
{
"name": "kty-mt-en",
"description": "Maltese to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-mt-en.zip"
}
]
},
"nl": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-nl-en",
"description": "Dutch to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-nl-en.zip"
}
]
},
"no": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-nb-en",
"description": "Norwegian Bokmål to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-nb-en.zip"
},
{
"name": "kty-nn-en",
"description": "Norwegian Nynorsk to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-nn-en.zip"
}
]
},
"pl": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-pl-en",
"description": "Polish to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-pl-en.zip"
}
]
},
"pt": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-pt-en",
"description": "Portuguese to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-pt-en.zip"
}
]
},
"ro": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-ro-en",
"description": "Romanian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ro-en.zip"
}
]
},
"ru": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-ru-en",
"description": "Russian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-ru-en.zip"
},
{
"name": "opr-ru-en",
"description": "OpenRussian is a user-contributed, libre Russian dictionary including the accents, examples, audio, related words and synonyms.",
"homepage": "https://github.com/ImenaOphelia/openrussian-to-yomitan",
"downloadUrl": "https://github.com/ImenaOphelia/openrussian-to-yomitan/releases/latest/download/opr-ru-en.zip"
}
]
},
"scn": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-scn-en",
"description": "Sicillian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-scn-en.zip"
}
]
},
"sga": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-sga-en",
"description": "Old Irish to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-sga-en.zip"
}
]
},
"sh": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-sh-en",
"description": "Serbo-Croatian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-sh-en.zip"
}
]
},
"sq": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-sq-en",
"description": "Albanian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-sq-en.zip"
}
]
},
"sv": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-sv-en",
"description": "Swedish to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-sv-en.zip"
}
]
},
"th": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-th-en",
"description": "Thai to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-th-en.zip"
}
]
},
"tl": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-tl-en",
"description": "Tagalog to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-tl-en.zip"
}
]
},
"tok": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "toki-pona-to-chinese",
"description": "Toki Pona to Chinese dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-chinese.zip"
},
{
"name": "toki-pona-to-czech",
"description": "Toki Pona to Czech dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-czech.zip"
},
{
"name": "toki-pona-to-dutch",
"description": "Toki Pona to Dutch dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-dutch.zip"
},
{
"name": "toki-pona-to-english",
"description": "Toki Pona to English dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-english.zip"
},
{
"name": "toki-pona-to-esperanto",
"description": "Toki Pona to Esperanto dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-esperanto.zip"
},
{
"name": "toki-pona-to-french",
"description": "Toki Pona to French dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-french.zip"
},
{
"name": "toki-pona-to-german",
"description": "Toki Pona to German dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-german.zip"
},
{
"name": "toki-pona-to-indonesian",
"description": "Toki Pona to Indonesian dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-indonesian.zip"
},
{
"name": "toki-pona-to-italian",
"description": "Toki Pona to Italian dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-italian.zip"
},
{
"name": "toki-pona-to-polish",
"description": "Toki Pona to Polish dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-polish.zip"
},
{
"name": "toki-pona-to-portuguese",
"description": "Toki Pona to Portuguese dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-portuguese.zip"
},
{
"name": "toki-pona-to-russian",
"description": "Toki Pona to Russian dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-russian.zip"
},
{
"name": "toki-pona-to-slovak",
"description": "Toki Pona to Slovak dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-slovak.zip"
},
{
"name": "toki-pona-to-spanish",
"description": "Toki Pona to Spanish dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-spanish.zip"
},
{
"name": "toki-pona-to-turkish",
"description": "Toki Pona to Turkish dictionary",
"homepage": "https://github.com/bee-san/awesome-toki-pona",
"downloadUrl": "https://github.com/bee-san/awesome-toki-pona/releases/download/0.0.3/toki-pona-to-turkish.zip"
}
]
},
"tr": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-tr-en",
"description": "Turkish to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-tr-en.zip"
}
]
},
"uk": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-uk-en",
"description": "Ukranian to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-uk-en.zip"
}
]
},
"vi": {
"frequency": [],
"grammar": [],
"kanji": [],
"pronunciation": [],
"terms": [
{
"name": "kty-vi-en",
"description": "Vietnamese to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-vi-en.zip"
}
]
},
"yue": {
"frequency": [
{
"name": "Words.hk Frequency",
"description": "Frequency list of Cantonese terms and honzi provided by words.hk.",
"homepage": "https://github.com/MarvNC/wordshk-yomitan",
"downloadUrl": "https://github.com/MarvNC/wordshk-yomitan/releases/download/2024-09-17/YUE.Freq.Words.hk.Frequency.zip"
}
],
"grammar": [],
"kanji": [
{
"name": "Words.hk 粵典 漢字",
"description": "A free and open Cantonese dictionary with definitions and example sentences in Cantonese and English.",
"homepage": "https://github.com/MarvNC/wordshk-yomitan",
"downloadUrl": "https://github.com/MarvNC/wordshk-yomitan/releases/download/2025-07-09/Words.hk.Honzi.2025-07-08.zip"
}
],
"pronunciation": [],
"terms": [
{
"name": "Words.hk 粵典",
"description": "A free and open Cantonese dictionary with definitions and example sentences in Cantonese and English.",
"homepage": "https://github.com/MarvNC/wordshk-yomitan",
"downloadUrl": "https://github.com/MarvNC/wordshk-yomitan/releases/download/2025-07-09/Words.hk.2025-07-08.zip"
},
{
"name": "CC-Canto",
"description": "CC-Canto is an open source Cantonese dictionary project created by Pleco, intended to be used alongside the CC-CEDICT dictionary. It provides Cantonese specific words and definitions.",
"homepage": "https://github.com/MarvNC/cc-cedict-yomitan",
"downloadUrl": "https://github.com/MarvNC/cc-cedict-yomitan/releases/latest/download/CC-Canto.zip"
},
{
"name": "CC-CEDICT Canto",
"description": "CC-CEDICT is a continuation of the CEDICT project with the aim to provide a complete downloadable Chinese to English dictionary with pronunciation in pinyin for the Chinese characters. This version includes Cantonese readings provided by Pleco.",
"homepage": "https://github.com/MarvNC/cc-cedict-yomitan",
"downloadUrl": "https://github.com/MarvNC/cc-cedict-yomitan/releases/latest/download/CC-CEDICT.Canto.zip"
},
{
"name": "Cantodict",
"description": "CantoDict was a Cantonese-English dictionary created and maintained by Adam Sheik and public contributors.",
"homepage": "https://github.com/MarvNC/yomitan-dictionaries?tab=readme-ov-file#cantodict",
"downloadUrl": "https://github.com/MarvNC/yomichan-dictionaries/raw/master/dl/%5BCantonese%5D%20Cantodict.zip"
}
]
},
"zh": {
"frequency": [],
"grammar": [],
"kanji": [
{
"name": "CC-CEDICT",
"description": "A free and open Chinese-English dictionary provided by the CC-CEDICT project.",
"homepage": "https://github.com/MarvNC/cc-cedict-yomitan",
"downloadUrl": "https://github.com/MarvNC/cc-cedict-yomitan/releases/latest/download/CC-CEDICT.Hanzi.zip"
}
],
"pronunciation": [],
"terms": [
{
"name": "CC-CEDICT",
"description": "A free and open Chinese-English dictionary provided by the CC-CEDICT project.",
"homepage": "https://github.com/MarvNC/cc-cedict-yomitan",
"downloadUrl": "https://github.com/MarvNC/cc-cedict-yomitan/releases/latest/download/CC-CEDICT.zip"
},
{
"name": "kty-zh-en",
"description": "Chinese to English dictionary created from Wiktionary data.",
"homepage": "https://yomidevs.github.io/kaikki-to-yomitan/",
"downloadUrl": "https://pub-c3d38cca4dc2403b88934c56748f5144.r2.dev/releases/latest/kty-zh-en.zip"
}
]
}
}

View File

@@ -1,862 +0,0 @@
{
"ar": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
}
],
"arz": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
}
],
"cs": [
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"da": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"de": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"el": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
},
{
"modification": {
"action": "set",
"path": "scanning.length",
"value": 24
},
"description": "Increase the default scanning length from 16 to 24 characters."
}
],
"en": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
},
{
"modification": {
"action": "set",
"path": "sentenceParsing.terminationCharacters",
"value": [
{
"enabled": true,
"character1": "\"",
"character2": "\"",
"includeCharacterAtStart": false,
"includeCharacterAtEnd": false
},
{
"enabled": false,
"character1": "'",
"character2": "'",
"includeCharacterAtStart": false,
"includeCharacterAtEnd": false
},
{
"enabled": true,
"character1": ".",
"character2": null,
"includeCharacterAtStart": false,
"includeCharacterAtEnd": true
},
{
"enabled": true,
"character1": "!",
"character2": null,
"includeCharacterAtStart": false,
"includeCharacterAtEnd": true
},
{
"enabled": true,
"character1": "?",
"character2": null,
"includeCharacterAtStart": false,
"includeCharacterAtEnd": true
},
{
"enabled": true,
"character1": "…",
"character2": null,
"includeCharacterAtStart": false,
"includeCharacterAtEnd": true
}
]
},
"description": "Disable apostrophes as a sentence terminator."
}
],
"eo": [
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"es": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"fi": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"fr": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "translation.textReplacements.groups",
"value": [
[
{
"pattern": "l('|)",
"ignoreCase": true,
"replacement": ""
},
{
"pattern": "j('|)",
"ignoreCase": true,
"replacement": ""
},
{
"pattern": "d('|)",
"ignoreCase": true,
"replacement": ""
},
{
"pattern": "s('|)",
"ignoreCase": true,
"replacement": ""
}
]
]
},
"description": "Separating the l', j', d', s' from the word."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"he": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"hi": [
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"hu": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"id": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"it": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"ja": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "character"
},
"description": "Scan text one character at a time."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "letter"
},
"description": "Lookup words by letter in the dictionary."
}
],
"ko": [
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"kn": [
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"la": [
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"lv": [
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"mn": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"nl": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"no": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"pl": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"pt": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"ro": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"ru": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"sga": [
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"sh": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"sq": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"sv": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"tr": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
},
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"uk": [
{
"modification": {
"action": "set",
"path": "parsing.enableScanningParser",
"value": false
},
"description": "Turn off Yomitan's internal parser for languages with spaces."
}
],
"vi": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "word"
},
"description": "Scan text one word at a time (as opposed to one character)."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "word"
},
"description": "Lookup whole words in the dictionary."
}
],
"yue": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "character"
},
"description": "Scan text one character at a time."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "letter"
},
"description": "Lookup words by letter in the dictionary."
}
],
"zh": [
{
"modification": {
"action": "set",
"path": "scanning.scanResolution",
"value": "character"
},
"description": "Scan text one character at a time."
},
{
"modification": {
"action": "set",
"path": "translation.searchResolution",
"value": "letter"
},
"description": "Lookup words by letter in the dictionary."
}
]
}

View File

@@ -1,34 +0,0 @@
{
"$id": "customAudioList",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"type",
"audioSources"
],
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"const": "audioSourceList"
},
"audioSources": {
"type": "array",
"items": {
"type": "object",
"required": [
"url"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
}
}
}

View File

@@ -1,125 +0,0 @@
{
"$id": "dictionaryIndex",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"isoLanguageCode": {
"type": "string",
"description": "ISO language code (ISO 639-1 where possible, ISO 639-3 otherwise).",
"pattern": "^[a-z]{2,3}$"
}
},
"type": "object",
"description": "Index file containing information about the data contained in the dictionary.",
"required": [
"title",
"revision"
],
"properties": {
"title": {
"type": "string",
"description": "Title of the dictionary."
},
"revision": {
"type": "string",
"description": "Revision of the dictionary. This value is displayed, and used to check for dictionary updates."
},
"minimumYomitanVersion": {
"type": "string",
"description": "Minimum version of Yomitan that is compatible with this dictionary."
},
"sequenced": {
"type": "boolean",
"default": false,
"description": "Whether or not this dictionary contains sequencing information for related terms."
},
"format": {
"type": "integer",
"description": "Format of data found in the JSON data files.",
"enum": [1, 2, 3]
},
"version": {
"type": "integer",
"description": "Alias for format.",
"enum": [1, 2, 3]
},
"author": {
"type": "string",
"description": "Creator of the dictionary."
},
"isUpdatable": {
"type": "boolean",
"const": true,
"description": "Whether this dictionary contains links to its latest version."
},
"indexUrl": {
"type": "string",
"description": "URL for the index file of the latest revision of the dictionary, used to check for updates."
},
"downloadUrl": {
"type": "string",
"description": "URL for the download of the latest revision of the dictionary."
},
"url": {
"type": "string",
"description": "URL for the source of the dictionary, displayed in the dictionary details."
},
"description": {
"type": "string",
"description": "Description of the dictionary data."
},
"attribution": {
"type": "string",
"description": "Attribution information for the dictionary data."
},
"sourceLanguage": {
"$ref": "#/definitions/isoLanguageCode",
"description": "Language of the terms in the dictionary."
},
"targetLanguage": {
"$ref": "#/definitions/isoLanguageCode",
"description": "Main language of the definitions in the dictionary."
},
"frequencyMode": {
"type": "string",
"enum": ["occurrence-based", "rank-based"]
},
"tagMeta": {
"type": "object",
"description": "Tag information for terms and kanji. This object is obsolete and individual tag files should be used instead.",
"additionalProperties": {
"type": "object",
"description": "Information about a single tag. The object key is the name of the tag.",
"properties": {
"category": {
"type": "string",
"description": "Category for the tag."
},
"order": {
"type": "number",
"description": "Sorting order for the tag."
},
"notes": {
"type": "string",
"description": "Notes for the tag."
},
"score": {
"type": "number",
"description": "Score used to determine popularity. Negative values are more rare and positive values are more frequent. This score is also used to sort search results."
}
},
"additionalProperties": false
}
}
},
"anyOf": [
{
"required": ["format"]
},
{
"required": ["version"]
}
],
"dependencies": {
"isUpdatable": ["indexUrl", "downloadUrl"]
}
}

View File

@@ -1,35 +0,0 @@
{
"$id": "dictionaryKanjiBankV1",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"description": "Data file containing kanji information.",
"items": {
"type": "array",
"description": "Information about a single kanji character.",
"minItems": 4,
"maxItems": 4,
"items": [
{
"type": "string",
"description": "Kanji character.",
"minLength": 1
},
{
"type": "string",
"description": "String of space-separated onyomi readings for the kanji character. An empty string is treated as no readings."
},
{
"type": "string",
"description": "String of space-separated kunyomi readings for the kanji character. An empty string is treated as no readings."
},
{
"type": "string",
"description": "String of space-separated tags for the kanji character. An empty string is treated as no tags."
}
],
"additionalItems": {
"type": "string",
"description": "A meaning for the kanji character."
}
}
}

View File

@@ -1,47 +0,0 @@
{
"$id": "dictionaryKanjiBankV3",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"description": "Data file containing kanji information.",
"items": {
"type": "array",
"description": "Information about a single kanji character.",
"minItems": 6,
"maxItems": 6,
"additionalItems": false,
"items": [
{
"type": "string",
"description": "Kanji character.",
"minLength": 1
},
{
"type": "string",
"description": "String of space-separated onyomi readings for the kanji character. An empty string is treated as no readings."
},
{
"type": "string",
"description": "String of space-separated kunyomi readings for the kanji character. An empty string is treated as no readings."
},
{
"type": "string",
"description": "String of space-separated tags for the kanji character. An empty string is treated as no tags."
},
{
"type": "array",
"description": "Array of meanings for the kanji character.",
"items": {
"type": "string",
"description": "A meaning for the kanji character."
}
},
{
"type": "object",
"description": "Various stats for the kanji character.",
"additionalProperties": {
"type": "string"
}
}
]
}
}

Some files were not shown because too many files have changed in this diff Show More