feat(reader): 跨平台两端对齐 —— @layer + 最小化 <br> 扫描 - #737
Conversation
|
测试epub: |
|
:has() 和 @layer 这两个属性在低版本的webview上应该不会生效,看看是不是加一下兜底的方案 |
|
感谢评审,兜底方案已加上(64039039 + 5f6b62b),先交代兼容性结论,再说方案。 兼容性结论(caniuse 已核实)
受影响的真实场景:旧发行版的 WebKitGTK(如 Ubuntu 20.04 自带 2.28)、iOS < 15.4、厂商冻结 WebView 的安卓机、企业固定版 WebView2。 兜底设计:能力检测 + 三档降级 启动时检测
性能说明(不会退回 #686 的问题):回退扫描虽然会遍历所有块级元素,但只读 顺带修复:书籍自带 text-wrap: pretty 时,两端对齐会变得难看(借鉴 readest 先解释问题。 修复:两端对齐开启时,断行交由阅读器接管——对对齐会覆盖到的容器(html、body、 验证:vitest 24/24(新增能力检测、三档 CSS 变体、无 :has 扫描、查询抛异常回退等 8 个用例),桌面 tsc 与 lint 通过,reader.html 已重新生成。桌面正常引擎行为零变化。 |
|
最新的低版本 WebView 降级方案方向没问题。不过开启两端对齐时写入的 inline |
|
另外移动端和桌面端目前有不少重复的 capability 检测、CSS 生成和对齐扫描逻辑,后续建议抽成公共实现,避免两端继续漂移。 |
|
两条意见都已处理(13084115):
验证:core vitest 598/598、双端 tsc/lint、桌面与安卓模拟器实测对齐与 |
… br scan Replace the DOM-scan justified-text.js (PR codedogQBY#686) with a light, cross-platform strategy shared by mobile WebView and desktop foliate: - CSS fallback in @layer readany-justify: body { text-align: justify } is the default, but unlayered book styles always win, so author alignment can never be overridden. :where() keeps specificity at 0. - :where(*:has(> br)) { text-align: start } so direct-br poetry/lyrics lines are not stretched; using :has(> br) (direct child) avoids cascading start onto unrelated siblings of a br-bearing outer container. - Code/tables/captions/forms excluded with text-align: start. - A tiny JS pass reads the computed alignment of only br-bearing block-level elements and pins author-aligned ones (center/right/end, however expressed) inline, so the book's alignment survives our start rule. Pins are tagged and un-pinned on disable (clean undo) or on vertical/fixed layouts. - Vertical/fixed documents are tagged data-readany-vertical (via getDirection on desktop, isVerticalDoc on mobile) so the justify CSS scopes to horizontal text. - justifyBodyText setting retained (default on); when off the layer is not injected.
…marker class=vrtl/vltr is an authoring hint for reflow-to-vertical, but it does not produce vertical writing by itself — the document is vertical only when a CSS writing-mode is actually applied (book's own stylesheet or ReadAny's injected :root.vrtl fallback). Detecting the class alone misclassifies a horizontal document with a bare class as vertical. Detect vertical writing purely from computed writing-mode: - body's writing-mode, then - the first non-inert child of body (some EPUBs set it there), mirroring foliate's getDirection. This is applied consistently on both platforms: - mobile reader.template.html isVerticalDoc: drop class check - mobile justified-text.js isVerticalDoc: drop class check - desktop document-loader.ts getDirection: add missing first-child check Verified (Chromium headless): body-vertical, class+CSS, and first-child-vertical all detect true; bare class without CSS now correctly detects false.
…t pin) apply() previously unpinned first, then re-pinned. When re-run after the justify stylesheet was already injected (e.g. applySettings ran, then a section load triggered applyDocStyles), reading the alignment saw 'start' (from :has(> br)) instead of the book's center, so the unpin dropped the pinned center and the re-pin did nothing — centered poetry went left. Make apply() idempotent: only unpin when justify is disabled or the layout is unsupported; when enabled, preserve() re-pins (reading the already-pinned inline center keeps it stable).
…ight) Mirror the mobile fix: only unpin when justify is disabled or the layout is unsupported. Re-running syncJustifyForDoc on later renders (after the justify stylesheet is already injected) must not clear a pinned center — re-reading the alignment then would see 'start' from :has(> br) and drop the alignment, so centered poetry went left.
…gnment lookup - querySelectorAll of BR_SELECTOR + ':has(> br)' only attached :has(> br) to the last selector (figcaption), so every p/div/... matched and got inline text-align:start, overriding the body justify fallback. Wrap the list in :is() so only genuinely br-containing blocks are touched. - Keep the ancestor-alignment lookup in inheritAlign: by the time apply() runs, the justify stylesheet is already injected and the :has(> br) start rule pollutes the element's own computed alignment, so the nearest ancestor's center/right must be read instead. - Honor align= attributes directly (sandbox may not expose UA styling). - Drop the !important on body justify (not needed once the scan is scoped). - Remove visible debug overlay and console diagnostics. - Update FakeDoc/FakeContainer in tests for the :is() selector and getAttribute; default-align br blocks are now pinned to start.
…as() Review asked for a fallback: both features are missing on old webviews (WebKitGTK < 2.36, iOS < 15.4, vendor-frozen Android WebViews), and the degradation is dangerous by default — engines that don't know @layer discard the whole layer block (justify silently disappears), and :has() drops the whole rule plus makes querySelectorAll throw SyntaxError. Add capability detection (CSSLayerBlockRule presence; CSS.supports selector queries) and split behavior accordingly: - @layer missing: serve the rules unlayered, every selector wrapped in :where() (specificity 0) when available, so book rules with real specificity still win. Engines without :where get bare selectors as a last resort (documented tie-loss risk). - :has() missing: drop the br-container CSS rule and let the JS scan pin those blocks inline — the scan now queries the plain block list and filters by iterating children, an API set that works everywhere. querySelectorAll(:is(...):has(...)) is additionally wrapped in try/catch for engine quirks. Desktop FoliateViewer mirrors the mobile helper (capability memoized per app run; all foliate docs share one engine). Rebuilt reader.html. Verified: vitest 24/24 (new tests cover the three degradation tiers and the throwing-query fallback), desktop tsc clean, build:reader OK.
Borrowed from readest (#5582): books like Standard Ebooks set text-wrap: pretty on body, and engines that apply pretty to justified text (Safari 26+, recent Chromium) overshoot inter-word spacing — the gaps balloon and word-spacing stops working. When justify is on the reader owns line breaking, so reset only the text-wrap-style longhand (an authored nowrap mode survives) on the containers justify reaches: html, body, p, li, blockquote, dd. !important so the reset also wins over unlayered author styles from inside our @layer. Mirrored in the mobile helper's three capability variants (the reset is a plain property unknown to old engines, so it degrades to a harmless no-op there).
…ning Review follow-up: pinning overwrote whatever inline text-align the book itself had set, and unpinning removed the property outright — so an author's own inline alignment could be lost after toggling justify. Record the element's previous inline text-align in data-readany-justify-original on first pin (guarded by the pin attribute so repeated apply cannot mistake the pinned value for the original), and on unpin restore it verbatim — or remove our property when the element had none.
… desktop/mobile) Review follow-up: capability detection, CSS generation and the alignment scan were duplicated between the desktop viewer and the mobile reader, and had already started to drift (the save/restore fix had to be applied twice). Move the whole engine to packages/core/src/reader/justified-text.ts: capability detection, three-tier CSS generation, the guarded br scan, and pin/unpin with the original inline text-align save/restore. The desktop viewer imports it directly; build-reader.js now bundles the same core module (unminified, so the logic stays auditable) into reader.html instead of injecting the static assets/reader/ justified-text.js file — that file is gone. The behavior tests moved to core and run against the shared module directly. No behavior change on fully capable engines; the three degradation tiers are covered by the moved test suite (core vitest 598 green, contract tests green).
1308411 to
742fc18
Compare
问题
当前的"正文两端对齐"实现(
justified-text.js,来自 #686)会遍历每个文档中的每一个<p>,逐个调用
getComputedStyle,给符合条件的段落打标记,再注入[data-marker] { justify !important }样式。在大书上,每次章节加载 / 设置变更都触发O(段落数) 次
getComputedStyle—— 在低端设备上成本明显。而且它只覆盖移动端:桌面阅读器只能继承 foliate 硬编码的
justify: true,无法响应阅读器级别的justifyBodyText设置。修复
用一套轻量、跨平台的策略替换 DOM 扫描:
@layer——body { text-align: justify }提供两端对齐默认值,但它位于
@layer readany-justify中。因为书籍样式是"未分层"的,任何作者设置的对齐都会胜出我们的 layer;
:where()又把特异性降到 0。这样在机制上就不可能覆盖书籍。<br>的块级元素 通过:where(*:has(> br))设置text-align: start,避免短诗 / 歌词行被拉伸(用
>直接子,避免误伤含 br 后代的整段容器)。text-align: start排除。<br>的块级元素(远少于全部段落)的计算后对齐,并把作者已对齐的(center/right/end,无论用 class、id、内联样式、
align属性还是对齐的祖先实现)内联固定,让书籍对齐即使被我们的
start规则匹配也得以保留。:has(> br)会直接覆盖元素自身的computed
text-align为start,读取时沿祖先链找最近的非start/inherit对齐,才能正确保留
div.center/.poem-wrap等继承居中。align属性直读:align="center"等 HTML 属性在阅读器沙箱中可能不产生computed
text-align,直接读属性兜底。选择器作用域修复(关键 bug):
querySelectorAll(选择器列表 + ":has(> br)")中,:has(> br)只会挂到选择器列表的最后一项(figcaption),导致前面的p, div, blockquote...都变成裸选择器、匹配文档中每一个对应元素,全部被设成内联
start,彻底盖掉 body justify。修复为
:is(选择器列表):has(> br),has条件应用到整组 —— 只碰真正含直接子<br>的块(实测:6 章共 90 个候选元素,修复后只处理 15 个含 br 的,其余 75 个普通段落完全不触碰)。
同一套 CSS 与逻辑现在同时用于移动端 WebView 阅读器(
reader.template.html+justified-text.js)与桌面 foliate 阅读器(FoliateViewer.tsx)。justifyBodyText设置保留(默认开启);关闭时不再注入 justify layer。验证
:is()选择器、getAttribute、align属性、startpin 断言)。app)与 core 的tsc --noEmit干净。getComputedStyle)中端到端验证:div.center/div.right、语义类名 div(div.poem-wrap)、.poem类、内联
text-align、align属性、<br>诗句(裸写及在对齐容器内)、嵌套居中、pre/figcaption/table、书籍body { text-align }覆盖 —— 全部尊重作者对齐。is()选择器作用域:修复后只处理真正含直接子<br>的块(6 章 90 → 15 个),普通段落(
text-align未设)保持干净、正确继承 body justify;中文长段落与西文长段落均实测 justify。syncJustifyForDoc幂等(重复 apply 不丢失居中)。
foliate 分页引擎按单一
#vertical处理,跨方向销毁重建)。<html class="vrtl">不设 CSS)、body writingMode、firstChildwritingMode 三种情况各自实证,缺任一都会漏判。
align属性、书 bodyleft覆盖、关闭开关恢复 —— 均符合预期。
已知局限
会继承 body justify,本方案与 原版 feat(reader): add optional justified EPUB body text #686 都无法排除 —— 原版
isVerticalDoc只查htmlclass /bodywriting-mode /body第一个子元素三处,且apply是文档级全有或全无;本方案至少让竖排 section 内含 br 的诗句被
:has(> br)强制start。这是 foliate 分页引擎本身不支持混合方向(
getDirection文档级检测、单一#vertical)的边缘场景,正常竖排书是整文档竖排,不受影响。