From 5cab1fa94b142c413cdcb0aeb417b5b5b261801d Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Fri, 12 Jun 2026 14:54:13 +0800 Subject: [PATCH] feat(css): override document layout also apply to hyphenation, closes #4529 (#4546) --- apps/readest-app/.claude/memory/MEMORY.md | 4 +++ .../.claude/memory/android-cdp-e2e-lane.md | 21 +++++++++++++ .../android-hyphen-selection-bounds-1553.md | 30 +++++++++++++++++++ .../footnote-aside-namespace-order-4438.md | 20 +++++++++++++ .../paragraph-mode-accidental-exit-4474.md | 16 ++++++++++ apps/readest-app/src/utils/style.ts | 8 ++--- 6 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 apps/readest-app/.claude/memory/android-cdp-e2e-lane.md create mode 100644 apps/readest-app/.claude/memory/android-hyphen-selection-bounds-1553.md create mode 100644 apps/readest-app/.claude/memory/footnote-aside-namespace-order-4438.md create mode 100644 apps/readest-app/.claude/memory/paragraph-mode-accidental-exit-4474.md diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md index db6513ac..cc9f5e93 100644 --- a/apps/readest-app/.claude/memory/MEMORY.md +++ b/apps/readest-app/.claude/memory/MEMORY.md @@ -33,6 +33,7 @@ - [Custom fonts disappear on cloud sync (#4410)](custom-fonts-reincarnation-4410.md) — CRDT remove-wins: re-import-after-delete needs a `reincarnation` token or the pull re-applies the tombstone; `addFont`/`addTexture` minted none; fix mirrors dictionary (both cases) + OPDS token style; coverage matrix per kind ## Testing +- [Android CDP e2e lane](android-cdp-e2e-lane.md) — `pnpm test:android`: adb+CDP drives the installed app on device/emulator; discover-don't-assume targeting, injected hyphenation, MediaStore VIEW transient open (canonical `_data` path gotcha), per-section frame restore; CI workflow with KVM emulator + debug x86_64 APK (no signing secrets) - [CDP Android WebView profiling](cdp-android-webview-profiling.md) — drive the on-device Readest WebView via adb+CDP to run JS probes/benchmarks inside the live app (no rebuild); gotchas: locked device freezes fetch (not invoke), visible:false throttles setTimeout, `__TAURI_INTERNALS__.convertFileSrc/invoke` always present, books in internal `/data/user/0/...`, fs `read{rid,len}` last-8-bytes=nread, `fs|close` not ACL-allowed, curl mishandles WebView HTTP framing - [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 @@ -40,6 +41,7 @@ - [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 +- [Android hyphen selection bounds (#1553)](android-hyphen-selection-bounds-1553.md) — Blink paints the start handle on the paragraph's LAST hyphen when a touch selection starts at the first word of a hyphenated paragraph (`ComputePaintingSelectionStateForCursor` lacks the generated-text offset remap, hyphen offsets {0,1}); drag-extend re-anchors base there. Fix = repair jumped anchor + suppress handles (empty-commit needs one painted frame) + `SelectionRangeEditor` custom handles; multicol NOT required; desktop/iOS unaffected - [Android NativeFile vs RemoteFile I/O](android-nativefile-remotefile-io.md) — why NativeFile is slow (4-IPC/chunk + bridge serialization, tauri#9190); RemoteFile CANNOT replace it on Android (asset-protocol Range broken: start>0 → "Failed to fetch", start-0 capped at 1,024,000; plain no-Range fetch returns full file at 281 MB/s); measured 44/100/281 MB/s; speedups = handle-reuse (2.3×), whole-file asset loader (6.3×), or fix wry upstream. Verified live via CDP. - [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 - [Android Open-with intent flow (#4521)](android-open-with-intent-flow.md) — "Open with"/"Send to" pipeline: `NativeBridgePlugin.kt::handleIntent` → `shared-intent` → `useAppUrlIngress` → `useOpenWithBooks` (VIEW=transient→reader, SEND=library+upload). Telegram fails where file-manager works on TWO axes: cold-start delivery (fixed by #4527, on dev NOT released v0.11.4) + foreign-private-file read (Telegram FileProvider non-persistable grant vs shared-storage FUSE real-path). adb MediaStore VIEW repro tests pipeline but CANNOT reproduce the read axis (MANAGE_EXTERNAL_STORAGE bypasses grant) @@ -65,6 +67,8 @@ - [Table dark-mode tint regression (#4419)](table-dark-mode-tint-4419.md) — `blockquote, table *` color-mix tint in `getColorStyles` must stay gated on `overrideColor` (gate added #2377, removed #4055, re-broke → #4419); safe now that #4392 light-bg rewriters handle #4028 zebra legibility; SAME rule paints vertical-TOC `.space`/▉ spacer cells (▉ U+2589 = blank glyph, contours=0) → "spacing changes" symptom; both fixed by the gate - [Double-click-drag turns page (#4524)](dblclick-drag-pageturn-4524.md) — web double-click+drag selection also turned the page; 1st click's deferred single-click (250ms) fires mid-drag while 2nd-click button held; fix = `isMouseDown` flag in `iframeEventHandlers.ts` gates the deferred `postSingleClick`; synthetic-repro gotchas (shadow-DOM iframe walk, chained-repro timing pollution, reload to re-bind listeners) - [RSVP font face/family (#4519)](rsvp-font-settings-4519.md) — RSVP word was hardcoded `font-mono`; now mirrors the reader font via `getBaseFontFamily(viewSettings)` (new export in `style.ts`, shares `buildFontFamilyLists` with `getFontStyles`). Overlay renders in the TOP document (portal to body) where custom + basic Google fonts are mounted; known gap = built-in CJK web fonts only in top doc when `isCJKEnv()` +- [Footnote aside border line (#4438)](footnote-aside-namespace-order-4438.md) — v0.11.4 regression: stray horizontal line below footnote marker. #4383 inlined custom `@font-face` BEFORE the `@namespace epub` (which lived in `getPageLayoutStyles`), invalidating it per CSS spec → namespaced `aside[epub|type~="footnote"]{display:none}` dropped → book's `aside{border:3px double}` showed. Only with custom fonts loaded. Fix = hoist `@namespace` to front of `getStyles`. Repro needs XHTML (`epub:type` namespaced only in XML); Playwright `setContent` parses HTML and won't reproduce +- [Paragraph-mode accidental exit + off-center bar (#4474)](paragraph-mode-accidental-exit-4474.md) — backdrop/center taps exited focus mode (stray "too high/low" taps); `ParagraphBar` only reshows on mousemove (no touch reshow) so can't just delete tap-exits → new `paragraph-show-controls` event reveals the bar instead. Also bar `absolute`→`fixed`: it centered on the gridcell which a pinned sidebar shifts right, while the paragraph centers on the `fixed inset-0` overlay/viewport ## Library Fixes - [Tauri menu append race (#4389)](tauri-menu-append-race-4389.md) — un-awaited `Menu.append()` (async IPC) in `BookshelfItem.tsx` → context-menu items shuffle order every open (native only, invisible in jsdom); fix = single `await Menu.new({ items })` of ordered `MenuItemOptions`; order/inclusion extracted to pure `getBookContextMenuItemIds` for unit testing diff --git a/apps/readest-app/.claude/memory/android-cdp-e2e-lane.md b/apps/readest-app/.claude/memory/android-cdp-e2e-lane.md new file mode 100644 index 00000000..99c23761 --- /dev/null +++ b/apps/readest-app/.claude/memory/android-cdp-e2e-lane.md @@ -0,0 +1,21 @@ +--- +name: android-cdp-e2e-lane +description: "pnpm test:android — CDP+adb e2e lane driving the installed app on a device/emulator; harness design, gotchas, CI workflow" +metadata: + node_type: memory + type: project + originSessionId: 16f94822-04b0-4be3-a47e-8a2e3cab290a +--- + +New test tier (branch fix/android-hyphen-selection-1553): `pnpm test:android` → `scripts/test-android.sh` → `vitest.android.config.mts` (node env, serial, retry 1) → `src/__tests__/android/*.android.test.ts`. Helpers in `src/__tests__/android/helpers/`: `adb.ts` (tap/longPress/`motionGesture` = one-shell DOWN/MOVE/UP chain), `cdp.ts` (forward `webview_devtools_remote_`, node:http discovery with Host header, `CdpPage.evaluate` async-IIFE), `reader.ts` (fixture open + probes). Soft-skips without adb/device/app. Covers the [[android-hyphen-selection-bounds-1553]] cases: prone long-press → app handles, drag repair clamp, tap dismissal, handle-drag extension, mid-paragraph native handles, cross-page corner-dwell auto-turn. + +Design principles (per chrox): discover-don't-assume (find a hyphenated on-screen paragraph at runtime, start in main text via `gotoChapter('chapter\\s*4')`), force hyphenation by injecting `p{hyphens:auto!important;text-align:justify!important}` into section docs (app settings irrelevant), poll-don't-sleep (`waitFor`), fixture `sample-alice.epub` opened TRANSIENTLY via MediaStore VIEW intent. + +Gotchas: +- MediaStore `_data` is the canonical `/storage/emulated/0/...` path — query `_data LIKE '%/'`, NOT the `/sdcard/` symlink you pushed to. `content query --projection` takes ONE column or space-separated (not comma). VIEW with `--grant-read-uri-permission` works on a permissionless fresh install. +- Multi-section books: each section is its own iframe — record + restore pagination via the TARGET section's frame x (`c.index === sectionIndex`), not `contents[0]`. +- Corner auto-turn (#1354) zone is the reading area INSET by content margins — a drag point in the bottom margin is ignored by `cornerAt`; aim ~4% inside the text area. +- adb `input motionevent` 5px moves are under touch slop → no pointermove; make post-turn drag movements large. +- Verified green on Xiaomi 13 (physical) AND fresh Pixel_9_Pro AVD (`emulator -avd Pixel_9_Pro`, install the aarch64 dev APK), ~21 s. + +CI: `.github/workflows/android-e2e.yml` — ubuntu-latest + KVM udev rule, debug x86_64 APK (`tauri android build --debug --target x86_64`; gradle skips keystore.properties when absent so NO signing secrets), `reactivecircus/android-emulator-runner@v2` (api 34, AVD snapshot cached), nightly + workflow_dispatch + `e2e-android` PR label; not PR-blocking. NOTE: emulator-runner not SHA-pinned yet (repo convention pins by SHA). diff --git a/apps/readest-app/.claude/memory/android-hyphen-selection-bounds-1553.md b/apps/readest-app/.claude/memory/android-hyphen-selection-bounds-1553.md new file mode 100644 index 00000000..f579795f --- /dev/null +++ b/apps/readest-app/.claude/memory/android-hyphen-selection-bounds-1553.md @@ -0,0 +1,30 @@ +--- +name: android-hyphen-selection-bounds-1553 +description: "#1553 Android selection breaks on first word of hyphenated paragraphs — Blink generated-hyphen bounds bug, full RCA + app-side repair/suppress fix" +metadata: + node_type: memory + type: project + originSessionId: 16f94822-04b0-4be3-a47e-8a2e3cab290a +--- + +Issue #1553 root cause (verified live on Xiaomi 13, WebView 147, via [[cdp-android-webview-profiling]]): + +**Upstream**: filed as crbug **522869957** (2026-06-12, by chrox, with full analysis + repro + screenshot). Pre-existing same-root-cause report: crbug **41496034** (Jan 2024, "­" framing — why searches missed it; P2 Available, on "Rendering Core 2026 Fixit" hotlist; MS triager confirmed soft-hyphen cause in Feb 2024). Cross-link comments posted on both. + +**Blink bug** — `LayoutSelection::ComputePaintingSelectionStateForCursor` (third_party/blink/renderer/core/editing/layout_selection.cc) compares `paint_range_` offsets (paragraph **IFC text-content space**, via `OffsetMapping::GetTextContentOffset`) against `position.TextOffset()` — but auto/soft-hyphen fragments are **layout-generated text with self-relative offsets {0,1}**. So a touch selection starting at IFC offset 0 (first word of a paragraph) makes EVERY hyphen fragment in that paragraph report `kStart` → each records a start bound (`SelectionBoundsRecorder`, last paint wins) → **native start handle is drawn at the paragraph's LAST hyphen**. The highlight itself is correct because the sibling path `ComputeSelectionStatus(InlineCursor&)` HAS the `IsLayoutGeneratedText()` remap; the bounds path lacks it. Still unfixed upstream as of Chromium main (June 2026); seemingly unreported. + +Key facts: +- Trigger: touch selection (handles visible — `ShouldRecordSelection` gates on `IsHandleVisible()`, so desktop/mouse unaffected; iOS=WebKit unaffected) + selection start at IFC offset ≤1 + generated hyphens in the same paragraph. **Multicol NOT required** (reproduced in a plain top-document div). +- Worse than cosmetic: long-press **drag-extension re-anchors the base by hit-testing the bogus bound** → observed anchor jump 0→325 (offset just before last hyphen), selection became [53,325] instead of [0,53]. Explains "select upward works" workaround (upward drags anchor on the correct end bound). +- Auto-hyphens show up as separate ~0.3em rects in `Range.getClientRects()` on hyphenated lines — usable as a generated-hyphen detector. +- **JS `removeAllRanges()+addRange()` does NOT hide already-visible touch handles synchronously** — the empty selection must commit through one painted frame (double-rAF in the iframe window) before re-adding; then handles stay hidden for all later JS selection updates. + +Fix (branch fix/android-hyphen-selection-1553): detection utils in `src/utils/sel.ts` (`isHyphenHandleBugProneRange`, `repairJumpedSelectionRange`, `hasTrailingHyphenRectPattern`); gesture-initial anchor capture + touchend sanitize in `useTextSelector` (repair jumped anchor → suppress handles via empty-commit → `makeSelection(handlesSuppressed)`); `SelectionRangeEditor.tsx` renders custom drag handles (reuses `Handle` + extracted `buildRangeFromPoints`/`getHandlePositionsFromRange` in annotatorUtil) for suppressed selections. Gated to Android app + exact bug condition; flipping to always-custom-handles later = drop the proneness gate. + +Gotcha: `input motionevent DOWN/MOVE/UP` (adb) simulates long-press-drag; `input swipe x y x y 700` simulates plain long-press. + +Two post-fix races found by chrox & fixed (commits c6e9f48, 9a63157): +1. **Tap-dismiss resurrection** — an Android tap doesn't clear the selection, it COLLAPSES it to a caret at the tap point, with selectionchange ~10-20ms AFTER touchend; the tap's touchend re-entered sanitize (fallback prone-check on the still-valid old selection), the collapse raced into the double-rAF window (also MUTATING the held Range in place), and makeSelection committed a collapsed range as a suppressed selection → "empty custom handles at the tap spot". Fix = gesture gate (`if (!initial) return`) + post-rAF abort when `sel.rangeCount > 0 || finalRange.collapsed`. +2. **Extent overshoot** — the corrupted drag has TWO modes: base re-anchors at the bogus bound (anchor jump, Argentina test) OR the EXTENT lands there while the anchor stays at 0 (range then CONTAINS the initial anchor → jump-repair doesn't fire; observed +1013 chars). Fix = `rangeFromAnchorToPoint`: rebuild [gesture-initial anchor → caret at last native touch position] (`pointerPos`, reset per-gesture in handleTouchStart), fallback to jump-repair. + +Verify-build gotcha: `pnpm dev-android` snapshots the Next bundle at build START — an amend after kickoff ships the PRE-amend frontend (grep of out/ chunks for comment text is a FALSE-POSITIVE check; compare out/ mtime vs commit time instead). diff --git a/apps/readest-app/.claude/memory/footnote-aside-namespace-order-4438.md b/apps/readest-app/.claude/memory/footnote-aside-namespace-order-4438.md new file mode 100644 index 00000000..bc2b353e --- /dev/null +++ b/apps/readest-app/.claude/memory/footnote-aside-namespace-order-4438.md @@ -0,0 +1,20 @@ +--- +name: footnote-aside-namespace-order-4438 +description: Footnote aside border line regression — @font-face inlined before @namespace invalidated the namespaced footnote-hiding selector +metadata: + node_type: memory + type: project + originSessionId: 788943e6-fede-4c8f-828c-695ca873f178 +--- + +#4438 (v0.11.4 regression): a stray horizontal line appeared below the footnote/annotation marker because the footnote `