尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Readest 分页渲染下的 CSS 防御式改写:transformStylesheet 如何中和 background-attachment: fixed 与负水平外边距

Readest 分页渲染下的 CSS 防御式改写:transformStylesheet 如何中和 background-attachment: fixed 与负水平外边距 Readest 分页渲染下的 CSS 防御式改写transformStylesheet 如何中和 background-attachment: fixed 与负水平外边距【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readestReadest 的 EPUB 阅读器把每一章放进一个多栏分页 iframe中渲染书籍作者书写的 CSS 一旦依赖页面即视口的假设就会在这一环境中画错、画花甚至画到相邻页面上。本文以 Issue #5711PR #5729已合入为线索深入解析 transformStylesheet 中针对两类毒样式——background-attachment: fixed文字花屏与负外边距 背景跨页渗色——的改写策略、实现细节与边界条件。读完你既能复现并验证这两类问题也能理解 Readest 在分页场景下做 CSS 归一化时少即是多的设计取舍。问题背景分页 iframe 并非书稿作者想象中的视口Readest 的正文渲染基于 foliate 的分页器paginator每个章节的文档被放入一个被 transform 移动的巨大多栏条带multi-column strip中CSS 多栏布局负责把内容切成一页一页。这与浏览器普通页面有本质区别视口是整条多栏带而非一页任何锚定到视口的渲染行为如background-attachment: fixed、vw/vh单位、orientation媒体查询都会按整条带的尺寸求值与作者预期的当前页完全脱节列与列之间没有裁剪内容一旦溢出所在列的盒模型就会直接画到相邻页面上内容随 transform 移动翻页时整条带被平移任何以视口为参照的绘制都会被拖走。第 #5711 号问题报告的正是两种由多看Duokan出品的书稿里常见、且恰好踩中上述特性的 CSS 写法。而承载修复的地方只有一个阅读器在注入样式前统一调用的transformStylesheet。transformStylesheet一处中枢多类改写transformStylesheet 的函数签名如下export const transformStylesheet ( css: string, vw: number, vh: number, vertical: boolean, isFixedLayout false, ) { ... };四个入参分别对应待改写的样式文本、阅读器视口宽高用于把vw/vh、超宽像素值等换算成可用的分页单位、是否为竖排书写模式、是否为固定版式FXL。它的调用链清晰可见阅读器装配点FoliateViewer.tsx 中的getDocTransformHandler在收到detail.type text/css时调用transformStylesheet(data, width, height, viewSettings.vertical, bookData?.isFixedLayout)对 XHTML/HTML 内容则构造TransformContext走 transformer 管线style 变换器styleTransformer 用正则抽取每个style ....../style块把transformStylesheet的结果回填它还保留原标签的type、media等属性避免mediaprint这类限定被丢弃后打印样式污染屏幕渲染readest/readest#6233变换编排transformContent 按名称依次执行 availableTransformers 中注册的 11 个变换器style位于其中。一个重要的全局开关是isFixedLayout固定版式页面是按作者自己的视口排版的缩放字号、改写vw/vh、重绘颜色都会破坏原版因此函数开头就直接return css原样放行对应 fxl-authored-colors-5649FXL 绕过全部变换。所有下列 #5711 修复也都因此天然不作用于固定版式。修复一把 background-attachment: fixed 改写为 scroll故障机理在分页 iframe 中background-attachment: fixed会把背景图像锚定到被 transform 的整个多栏条带视口上背景图相对视口固定而承载它的元素随翻页条带移动于是图像远离了它本该装饰的元素Blink 在绘制时会把覆盖在 fixed 背景上的文字抹花garbled band形成一段花屏带。从规范角度讲CSS 变换transform子树内部的 fixed 附着本就被规定按scroll行为求值所以把fixed改写成scroll不只是绕过 bug更是回到规范要求的语义。这也是 transformStylesheet 采取的策略统一重写fixed→scroll且同时覆盖长写属性background-attachment与简写background多图层列表multi-layer也逐层处理。实现细节先掩码再改写直接对fixed做全局替换是有风险的url(fixed.png)里的 fixed 只是文件名var(--fixed, fixed)里的fixed只是自定义属性的值。因此实现分三步url() 全表掩码先用READEST_URL_N_PLACEHOLDER占位符替换所有url(...)片段。这一步必须在声明切分之前做因为未加引号的 data URI 里可能含分号会提前截断声明匹配带引号的 url 里可能含右括号甚至 fixed 字样函数 token 逐值掩码对匹配到的声明值再把var()、渐变等name(...)函数掩码为READEST_FN_N_PLACEHOLDER然后只对剩余的裸\bfixed\b做替换还原占位符依次还原函数 token 与 url token保证url(fixed.png)、var(--fixed)原样存活。css css.replace(/url\(\s*(?:[^]*|[^]*|[^)]*)\s*\)/gi, (url) { urlTokens.push(url); return READEST_URL_${urlTokens.length - 1}_PLACEHOLDER; }); css css.replace( /((?:^|[{;\s])background(?:-attachment)?\s*:)([^;{}]*)/gi, (match, prop, value) { if (!/\bfixed\b/i.test(value)) return match; // 掩码函数 token 后替换 fixed → scroll再还原 ... }, ); css css.replace(/READEST_URL_(\d)_PLACEHOLDER/g, (_, i) urlTokens[i]!);改写是纯文本级、幂等友好的!important被保留background-attachment: fixed !important→scroll !important而position: fixed因为正则要求background前缀而完全不受影响。测试覆盖style.test.ts 的background-attachment fixed (issue 5711)测试组完整锁定了这些行为长写与简写、多图层列表fixed, scroll, fixed→scroll, scroll, scroll都被改写!important保留url(images/fixed.png)与var(--fixed, fixed)不被触碰含分号的 data URIurl(data:image/png;base64,AAAA)不截断匹配带右括号与 fixed 的带引号 urlurl(weird) fixed.png)不被破坏内联样式无{}的声明串同样生效position: fixed原样保留。修复二中和负水平外边距 背景的跨页渗色故障机理多看书稿常用一种通栏出血full-bleed技巧用与多看固定 2em 页边距配套的负外边距把色带撑满整页例如h1.title { background-color: #0069B7; color: white; margin: -2em -2em 1.5em -2em; /* 左右 -2em正好吃掉 2em 页边距 */ padding-top: 3em; }在多看自己的渲染器里页边距是固定的 2em负-2em外边距恰好把背景溢出到页面边缘形成通栏色带。但 Readest 的多栏分页中列与列之间没有裁剪这个-2em的外溢量会直接画到相邻页面上出现上一页的色带残留到下一页的跨页渗色。修复策略以报告者的临时方案为政策实现先尝试过一个更聪明的方案——用max()钳制并通过--page-margin-*变量保留通栏效果但被维护者以过度复杂、宁可少一点兼容性也要更简单为由否决要求不得复活该方案这一点在源码注释与记忆文档中均有明确记录。最终合入的策略极其朴素对命中条件的规则追加margin-left: 0 !important;与margin-right: 0 !important;让色带停在列边缘——不再通栏NO full-bleed但这恰好就是问题报告者自己在自定义 CSS 里使用的规避手段属于把用户验证过的 workaround 固化为产品策略。命中条件Gate改写不是无脑的实现对每条规则做了四项检查全部满足才追加归零声明门控条件意图反例规则确实绘制了背景any-painting background gate悬垂缩进hanging indent等纯排版负外边距不参与margin-left: -1em且无背景 → 不动水平方向的外边距解析后确实为负只归零负的那一侧margin: 1em -2em 3em 4em→ 仅margin-right归零非竖排书写模式!vertical竖排书的分栏几何不同vertical: true→ 跳过简写不含calc()/var()函数式值无法可靠按空白拆分边margin: calc(0px - 2em) 0→ 跳过背景判定上none、transparent以及 alpha 为 0 的rgba()/hsla()都算不绘制声明顺序上按后声明覆盖先声明逐边解析最终值margin: -2em; margin-left: 1em;时左侧最终为1em只归零右侧。由于归零声明以!important追加在块末尾、级联靠后即使作者写了!important的负外边距也能被压过测试wins over an authored !important margin专门验证了这一点实现注释也说明忽略块内!important优先级的最坏结果只是留下作者原本的负外边距绝不会破坏有效布局。测试覆盖style.test.ts 的negative horizontal margins with background (issue 5711)测试组逐条锁定了四值简写、两值简写、长写属性、按侧归零、后声明覆盖、background: none之后再声明background-color仍能检测到绘制、alpha 零色不算绘制、无背景/透明背景不动、仅垂直方向为负不动、margin: -1em auto的自动居中保留、calc()/var()简写跳过、竖排跳过。值得区分的是transformStylesheet中另有一套独立的duokan-bleed属性处理style.ts它针对显式书写duokan-bleed: left right的书稿用margin-left: calc(-1 * var(--page-margin-left))等变量保留通栏并补position/overflow/display/width声明。这与 #5711 的负外边距检测 归零是两条并行的路径前者是作者主动声明出血意图后者是隐式出血技巧的防御式中和。排障实录三个容易踩的坑记忆文档还记录了复现与排查过程中三个非显而易见的陷阱对任何调试谁画坏了这一页的人都有直接价值Issue 附件里的部分 EPUB会说谎复现书 2 的part0010.xhtml引用了../Styles/style0001.css但 zip 里只打包了style0010.css。缺失引用的结果不是报错而是整个样式表静默加载成 0 条规则bug 因此无法复现——必须把 CSS 复制到被引用的名字并重新打包才能暴露真正的问题。排查分页排版问题时务必先核对 zip 内实际打包的文件名与 XHTML 中的引用是否一一对应。body idb2上的装饰性角标foliate 的paginator.jsgetBackground()在加载时读取 body 的计算后背景随后设置doc.body.style.background none再把它重绘到与页面同尺寸的背景分段 div 上相关机制可参考 paginator-swipe-bg-flash 中关于#background层与computeBackgroundSegments的记录。因此一个no-repeat right bottom的 body 装饰图如 Sherlock 烟斗图会被镜像到每个分页容器的右下角盖住 Readest 的页脚/页码。这是既存的外观问题不属于 #5711 的修复范围将来若要修复应落在 paginator 的背景镜像逻辑里而不是transformStylesheet。追查这像素是谁画的阅读器里有多个绘制层。排查时应枚举所有 iframe 以及顶层文档的 shadow root中computedStyle.backgroundImage ! none的元素——绘制者往往是一个 foliate 的 shadow-DOM div它在elementFromPoint下不可见属于内容下方的画布级绘制漏掉 shadow root 就会误判元凶。联动与边界同一中枢的相邻问题transformStylesheet是全仓库 CSS 归一化的唯一中枢多个版式问题都在这里收敛table-cell-overflow-wrap-anywhere-5681表格overflow-wrap: anywhere会把每列最小宽度压到单字符导致窄列碎成一摞字母transformStylesheet配套注入td, th { overflow-wrap: break-word; }见 getColorStyles与 #5711 共用同一改写枢纽fxl-authored-colors-5649固定版式绕过所有变换因此 #5711 的两项修复对 FXL 天然无影响。截至记忆文档的索引快照2026-08-24#5711 已通过 PR #5729 合入含评审修正function-token 掩码、切分前的 url() 掩码、按侧解析外边距、any-painting 背景门控而body 角标盖住页脚的 paginator 背景镜像问题仍处于 OPEN 状态。结语防御式 CSS 改写的取舍回看 #5711 的整个修复过程最有价值的并非两条正则而是两次克制面对fixed时没有否定规范、而是回到规范要求的scroll语义面对通栏出血时放弃了复杂的max()钳制方案采纳了报告者验证过的归零策略——在分页渲染这个充满作者假设的场景里简单、可预期、不破坏有效布局的改写往往比追求完整兼容的实现更值得合入。这正是 transformStylesheet 及其 测试套件 留给后续维护者的方法论遗产。【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表