diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md index 78902181..b3d7bedb 100644 --- a/apps/readest-app/.claude/memory/MEMORY.md +++ b/apps/readest-app/.claude/memory/MEMORY.md @@ -27,15 +27,22 @@ - [KOSync CFI spine resolution](kosync-cfi-spine-resolution.md) — convert via the CFI's own spine (`getXPointerFromCFI`/`getCFIFromXPointer`), never `new XCFI(primaryDoc, primaryIndex)`; primaryIndex lags during scroll → spine-mismatch throw - [Empty-start CFI sync bug](empty-start-cfi-sync.md) — `epubcfi(/6/24!/4,,/20/1:58)` (empty-start range) from the cfi-inert skip-link transitional window; jumps to wrong section end; `isMalformedLocationCfi` → discard the synced value in `useProgressSync` (NOT the local open path); foliate fix doesn't repair already-synced values +## Testing +- [Tauri Rust↔JS parser parity tests](tauri-parser-parity-tests.md) — #4369 native Rust EPUB/MOBI parser; how to cross-check vs foliate-js in the `.tauri.test.ts` WebView suite (CWD disk path for Rust, Vite URL for JS, normalizer-based compare, cover presence-only, desc whitespace-collapse); the `dcterms:modified`→`published` divergence fix + ## Build & Vendoring - [pdfjs vendor wasm decoders](pdfjs-vendor-wasm-decoders.md) — scanned PDFs blank in CI build only (0.11.2 regression); pdfjs 5.7.x moved JBIG2 to `jbig2.wasm`, `copy-pdfjs-wasm` allow-list dropped it; `cpx` no-errors on empty glob; local stale `public/vendor` (gitignored, not refreshed by `tauri build`) masked it; fix = copy `wasm/*` +## Platform Compat +- [Window-state sanitizer (#4398)](window-state-sanitize-4398.md) — Windows launch crash (WebView2 0x80070057) from invalid `.window-state.json` (`-32000` minimized sentinel / `0×0`); our plugin already has upstream #253 fix so bad files are stale; defense-in-depth `window-state-sanitizer` plugin registered BEFORE window-state (plugin init = registration order); coord threshold `-16000` (~halfway to the -32000 sentinel; real desktops sit a few thousand px off origin) keeps multi-monitor negatives + ## Feature Notes - [Manage Cache + iOS container layout](manage-cache-ios-layout.md) — `'Cache'` base = `Library/Caches/` only (not all of Caches); iOS `Documents/Inbox` cleared too; WebKit cache + tmp out of reach; never touch App Support - [D-pad Navigation](dpad-navigation.md) — Android TV remote / keyboard arrow navigation design, key files, and pitfalls - [Cloudflare Workers WebSocket](cloudflare-workers-websocket.md) — use fetch() Upgrade pattern (not `ws` npm); CF delivers binary frames as Blob (must serialize async decodes) - [Share-a-Book Feature (in progress)](share-feature.md) — locked decisions for the /s/{token} share-link feature; plan at ~/.claude/plans/ok-we-will-learn-cosmic-acorn.md - [readest.koplugin i18n](koplugin-i18n.md) — gettext loader at `apps/readest.koplugin/i18n.lua`, `.po` catalog at `locales//translation.po`, extract/apply scripts in `scripts/` +- [koplugin cover upload](koplugin-cover-upload.md) — #4374 uploadBook only shipped cached cloud covers; local-origin books uploaded blank. Fix = `extractLocalCover` via `FileManagerBookInfo:getCoverImage(nil, file)` → `writeToFile(path,"png")`. KOReader checkout at `/Users/chrox/dev/koreader` ## Patterns - [Virtuoso + OverlayScrollbars](virtuoso_overlayscrollbars.md) — useOverlayScrollbars hook integration for overlay scrollbars on mobile webviews @@ -50,6 +57,7 @@ - Safe area insets flow: Native plugin -> useSafeAreaInsets hook -> component styles - Dropdown menus use `DropdownContext` (not blur-based) for screen reader compat - [Foliate touch-listener capture phase](foliate-touch-listener-capture-phase.md) — to suppress reader gestures from the app, use `{capture:true}`; the paginator registers bubble-phase doc listeners first (during `view.open()`) +- [iframe cross-realm instanceof](iframe-cross-realm-instanceof.md) — app-bundle code (style.ts, iframeEventHandlers.ts) runs in top realm; `iframeEl instanceof Element` is ALWAYS false → guards silently drop all iframe elements (passes jsdom, dead in app). Duck-type `'closest' in target` instead. Bit PR #4391's touch routing + applyTableStyle dedupe ## Workflow - [Test file filter](feedback_test_file_filter.md) — use `pnpm test ` without `--` to run a single file diff --git a/apps/readest-app/.claude/memory/iframe-cross-realm-instanceof.md b/apps/readest-app/.claude/memory/iframe-cross-realm-instanceof.md new file mode 100644 index 00000000..5e55f7cf --- /dev/null +++ b/apps/readest-app/.claude/memory/iframe-cross-realm-instanceof.md @@ -0,0 +1,25 @@ +--- +name: iframe-cross-realm-instanceof +description: "App-bundle code handling foliate iframe DOM must not use `instanceof Element/HTMLElement` — cross-realm, always false" +metadata: + node_type: memory + type: project + originSessionId: 2e8d274e-a4da-4d63-8afb-d7a600d560b2 +--- + +Reader content lives in foliate iframes, each with its **own JS realm**. App-bundle +code (e.g. `src/utils/style.ts`, `iframeEventHandlers.ts`) runs in the **top window +realm**, so its `Element`/`HTMLElement` constructors differ from the iframe's. + +`someIframeEl instanceof Element` (top-realm `Element`) is **always `false`** — verified: +`Element === iframeWin.Element` is false. So any guard like `if (!(target instanceof Element)) return` +silently drops every real iframe element. Functions still pass jsdom unit tests (single realm) +yet are dead in the running app. + +**Use duck-typing instead**: `if (!target || !('closest' in target)) return;` then +`(target as Element).closest(...)`. For node-type checks use `node.nodeType === Node.ELEMENT_NODE` +(numeric constant, realm-safe) or check `classList`/`closest` presence. + +Seen in PR #4391 (wide-table horizontal scroll): the touch `findWrapper` and `applyTableStyle`'s +re-entrancy guard both used `instanceof`, so the touch routing never fired and the dedupe guard +never tripped. Related: [[foliate-touch-listener-capture-phase]]. diff --git a/apps/readest-app/.claude/memory/koplugin-cover-upload.md b/apps/readest-app/.claude/memory/koplugin-cover-upload.md new file mode 100644 index 00000000..41caed4a --- /dev/null +++ b/apps/readest-app/.claude/memory/koplugin-cover-upload.md @@ -0,0 +1,18 @@ +--- +name: koplugin-cover-upload +description: "koplugin cover system — where local/cloud covers come from, and the" +metadata: + node_type: memory + type: reference + originSessionId: 95cdff70-ce2c-4077-82a8-922f9578a22f +--- + +readest.koplugin cover handling (in `apps/readest.koplugin/library/`): + +- **Local book covers** come from coverbrowser.koplugin's `BookInfoManager` (a hard dependency). `coverprovider.get_local_cover(file_path)` returns `BIM:getBookInfo(file_path, true).cover_bb` (a downscaled thumbnail bb) and kicks off background extraction on a miss. +- **Native-resolution cover** for a file (no open doc needed): `require("apps/filemanager/filemanagerbookinfo"):getCoverImage(nil, file_path)` — opens+closes the doc itself, honors KOReader custom covers, returns a blitbuffer. Same `(nil, file)` call form `calibre.koplugin` uses. Write it to PNG with `bb:writeToFile(path, "png")` (pcall-wrapped internally) then `bb:free()`. +- **Cloud covers** are downloaded to `DataStorage:getSettingsDir()/readest_covers/.png` (`cloud_covers.covers_dir()`); cloud storage key is `/Readest/Books//cover.png` (`build_cover_key`), which matches readest's `getCoverFilename` (`/cover.png`). + +**Issue #4374 (fixed):** `syncbooks.uploadBook` only shipped a `cover.png` if one was *already cached* under `readest_covers/.png` from a prior cloud download — so books that originated locally in KOReader uploaded with no cover and showed blank in readest. Fix = `syncbooks.extractLocalCover(file_path, dst_png)` (extracts via `getCoverImage` → writeToFile), called from `uploadBook` when `has_cover` is false. Only file-upload path is `librarywidget.lua` "Upload to Cloud" → `uploadBook` (FileManager `addToReadest` only stages a local row). + +Tests: network/live parts of `syncbooks.lua` aren't unit-tested (manual matrix in `docs/library-design.md`); `extractLocalCover` IS tested by injecting a fake `apps/filemanager/filemanagerbookinfo` via `package.loaded` in `spec/library/syncbooks_spec.lua`. A real KOReader checkout lives at `/Users/chrox/dev/koreader` for verifying KOReader APIs. See [[koplugin-i18n]]. diff --git a/apps/readest-app/.claude/memory/tauri-parser-parity-tests.md b/apps/readest-app/.claude/memory/tauri-parser-parity-tests.md new file mode 100644 index 00000000..749e92a2 --- /dev/null +++ b/apps/readest-app/.claude/memory/tauri-parser-parity-tests.md @@ -0,0 +1,29 @@ +--- +name: tauri-parser-parity-tests +description: "How to test the native Rust EPUB/MOBI parser against foliate-js in the Tauri WebView suite, plus the dcterms:modified→published parity gotcha" +metadata: + node_type: memory + type: reference + originSessionId: 90d14df3-59ce-438f-ae91-b72e9d2b6f99 +--- + +PR #4369 added native Rust EPUB/MOBI parsers (`src-tauri/src/epub_parser.rs`, `mobi_parser.rs`, shared `parser_common.rs`) with foliate-js fallback. Parity between the two parsers is the key risk. + +**Cross-language parity test harness** (`src/__tests__/tauri/epub-parser-parity.tauri.test.ts`): +- The `.tauri.test.ts` suite (run by `scripts/test-tauri.sh` / `pnpm test:tauri`, included only by `vitest.tauri.config.mts`) is the ONLY env where both parsers coexist: Rust via `invoke()` (`./tauri-invoke.ts`), foliate-js via `DocumentLoader`. Default `pnpm test` excludes `**/*.tauri.test.ts`. +- Rust commands read an absolute on-disk path → build it from `process.env.CWD` (injected by the tauri config = readest-app dir): `${CWD}/src/__tests__/fixtures/data/`. The JS side fetches the SAME file via a Vite URL: `new URL('../fixtures/data/', import.meta.url).href` (same pattern as `paginator-stabilization.browser.test.ts`). +- Compare via the app's own normalizers so you compare user-visible values, not raw parser shapes: `formatTitle`, `formatAuthors(authors, lang)` (Rust gives `string[]`, foliate gives `string|Contributor|array` — both through formatAuthors), `getPrimaryLanguage`, `formatPublisher`, `formatDescription`. +- **Cover is presence-only**: Rust downscales/re-encodes the cover to a ~512px JPEG (`parser_common::maybe_resize_cover`), so bytes never match foliate's original — assert `(rust.coverBase64 != null) === ((await js.getCover()) != null)`. +- **Description needs whitespace-collapse**: foliate collapses an internal source newline to a space; Rust preserves the raw `\n`. Compare `formatDescription(x).replace(/\s+/g,' ').trim()`. +- Section `href` is undefined on foliate `SectionItem` — compare sections by `{id,size,linear}` only. `linear` can be `null`. +- Strongest test = open the same EPUB twice through `DocumentLoader` (with `{nativeFilePath}` → Rust prefetch, and without → pure zip.js) and assert identical `metadata`, sections, `toc`, and `computeBookNav` output (toc tree + per-section fragment CFIs/sizes). This validates `parse_epub_full` prefetch + the parallelized TOC pipeline are behavior-preserving in one shot. `isTauriAppPlatform()` is true in the webview (`.env.tauri` sets `NEXT_PUBLIC_APP_PLATFORM=tauri`) so the prefetch fires. +- No `.mobi`/`.azw3` fixture exists anywhere in the repo → MOBI parity is NOT covered by this harness; add a Kindle fixture to extend. Rust MOBI is covered by `mobi_parser`'s own unit tests. +- Offline verification trick (no chromedriver/tauri-driver locally): dump foliate `book.metadata` via a temp node vitest test, dump Rust `parse_epub_metadata_sync`/`compute_partial_md5` via a temp `#[cfg(test)]` mod using `env!("CARGO_MANIFEST_DIR")/../src/__tests__/fixtures/data`, diff. Lets you lock every expected value before CI runs the WebView suite. + +**Running the `.tauri.test.ts` suite locally on macOS** (no chromedriver/tauri-driver needed — `tauri-plugin-webdriver` 0.2 embeds an axum WebDriver server with a `platform/macos.rs` WKWebView impl, default port 4445): +- `pnpm test:tauri` runs `scripts/test-tauri.sh`: starts `next dev` on :3000, `tauri dev --features webdriver --no-watch`, waits for :4445/status, then `vitest --config vitest.tauri.config.mts`. Its cleanup does `lsof -ti :3000 | xargs kill` — so it KILLS whatever is on :3000 (e.g. another worktree's dev server). To avoid that, copy the script and shift `DEV_PORT`/`next dev -p`/`--config devUrl` to a free port (e.g. 3100). Run via `pnpm exec bash