Skip to content

fix(renderer): export live rendered content through shared markdown conversion - #1337

Open
lanyue-llk wants to merge 10 commits into
codex/content-tables-fragmentsfrom
codex/content-rendered-state
Open

lanyue-llk wants to merge 10 commits into
codex/content-tables-fragmentsfrom
codex/content-rendered-state

Conversation

@lanyue-llk

@lanyue-llk lanyue-llk commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

问题

Markdown 导出需要读取当前 DOM、表单值和资源地址。序列化属性可能仍是初始状态;按透明度、颜色或框架属性猜测正文,也会误删有效内容。

可见性恢复还需要区分两种情况:页面继承的加载遮罩,以及元素自身明确设置的隐藏。两者混用时,内容面板的恢复路径会误删旁边的正文。

一图看懂

Markdown 导出的内容边界

方案

  • 在一次样式读取中取得 display、visibility、baseline-shift 和自身可见性来源,按 DOM 先序计算继承状态,并检查父节点先于子节点。
  • 只有 display:none 祖先参与披露路径的兄弟过滤。visibility 单独处理,允许后代恢复可见;重复 ID 只选择第一个目标。
  • 可见 pre 保留折叠源文本,普通行内 code 仍遵守隐藏规则。脚本、样式、模板和代码操作控件不进入源文本。
  • 保留明确的内容面板与原生披露内容,不自动展开隐藏导航、菜单或对话框。不根据图片尺寸、透明度、颜色、框架属性或 Read more 文案删除内容。
  • 实时表单值、勾选状态和解析后的资源地址通过只读视图交给共享转换器。URL 属性清单由转换器统一维护;按元素原始 display、父容器排布及是否脱离普通流确定补充文字边界。

CSS 会把 flex/grid 子项、浮动和绝对定位元素的 display 调整为块级;这不表示作者设置了段落。导出从同一份 Stylo 观测中读取原始 display:横向控件保持一行,纵向布局保留分隔,定位文字不拆开原链接。普通逐字 span 不自动添空格,作者写出的空白仍保留。公开 CSSOM 值与原 DOM 均不改变。

验证

验证范围 结果
共享 Markdown 转换库 398 项通过
页面 Markdown 导出 33 项通过;覆盖继承与显式隐藏、披露内容与正文兄弟、重复 ID、实时表单、源码区、媒体和 CSS 边界
srcset 图片选择 9/9 通过,与共享解析器联动
CLI 布局回归 8 路并发 10/10 通过,包含 CI 中超时的 7 项
真实 CLI fetch 场景 14/14 通过,包括透明文章、图片上的同色文字、框架属性、隐藏导航、实时 textarea 和大页面
CSS 分段与控件反例 9/9 通过,修复前均失败;涵盖 flex、定位链接、逐字 span、空与无效控件值
三个原始失败页面 3/3 成功并命中参考正文;狼人杀页核对全部配置段
WebMainBench 8798f93a… 91/91 组折叠明细保留,未点击页面或改写 DOM
表格精确快照 三类表格在布局开启、关闭时共 6/6 通过
格式与全工作区 Clippy 通过;全仓库测试及集成检查交给 CI

五个 CLI 测试覆盖六份布局开关快照及真实 HTTP 地址解析。当前 HEAD 的 CLI 复现绑定见下文。

真实页面的精确输出覆盖紧凑列表、嵌套列表、下拉分组、行内控件、隐藏中间单元格和关闭的 dialog/popover。隐藏单元格保留列位置,只去掉不可见文字;整张不可见的表格不生成空骨架,后代恢复可见时仍保留其内容,collapse 行从输出中移除;原生结构不因 UA 默认 CSS 变成额外段落。样式读取范围为正文子树与继承所需的根元素。

隐藏祖先下的独立 tabpanel 与 hidden=until-found 内容,先登记为内容根,再计算保留路径;路径中的隐藏兄弟与导航模板继续排除。计算样式的块边界覆盖多关键词 list-item,不依赖渲染布局。

未更改的 range/color 读取作者属性,空属性不变成 50 或黑色默认值;更改后的控件读取当前实时值。普通表单值属于授权 DOM 导出的内容;密码、隐藏和文件输入仍排除。HTML with_frames 沿用现有契约,保留 iframe 地址与嵌入元数据,不把 srcdoc 合并为主文档正文。

已发布测量

历史完整运行绑定源码 721bd2e329f96c06573db6e6b257a149cf17a6fe 和二进制 fac987f5…。同一 545 页的 PR785 与该运行全文均值分别为 41.7243% 和 40.7252%;折叠明细丢失是其中一个已定位问题,HTML 结构增加也会改变字符串相似度。

本 PR 的定向验证没有替换历史成绩。历史 WebMainBench 报告。

复现

以下命令验证完整 stack 的浏览器导出路径。

最小用例

横向表单保持一行,逐字样式包装不把 Hi 拆开;隐藏导航不进入输出。

fixture_html=$(cat <<'HTML'
<form style="display:flex"><input value="rust"><button>Search</button></form><h1><span style="display:inline-block">H</span><span style="display:inline-block">i</span></h1><nav hidden>Secret menu</nav>
HTML
)
"$MOLI_BINARY" fetch "data:text/html;base64,$(printf '%s' "$fixture_html" | base64 | tr -d '\n')" --dump markdown

实际输出:

rust Search

# Hi

真实 WebMainBench 页面

三个原始失败页 d80cf991…、72dd5ff7…、ccb6033c… 均命中目标正文;8798f93a… 保留全部 91 组明细。

七个相关页面通过 browser-eval 的现有 runner 共用一次定向运行,7/7 正常导出,并核对上述内容;不是 545 页质量重测。查看冻结输入、实际输出与核对结果。

  • Moli:19bfc785;二进制 SHA-256:1c17bba8cb6b0a532144697411d13ec6a3872d1abad96d5872da0ded93d7b8b4。
  • browser-eval:345ea093。
  • 原始数据集 SHA-256:0efaa4b49a45e320a27fe6e5a0b6aad5b57259fc3321ac3448519cacc74c537e,runner 会校验。保留原网址,代理返回冻结 HTML,外部资源禁止访问,无点击或 DOM 改写。
环境准备与可复制命令
# Moli 仓库:构建完整 stack,供下面的 CLI 复现使用。
git fetch origin refs/pull/1337/head
git switch --detach 19bfc785f2e0114d7d88e17457de7c108a54069d
cargo build -p moli --bin moli --locked
export MOLI_BINARY="$PWD/target/debug/moli"

# 在单独目录准备 browser-eval。
git clone https://github.com/lexmount/browser-eval.git
cd browser-eval
git checkout 345ea09370a633e871670fde2de4ec25b53a4d05
uv sync --frozen
mkdir -p "$HOME/.browser-eval/public-benchmarks/webmainbench-545"
curl --fail --location \
  https://huggingface.co/datasets/opendatalab/WebMainBench/resolve/main/WebMainBench_545.jsonl \
  --output "$HOME/.browser-eval/public-benchmarks/webmainbench-545/WebMainBench_545.jsonl"

# 在 browser-eval 仓库运行;使用 macOS 的 loopback-only 沙箱。
# MOLI_BINARY 为上面构建的 Moli 的绝对路径。
export MOLI_BINARY
repro_root=$(mktemp -d)
cat > "$repro_root/cases.txt" <<'CASES'
75b0c1e9-d112-46d0-adc8-d1d09aa48731
bfaf0844-440c-466b-b55a-f82c88a54a4e
34b822a2-f7d6-4b69-b6af-133a1ca21326
8798f93a-240c-46d7-a8d1-a5fb011279fc
d80cf991-ba87-4068-80b4-3e9bd49ac8fd
72dd5ff7-8cb5-4660-8941-c1846a43a15d
ccb6033c-0a12-4f9c-8c68-794f26129841
CASES
uv run --frozen python scripts/run_webmainbench_benchmark.py \
  --provider moli --case-ids-file "$repro_root/cases.txt" \
  --preserve-original-origin --expected-version 1.1.19 \
  --timeout 90 --max-failure-retries 0 \
  --output "$repro_root/results"
printf '结果目录:%s\n' "$repro_root/results"

results.jsonl 记录成功状态、错误和输入/输出哈希,execution.json 记录二进制身份,artifacts/moli/ 保存实际 Markdown。定向运行只用于定位和回归,不进入正式榜单;报告中的历史成绩仍绑定原测量版本。

依赖

直接基于 #1336。Stylo 固定 e2baa14adb66df48a56416a93511690d27bfd600,在 main 已使用的 973988b9 上仅增加自身可见性来源标记;对应 Stylo #2。已移除无消费者的动画接口和与导出无关的 font color 修改,保留 main 的其它依赖更新。

#1334 → #1335 → #1336 → #1337 → #1380

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Sequential Navigation Soak A/B

✅ HEAD completed the 200-navigation resilience and memory observation.

One browser process, one target, and one CDP session navigate CSDN → SegmentFault → Huaban → example.com repeatedly. This run issued 200 Page.navigate commands per binary.

Common ancestor 4a817658a7 → HEAD e652af415d; benchmark order: head-first; workflow: success.

Session resilience

Metric Base HEAD
Attempted / planned 200 / 200 200 / 200
Direct observable pass 193 / 200 197 / 200
Failures / recovered 7 / 7 3 / 3
Recovery failures 0 0
Lifecycle order violations 0 0
Network order violations 0 0
Wall time 533.5 s 507.0 s

A public-page failure is reported separately from an unrecoverable session. The soak requires all navigation attempts, zero failed recovery, zero lifecycle/network ordering violations, and complete resource evidence.

Process-tree memory

Metric Base HEAD Δ
Boundary samples 200 200 0
Boundary RSS samples 200 200 0
Boundary PSS samples 200 200 0
Periodic peak RSS 236.3 MiB 247.9 MiB +11.6 MiB
Periodic peak PSS 232.4 MiB 244.3 MiB +11.9 MiB
RSS · first 10 avg 154.8 MiB 148.3 MiB -6.5 MiB
RSS · last 10 avg 174.3 MiB 172.7 MiB -1.6 MiB
RSS · last − first window +19.5 MiB +24.4 MiB +4.9 MiB
RSS · warm slope / 100 nav +4.1 MiB +4.9 MiB +0.8 MiB
PSS · first 10 avg 150.7 MiB 144.6 MiB -6.1 MiB
PSS · last 10 avg 170.4 MiB 168.5 MiB -1.9 MiB
PSS · last − first window +19.7 MiB +24.0 MiB +4.3 MiB
PSS · warm slope / 100 nav +3.7 MiB +4.7 MiB +1.0 MiB
Peak file descriptors 60 62 2
Peak threads 29 31 2

HEAD memory by 50-navigation quarter

HEAD range Avg RSS Final RSS Peak RSS Avg PSS Final PSS Peak FD
1–50 161.8 MiB 144.3 MiB 205.8 MiB 158.1 MiB 140.7 MiB 52
51–100 169.0 MiB 160.5 MiB 214.4 MiB 165.1 MiB 156.3 MiB 53
101–150 168.7 MiB 124.4 MiB 203.2 MiB 164.9 MiB 120.8 MiB 53
151–200 173.4 MiB 163.2 MiB 211.4 MiB 169.5 MiB 159.1 MiB 56

Raw reports and per-navigation boundary samples: workflow run and full sequential-navigation-soak-results artifact.

Public-site timing and memory are observational. Use the A/B deltas and trend shape as evidence, not as a deterministic performance threshold.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Spider Bench A/B

✅ All benchmark browser service runs completed; results are informational.

Public HEAD: 184 / 240 rows; 38 / 48 sites produced rows; 10 unexpected empty sites.

✅ Deterministic fixture contract is clean.

Common ancestor 4a817658a707 → HEAD e652af415dc6; benchmark order: head-first; workflow: success.

Public 48-site run · informational

Metric Base HEAD Δ
Run status success success —
Extracted rows / contract 184 / 240 184 / 240 0 (0.00%)
Contract fill 76.67% 76.67% 0.00 pp (0.00%)
Sites with rows 38 / 48 38 / 48 0 (0.00%)
Sites meeting row contract 36 / 48 36 / 48 0 (0.00%)
Unexpected empty sites 10 10 0 (0.00%)
Partial-row sites 2 2 0 (0.00%)
Site outcomes extracted: 37; HTTP error + rows: 1; empty: 7; HTTP error + empty: 1; snapshot error + empty: 2 extracted: 36; HTTP error + rows: 2; empty: 7; HTTP error + empty: 1; snapshot error + empty: 2 —
Observed duration 44.14 s 44.09 s -0.06 s (-0.13%)
Estimated CPU 35.84 s 39.24 s +3.40 s (+9.49%)
Peak PSS 262.70 MiB 275.99 MiB +13.29 MiB (+5.06%)
Peak RSS 266.07 MiB 279.51 MiB +13.43 MiB (+5.05%)
Peak threads 27 27 0 (0.00%)
Mismatched sites 0 0 0
Timeout with items 0 0 0
Category rows Base HEAD Δ
news 35 / 40 35 / 40 0
stocks 20 / 40 20 / 40 0
tech 35 / 40 35 / 40 0
sports 27 / 40 27 / 40 0
games 37 / 40 37 / 40 0
life 30 / 40 30 / 40 0

Public-site content, timing, and memory are noisy. This single A/B run reports evidence only; site outcome counts explain missing rows without treating them as a deterministic regression.

CPU and memory timelines

These are bounded 40-point views of the same complete process-tree samples used by the HTML report. CPU uses 100% per occupied logical core; memory lines are RSS first and PSS second when complete PSS samples are available.

Base

xychart-beta
    title "Base CPU"
    x-axis "Elapsed seconds" [1.034, 2.534, 3.535, 4.537, 5.038, 6.037, 7.538, 8.54, 10.043, 11.045, 12.548, 13.55, 15.052, 16.054, 17.556, 18.555, 20.055, 21.056, 22.559, 23.56, 24.06, 24.561, 26.061, 27.061, 28.563, 29.564, 31.066, 32.068, 33.569, 34.57, 36.07, 37.071, 38.573, 39.574, 41.074, 41.575, 42.075, 43.578]
    y-axis "CPU percent" 0 --> 200
    line [12, 112, 105.92, 129.66, 139.85, 134.08, 136.01, 61.85, 139.83, 103.76, 143.7, 107.77, 2, 0, 4, 4, 90, 25.98, 83.83, 105.97, 103.93, 107.84, 123.79, 131.99, 129.93, 101.92, 101.88, 101.92, 81.8, 122.02, 4, 51.97, 0, 13.98, 101.9, 161.79, 11.99, 121.84]
Loading
xychart-beta
    title "Base memory: RSS then PSS"
    x-axis "Elapsed seconds" [0.034, 1.034, 2.534, 3.535, 4.537, 5.038, 6.037, 7.538, 8.54, 10.043, 11.045, 12.548, 13.55, 15.052, 16.054, 17.556, 18.555, 20.055, 21.056, 22.559, 23.56, 24.06, 24.561, 26.061, 27.061, 28.563, 29.564, 31.066, 32.068, 33.569, 34.57, 36.07, 37.071, 38.573, 39.574, 41.074, 41.575, 42.075, 43.578, 44.145]
    y-axis "Memory MiB" 0 --> 500
    line [13.11, 71.38, 105.19, 120.44, 150.8, 163.38, 176.65, 146.11, 124.7, 144.2, 141.66, 135.45, 119.63, 122.78, 121.74, 121.71, 121.73, 133.86, 152.91, 149.96, 207.65, 266.07, 190.67, 189.06, 149.54, 150.13, 172.25, 137.32, 141.06, 145.92, 186.96, 134.95, 143.73, 142.15, 135.6, 153.61, 157.23, 127.3, 172.48, 0]
    line [9.73, 67.7, 101.51, 116.99, 147.16, 159.62, 172.91, 140.5, 121.14, 140.51, 137.89, 132.03, 115.79, 119, 117.9, 117.92, 117.94, 130.11, 149.19, 146.62, 203.97, 262.7, 185.77, 185.37, 145.82, 146.39, 168.47, 133.54, 136.97, 142.17, 183.18, 131.17, 140.41, 138.37, 132.52, 149.94, 153.45, 123.52, 168.77, 0]
Loading

HEAD

xychart-beta
    title "HEAD CPU"
    x-axis "Elapsed seconds" [1.535, 2.536, 4.039, 5.039, 6.539, 7.541, 9.043, 10.043, 11.545, 12.547, 14.048, 15.55, 16.552, 18.054, 18.555, 19.055, 20.556, 21.556, 23.058, 23.558, 24.06, 25.561, 26.563, 28.065, 29.065, 30.568, 32.071, 33.071, 34.573, 35.575, 37.076, 38.077, 39.577, 40.577, 42.08, 43.081, 43.582, 44.082]
    y-axis "CPU percent" 0 --> 200
    line [87.84, 103.85, 95.76, 118.02, 133.94, 129.73, 95.93, 134.03, 133.73, 29.94, 23.99, 59.92, 47.9, 46.01, 0, 27.96, 31.99, 114.05, 97.77, 105.88, 113.71, 125.81, 135.77, 138, 97.94, 75.8, 89.75, 7.99, 109.97, 113.91, 89.95, 7.99, 64.01, 128, 47.89, 103.85, 171.81, 127.79]
Loading
xychart-beta
    title "HEAD memory: RSS"
    x-axis "Elapsed seconds" [0.033, 1.535, 2.536, 4.039, 5.039, 6.539, 7.541, 9.043, 10.043, 11.545, 12.547, 14.048, 15.55, 16.552, 18.054, 18.555, 19.055, 20.556, 21.556, 23.058, 23.558, 24.06, 25.561, 26.563, 28.065, 29.065, 30.568, 32.071, 33.071, 34.573, 35.575, 37.076, 38.077, 39.577, 40.577, 42.08, 43.081, 43.582, 44.082, 44.088]
    y-axis "Memory MiB" 0 --> 500
    line [13.44, 92.57, 105.93, 135.24, 150.84, 136.49, 134.19, 133.92, 151.02, 138.37, 114.56, 121.56, 137.97, 136.25, 121.09, 121.12, 126.83, 155.21, 138.84, 224.25, 279.51, 175.53, 191.07, 214.08, 150.41, 167.82, 134.98, 145.11, 129.81, 171.92, 242.86, 239.81, 148.04, 136.16, 163.62, 135.1, 149.82, 171.93, 0, 0]
Loading

Deterministic fixture · required contract

The fixture exercises 15 routes: 8 are expected to emit rows and 7 intentionally exercise empty, loading, or timeout behavior.

Metric Base HEAD Δ
Run status success success —
Extracted rows / contract 40 / 40 40 / 40 0 (0.00%)
Contract fill 100.00% 100.00% 0.00 pp (0.00%)
Sites with rows 8 / 15 8 / 15 0 (0.00%)
Sites meeting row contract 15 / 15 15 / 15 0 (0.00%)
Unexpected empty sites 0 0 0
Partial-row sites 0 0 0
Site outcomes extracted: 7; HTTP error + rows: 1; empty: 3; timeout + empty: 4 extracted: 7; HTTP error + rows: 1; empty: 3; timeout + empty: 4 —
Observed duration 19.84 s 19.85 s +0.01 s (+0.03%)
Estimated CPU 0.39 s 0.41 s +0.02 s (+5.10%)
Peak PSS 67.43 MiB 67.02 MiB -0.42 MiB (-0.62%)
Peak RSS 71.02 MiB 70.78 MiB -0.25 MiB (-0.35%)
Peak threads 18 18 0 (0.00%)
Mismatched sites 0 0 0
Timeout with items 0 0 0

Full HTML, JSON, CSV, logs, and page snapshots: workflow run and full spider-bench-results artifact.

The public-web A/B is informational. The exact HEAD fixture contract runs as a separate required CI check; timing and public-site content deltas remain non-blocking.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

CI Regression Report

Source CI run · source state at render: in_progress · artifacts: 6/6

Check Status Signal
Release regression ✅ HEAD/base failures 0/0; raw binary +0.023974%
Frontend differential ❌ 1,008/1,020 cases matched; 12 issues
Agent episodes ✅ 8/8 Moli episodes passed; 0 failures
Runtime/CDP contracts ✅ 26 contract cases; 0 failures
CDP smoke ✅ 52/52 groups passed; 621 scenarios
WebMainBench ✅ 545/545 completed; 0 unexpected failures
Release regression — ✅ HEAD/base failures 0/0; raw binary +0.023974%

Package and image size

Metric Base HEAD Delta Delta %
Raw binary 153.36 MiB 153.39 MiB +37.65 KiB +0.023974%
Stripped binary 106.62 MiB 106.66 MiB +36.00 KiB +0.032973%
gzip binary 52.83 MiB 52.85 MiB +17.84 KiB +0.032977%
Rootfs 160.25 MiB 160.29 MiB +40.00 KiB +0.024375%
gzip rootfs 61.89 MiB 61.91 MiB +19.78 KiB +0.031214%

Startup latency and PSS

Case Base p50 HEAD p50 Delta Base PSS HEAD PSS Delta
Serve ready 27.02 ms 27.26 ms +0.24 ms 3.77 MiB 4.21 MiB +451.50 KiB
CDP first page 49.51 ms 50.69 ms +1.18 ms 3.78 MiB 4.22 MiB +451.00 KiB
CDP warm page 11.97 ms 12.38 ms +0.41 ms 50.24 MiB 50.36 MiB +118.50 KiB
CLI about:blank 37.50 ms 39.28 ms +1.77 ms 3.63 MiB 2.72 MiB −935.00 KiB
CLI local JS 43.91 ms 47.12 ms +3.21 ms 3.83 MiB 4.43 MiB +611.00 KiB

Concurrency matrix

Concurrency Base p50 HEAD p50 Delta Base PSS HEAD PSS Delta HEAD failures Unstable B/H
1 42.50 ms 41.96 ms −0.53 ms 603.00 KiB 582.00 KiB −21.00 KiB 0 0/0
5 42.62 ms 41.52 ms −1.09 ms 4.21 MiB 1.57 MiB −2.64 MiB 0 0/0
10 42.92 ms 43.28 ms +0.36 ms 4.39 MiB 2.94 MiB −1.45 MiB 0 0/0
25 42.72 ms 43.10 ms +0.38 ms 3.60 MiB 1.83 MiB −1.78 MiB 0 0/0
100 43.67 ms 42.10 ms −1.58 ms 1.44 MiB 2.18 MiB +763.00 KiB 0 0/0
Frontend differential — ❌ 1,008/1,020 cases matched; 12 issues
Match DOM mismatch Diagnostic mismatch Moli error Reference error Infrastructure error Mismatched frames Duration
0 0 0 0 12 0 0 4m 40.20s

Recovered Chromium reference infrastructure failures

  • angular/gallery-marketing/architecture-case-study — 1 failed attempt(s) before recovery · unknown
  • angular/gallery-marketing/nonprofit-impact-report — 1 failed attempt(s) before recovery · CDP connection failed: timed out
  • angular/gallery-marketing/developer-cloud-platform — 1 failed attempt(s) before recovery · CDP connection failed: timed out
  • angular/gallery-marketing/security-enterprise-suite — 1 failed attempt(s) before recovery · CDP connection failed: timed out
  • angular/gallery-marketing/product-comparison-hub — 1 failed attempt(s) before recovery · CDP connection failed: timed out
  • angular/gallery-marketing/conference-event-launch — 1 failed attempt(s) before recovery · CDP connection failed: timed out
  • angular/gallery-marketing/museum-exhibition-story — 1 failed attempt(s) before recovery · CDP connection failed: timed out
  • angular/gallery-commerce/fashion-marketplace — 1 failed attempt(s) before recovery · CDP connection failed: timed out
  • angular/gallery-commerce/furniture-configurator — 1 failed attempt(s) before recovery · CDP connection failed: timed out
  • angular/gallery-commerce/grocery-delivery — 1 failed attempt(s) before recovery · CDP connection failed: timed out

First failing cases

  • angular/gallery-marketing/architecture-case-study — reference_error · Chromium error CDP connection failed: timed out
  • angular/gallery-marketing/nonprofit-impact-report — reference_error · Chromium error CDP connection failed: timed out
  • angular/gallery-marketing/developer-cloud-platform — reference_error · Chromium error CDP connection failed: timed out
  • angular/gallery-marketing/security-enterprise-suite — reference_error · Chromium error CDP connection failed: timed out
  • angular/gallery-marketing/product-comparison-hub — reference_error · Chromium error CDP connection failed: timed out
  • angular/gallery-marketing/conference-event-launch — reference_error · Chromium error CDP connection failed: timed out
  • angular/gallery-marketing/museum-exhibition-story — reference_error · Chromium error CDP connection failed: timed out
  • angular/gallery-commerce/fashion-marketplace — reference_error · Chromium error CDP connection failed: timed out
  • angular/gallery-commerce/furniture-configurator — reference_error · Chromium error CDP connection failed: timed out
  • angular/gallery-commerce/grocery-delivery — reference_error · Chromium error CDP connection failed: timed out
Agent episodes · Moli vs Chromium — ✅ 8/8 Moli episodes passed; 0 failures
Fast contract Episodes Passed Assertions Ready p50 Episode p50
Moli 8 8 47/47 28.41 ms 24.12 ms
Chromium 8 8 47/47 246.19 ms 84.69 ms
Operation p50 Chromium Moli Delta Delta %
navigate 41.83 ms 5.79 ms −36.04 ms −86.17%
observe 0.79 ms 0.96 ms +0.17 ms +20.86%
fill 3.64 ms 3.18 ms −0.46 ms −12.76%
click 4.70 ms 3.62 ms −1.08 ms −23.02%
Idle-resource episode Peak RSS Peak PSS Average CPU Peak CPU Peak processes Sampler
Moli 67.16 MiB 65.35 MiB 0.80% 14.01% 1 ✅
Chromium 1285.90 MiB 408.54 MiB 0.71% 12.00% 13 ✅
Runtime and CDP session contracts — ✅ 26 contract cases; 0 failures
Suite Cases Failures Median case p50 Median case p95 Median peak PSS p50
Synthetic fetch 13 0 41.30 ms 42.66 ms 614.00 KiB
Long-lived CDP session 13 0 18.01 ms 18.60 ms —
CDP smoke — ✅ 52/52 groups passed; 621 scenarios

Workers: 4 · cumulative group time: 201.04 s · failed groups: 0

Slowest group Scenarios Duration Status
scrollbar-visibility 22 29.61 s ✅
target-lifecycle 24 17.12 s ✅
dom-input 27 13.32 s ✅
chromium-cdp 56 11.86 s ✅
multi-page 45 11.78 s ✅
WebMainBench · 545 pages — ✅ 545/545 completed; 0 unexpected failures
Completed Successful Expected DNS failure Unexpected failures Panics Crashes Timeouts Empty output
545/545 544 1 0 0 0 0 0

Frozen HTML · Linux · --wait done · 45 seconds per page · no retries. Content quality is evaluated separately.

Diagnostics: webmainbench-results — every page's Markdown, stderr, and exit status. Source run and artifacts

All artifact fields are parsed by the trusted default-branch renderer; missing or invalid inputs remain visible as unavailable.

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of the updated version (3fb74bccb)

This PR makes the renderer's Markdown export read live state: computed display/visibility/baseline-shift, own-visibility provenance from Stylo, live form values and checkedness, and base-resolved URLs. It does this through a MarkdownDom adapter over the shared moli-html2md converter. This revision addresses every major point from the last round. Disclosure sibling filtering now uses only display:none ancestors. Styles are read once per dump. The font-color scope creep is gone. The Stylo pin now sits directly on main's 973988b9 with a 14-line diff, the head merges cleanly with origin/main, and #1592 is now stacked on this branch and extends the same adapter. Verdict: comment, close to approve. The remaining items are minor: the Stylo pin points at an unmerged fork PR, intermediate commits do not build, one disclosure path is inconsistent, and there are a few nits.

Previous findings

# Finding Status Evidence
1 [major] visibility-hidden ancestor turns disclosure path into sibling filter FIXED hidden_nodes is display == "none" only (visibility.rs:83-86). New test markdown_document_export_distinguishes_source_from_hidden_interface puts body{visibility:hidden} next to an aria-controls section and keeps both intro and conclusion; it would fail on the old code (old hidden_nodes put body on the path).
2 [nit] redundant id-based disclosure clause / last-wins ids / role tokens FIXED Clause removed (L149-151); targets.entry(id).or_insert is first-wins (L62-66); has_role splits tokens (L334-338). The duplicate-id test fails without first-wins.
3 [minor] ≤1.5px tracking-pixel heuristic FIXED Rule and width/height sampling removed; the test keeps a 1px image.
4 [minor] any visible code/pre bypasses hidden rules FIXED Source regions now start only at pre (L198-203). Hidden inline code content and script/copy button inside pre are asserted omitted.
5 [minor] display keyword lists miss table/ruby boxes PARTIAL (enum pushback accepted) Table-internal boxes added and tested. The three ruby* arms cannot match, though: Stylo parses those keywords only under feature = "gecko", and Moli builds the servo feature. Multi-keyword list-item serializations are also not covered. See the inline nit.
6 [minor] URL-attribute list diverges from html2md FIXED moli_html2md::resource_url_attributes is consumed by the adapter (L240-264) and covers source/srcset/data-original on media; tested for video/audio/iframe. Nit: the inventory repeats LAZY_SOURCE_ATTRIBUTES literally.
7 [minor] second full style pass FIXED One computed_style_export_values_for_document_snapshot call returns values and the own-visibility flag from the same observation.read (styles.rs:554-580); the old two-pass API is gone.
8 [minor] color invalidation / <font color> scope creep FIXED presentation.rs and mutation_effect.rs are no longer in the diff.
S1 [major] Cargo conflict with main / reverting main's Stylo; dead opacity code in pin FIXED git merge-tree --write-tree origin/main refs/review/new-1337 is clean. Stylo e2baa14 is a descendant of main's 973988b9, and git diff 973988b9 e2baa14 touches only cascade.rs (+5) and computed_value_flags.rs (+9). Taffy and the other main updates are kept.
S2 [major] overlap/contradicting contracts with #1592 FIXED #1592 is now based on codex/content-rendered-state and extends this MarkdownDom with per-document bases, first-wins (document, id) targets and frame roots, using the same URL inventory. Only one adapter remains.
S3 [minor] style inventory computed twice FIXED Same as #7.
S4 [minor] scope creep font color FIXED Same as #8.
S5 [minor] document export heuristics as policy PARTIAL The paint/size guesses are gone, but the remaining rules (aria-controls + section/article/details/tabpanel/region targets, uncontrolled tabpanel, hidden=until-found, nav/header/footer/menu/dialog exclusion, root veil, pre source retention) are described only in the PR body and in short code comments. Neither the README nor the #1380 docs covers them.
S6 [nit] long minified test scenarios NOT FIXED markdown_dump.rs still packs scenarios into 6 tests, with 87 lines over 100 columns. The root-veil + accordion case was added, as requested.
S7 CI n/a Format, Clippy, CDP smoke and harness checks pass. Nextest was still running and the release builds were queued at review time.

New findings

  1. [minor] The Stylo pin points at the head of an open fork PR (lexmount/stylo#2). Its branch history adds and then reverts opacity code, and its title is stale. (Cargo.toml)
  2. [minor] An uncontrolled role=tabpanel or hidden=until-found root inside a display:none ancestor is lost, while an aria-controls target in the same place is kept. (visibility.rs:151)
  3. [nit] Ruby display arms are unreachable in the servo build, and multi-keyword list-item values are not classified. (visibility.rs:226)
  4. [nit] resource_url_attributes repeats the lazy-source names instead of reusing LAZY_SOURCE_ATTRIBUTES. (media.rs:16)

Findings not tied to a diff line

  • [minor] Commits 1-4 do not compile on their own. 8ca089c70, 356adafe9, d75a11a22 and 1bd79a738 all reference ComputedValueFlags::HAS_OWN_VISIBILITY (style_engine/computed.rs), but Cargo.toml in those commits still pins Stylo 973988b9, which lacks the flag. The bump only arrives in c08ba7c3b. Commit 1 also adds about 700 lines of paint heuristics (opacity, final-keyframe opacity, color, framework attributes) that commit 2 deletes. git bisect breaks across the stack and the history misrepresents the design. Fix: squash-merge, or squash into one commit that carries the Stylo bump. Also add #1592 to the stack list in the PR body, since it now depends on this branch.
  • [nit] Fixture location and unrelated html2md edits. moli-html2md/tests/fixtures/hacker-news-layout-computed.md is consumed only by moli/tests/fetch_cli/markdown.rs, not by any moli-html2md test, so a converter crate now stores a renderer expectation. Move it under moli/tests/fixtures/ or name the owner. The doc-comment rewrite of html_fallback::needed (L5-7) belongs to the tables PR (#1336).

Automated review by Claude Code.

Comment thread Cargo.toml
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs Outdated
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs Outdated
Comment thread moli-html2md/src/media.rs Outdated
@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Stack-level review (#1334–#1337)

Scope. This review covers only problems that come from how the layers interact, or that exist only in the combined state. Each layer's own review is posted separately. Refs reviewed: origin/main bd8e0b27a ← #1334 e9dccde1e ← #1335 014debd29 ← #1336 ef514e6f4 ← #1337 3fb74bccb. Locations are file:line at 3fb74bccb, with the commit and PR that introduced them. I read the code only and built nothing. Where a finding depends on runtime output, I name the code path that produces it.

Summary

# Severity PR(s) Finding
1 High #1336 hook, #1337 impl UA-default display values reach has_block_layout, so every list in the live dump becomes loose and <option>s are split by blank lines
2 High #1337 vs table code Adapter reports hidden elements as NodeKind::Document; Table::from_dom rejects that, and the HTML fallback drops the <td>, which shifts later cells under the wrong headers
3 Medium #1337 has_text_boundary fires for every UA inline-block control; the Markdown path writes a newline and the HTML fallback writes a space
4 Medium #1334, #1336 Lower PRs ship Dom hooks that nothing outside tests implements until #1337; the README contract is out of date
5 Medium #1337 → #1336 A #1336 bug (fragment target on <textarea> lost in the HTML fallback) is fixed inside a #1337 renderer commit
6 Medium #1336, #1337 The renderer's unit tests run on a #[cfg(test)] path with no styles and no base URL, and the CLI tests use data: URLs. The production adapter is not covered for lists, controls or URL resolution
7 Medium #1337 Stylo is pinned to the head of an unmerged fork feature branch, inside an unrelated commit
8 Medium all Performance: at least 4 full-document passes plus one style read per element for each live conversion; the fragment-link skip almost never triggers
9 Low #1337 may_have_fragment_links uses a different predicate from the anchor prepass it is meant to skip
10 Low #1334–#1337 Five different role matching rules; two URL policies; a hand-copied attribute inventory
11 Low hygiene 4 empty ci: commits, a mis-titled commit, and add-then-revert churn
12 Info #1591, #1592 #1592 merges cleanly but rewrites #1337's adapter; #1591 (closed) conflicts with #1334 and is superseded

1. High: computed UA display makes live lists loose (#1336 hook semantics vs #1337 implementation)

What goes wrong (default moli fetch --dump markdown, which takes the fast path in page_dump.rs:49-58): <ul><li>a</li><li>b</li></ul>.
visit(li) calls boundary(2) on the list writer (writer.rs:269-277 sets breaks = 2). EndItem then writes block(item, 1, 1), which keeps max(2,1), and flush_breaks (writer.rs:490-503) emits - a\n\n- b. A nested <ul> inside an item calls boundary(2) on the item writer, which sets has_blocks, so the parent item becomes loose as well. The has_blocks restore at machine.rs:127-134 runs after that and does not undo it. <select> options pick up the same blank lines, on top of their own ChoiceBoundary (machine.rs:286-298). Source-only conversion produces tight lists, so the two paths now disagree for nearly every page. No live-path test covers a list: markdown_dump.rs has none, and the list tests in page_dump.rs:325,482,551 use the style-less render_markdown_document.

Fix. Pick one meaning for the hook and enforce it in one place. Either (a) the adapter reports a block only when computed display differs from the element's UA default (or only for elements html2md treats as inline), or (b) html2md ignores has_block_layout for elements it already structures itself (li, ul, ol, option, optgroup, select, table parts, headings, p, pre, blockquote). Add a live-path test for tight lists, nested lists and <select>.

2. High: "invisible ⇒ NodeKind::Document" breaks table structure (#1337 adapter vs table code)

What goes wrong: <table><tr><th>Name</th><th>Status</th><th>Owner</th></tr><tr><td>A</td><td style="visibility:hidden">–</td><td>alice</td></tr></table>. Source-only output is a correct GFM table. In the live output, from_dom returns None. has_data_semantics is true because of the ths, so the table goes through the HTML fallback, which writes <tr><td>A</td><td>alice</td></tr>. "alice" now sits under "Status". A visibility:collapse row also forces the GFM path into HTML. A browser keeps the hidden cell's box, so this is a mismatch with the actual rendering.

Fix. Do not overload NodeKind::Document. Either add an explicit Dom query such as suppresses_own_content(node) and keep reporting the real tag, or have the adapter keep the tag for table-structure elements (table, thead/tbody/tfoot, tr, td, th, caption, li) and hide only their text. Document in dom.rs and the README what Document may mean in a mid-tree position. Add a live test with a hidden mid-row cell.

3. Medium: has_text_boundary on UA inline-block controls (#1337), with different results in the two output paths

visibility.rs:220-231 marks every inline-block element, and the UA sheet makes input, textarea, select, button inline-block (ua/html.css:54-56). In the Markdown path (machine.rs:221-223) that produces boundary(1), a newline: Search <input value="q"> <button>Go</button> becomes Search\nq\nGo, and inside a GFM cell it becomes <br>. The HTML fallback (html_fallback.rs:186-189) inserts a space for the same element. One element therefore gets two different separators depending on which path converts it.
Fix. Apply the same UA-default rule as in #1. Use one separator semantic in both paths: boundary(1) should map to a space, or to a <br> inside fallback HTML, consistently.

4. Medium: hooks shipped without producers; README contract out of date (#1334, #1336)

Mergeability: #1334 and #1336 can each merge without breaking anything. But each adds public Dom API with no production consumer, and #1336's hook semantics are then redefined by #1337 (see #1). If the stack stops at #1336, those hooks stay dead.
Fix. Move each hook into the PR that first implements it (control_value and the three layout hooks into #1337), or implement control_value for NativeDom in #1334. Update README.md:5,19,44 in the PR that makes each claim false.

5. Medium: a #1336 bug fixed in #1337

c08ba7c3b (#1337, a renderer commit) removes "textarea" from html_fallback::allowed (at #1336 it is html_fallback.rs:380). Because the textarea arm at html_fallback.rs:221-227 never writes a tag, keeping it in allowed made html_fallback.rs:191-199 skip anchor markup. So in #1336, <textarea id="note"> referenced by #note inside a fallback table or <pre> loses its target. The same renderer commit also adds resource_url_attributes to moli-html2md/src/media.rs:8-32.
Fix. Move the allowed change, with a regression test, into #1336. Put the html2md API addition in its own html2md commit.

6. Medium: the production adapter path is barely tested (#1336, #1337)

Fix. Make the unit-test helper build a MarkdownDom from real computed styles (or delete it and move those cases into markdown_dump.rs). Restore an exact live snapshot. Add one CLI case served over http:// from the test server so that base-URL resolution is exercised.

7. Medium: Stylo pin to an unmerged fork branch, inside an unrelated commit (#1337)

Cargo.toml:115-121 and Cargo.lock move lexmount/stylo from 973988b9 to e2baa14a inside c08ba7c3b ("preserve source and disclosure boundaries in one style pass"). e2baa14a is the head of codex/moli-style-provenance, 38 commits ahead of stylo main. The bump brings in 4 fork commits (content provenance, two keyframe-opacity commits, then a revert down to the HAS_OWN_VISIBILITY flag). The net diff is cascade.rs +5, computed_value_flags.rs +9.
What goes wrong: if that branch is rebased or deleted, the pinned rev can no longer be fetched. Any other open PR that bumps stylo will also conflict on these 7 lines.
Fix. Squash the fork branch, then merge or tag it in lexmount/stylo. Bump the pin in its own commit at the start of #1337.

8. Medium: passes per live conversion (all layers combined)

Pass Where Introduced
Full-DOM collect_node_ids, including <head> and display:none subtrees page_dump.rs:17-23 #1337
One computed-style read per element (3 serialized strings each) page_dump.rs:24-45 #1337
6 linear passes over the element vector, plus parent-chain walks per disclosure root, plus an ancestor walk (content_context) per tabpanel or aria-controls target, which is O(n·depth) on tab-heavy pages visibility.rs:53-109,88-98,150,294-332 #1337
Anchor prepass: 1–2 full walks anchors.rs:46,62 #1336
Main conversion walk machine.rs main
Per table: has_data_semantics + needed + anchors::contains + from_dom + (has_cells or render) = 3–5 subtree walks machine.rs:427-499 #1336
Per pre/code: subtree_has_link + anchors::contains (+ render) machine.rs:355-378 #1336
Slow path (--strip/--with-base/--with-frames): DOM clone plus strip walk page_dump.rs:59-74 main

Every Dom query also goes through 2–5 HashSet/HashMap probes in the adapter (visibility.rs:358-402), and html2md makes many queries per node (has_block_layout, has_text_boundary, node_kind, serialized-state check, anchor targets, primary_alternative, fallback_text).
Fix. Build the adapter state during the pre-order collection itself, restricted to the body subtree when the root is body. Store a per-node flags struct in one HashMap<NodeId, Flags> rather than seven sets. Have the adapter compute referenced fragment targets once and hand them to html2md, which removes both anchor walks. Cache the table classification from one subtree walk.

9. Low: the fragment-link skip uses a different predicate (#1337 vs #1336)

visibility.rs:57-59 sets fragment_links for any href starting with #, including href="#", and it scans every element, suppressed ones included. anchors.rs:47-50 counts only non-empty fragments. Nearly every real page has an href="#" or a skip link, so may_have_fragment_links (anchors.rs:39) almost never avoids the prepass.
Fix. Use the same predicate (strip_prefix('#') and non-empty), or remove the hook in favour of passing targets in (see #8).

10. Low: inconsistent output rules across helpers

  • role matching uses five rules. First token only: content.rs:10,42. Whole value, ASCII-case-insensitive: content.rs:100. Whole value with Unicode trim(): machine.rs:428-433, html_fallback.rs:63-67. Whole value with HTML-space trim: table.rs:29-32. Any token: visibility.rs:334-338. As a result, role="presentation none" counts as presentational for labels but not for tables.
  • URL policy differs by element. Media uses an allowlist (media.rs:251-267), links use a denylist (media.rs:307-319), and the fallback applies both by attribute name (html_fallback.rs:304-306). If that split is intentional, document it.
  • The attribute inventory is copied by hand. resource_url_attributes (media.rs:11-32, fix(renderer): export live rendered content through shared markdown conversion #1337) repeats LAZY_SOURCE_ATTRIBUTES (media.rs:6, fix(html2md): preserve media resources and readable controls #1334) as literals and also resolves source[srcset], which html2md never reads (media.rs:62-69 reads only src and the lazy attributes). Derive it from the const.
  • Labels follow three separate rules. content::fallback_text (aria-label, then title), media::label (title only, then an English default) and form::input_text (value, then aria-label, then title).

Fix. Write one role_tokens() helper and one documented URL policy module, and derive the inventory from the constants the converter reads.

11. Low: stack hygiene

Fix. Drop the empty commits and squash the add-then-revert pairs before merging.

12. Info: overlapping PRs

Automated review by Claude Code.

@lanyue-llk
lanyue-llk force-pushed the codex/content-rendered-state branch from 3fb74bc to f5e6630 Compare October 22, 2026 06:26
@lanyue-llk
lanyue-llk force-pushed the codex/content-rendered-state branch from f5e6630 to f04b0b1 Compare October 22, 2026 06:32

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-3 re-review of the updated version (f04b0b151)

Re-review of the updated version (round 3, head f04b0b151, base #1336 9c416cf06). This PR makes the renderer's Markdown export read live state (computed display/visibility/baseline-shift, Stylo own-visibility provenance, live form values, base-resolved URLs) through a MarkdownDom adapter over moli-html2md. The only new commit, f04b0b151, fixes all four round-2 inline findings. It does not change the three main stack-level problems (#1–#3 in the stack review) that land in this PR. They are still reproducible by tracing the code, so this round is final and lists everything that is still open.

Verdict: request changes. Two major output regressions remain on the default live path: every list is emitted with blank lines between items (looks loose), and a hidden table cell moves later cells under the wrong headers. Neither has a live-path test.

Previous findings

# Finding Status Evidence
R2-1 [minor] Stylo pin on an unmerged fork PR Pushback accepted, merge-order item remains stylo#2 is retitled "record own visibility after cascade". refs/pull/2/head keeps e2baa14 fetchable by full SHA. Remaining: stylo#2 is still OPEN, so land it before or with this PR.
R2-2 [minor] uncontrolled tabpanel / hidden=until-found under display:none lost FIXED (one regression, see new #4) They join disclosure_roots before paths are built (visibility.rs:83-89). The new markdown_nested_disclosures_and_resource_urls test fails without this. But content_context does not exclude the native <dialog> element, so a tabpanel inside a closed <dialog> is now exported.
R2-3 [nit] dead ruby arms / multi-keyword list-item FIXED Classification now uses the outer type (L208-215), and the ruby arms are gone. The new unit test covers flow-root list-item, inline list-item and contents.
R2-4 [nit] lazy names repeated in resource_url_attributes FIXED The list is now chained from LAZY_SOURCE_ATTRIBUTES (media.rs:11-28). Small nit about the duplicated tag list, see new #7.
R1-5 [minor] display keyword lists FIXED Superseded by R2-3.
R2-S5 [minor] export policy documented only in PR body PARTIAL README:105-107 adds one sentence. The rules themselves (aria-controls target kinds, uncontrolled tabpanel, until-found, nav/header/footer/menu/dialog exclusion, root veil, pre retention) are still not documented.
R2-S6 [nit] minified test scenarios NOT FIXED The new test adds more lines over 100 columns (markdown_dump.rs:449-450).
R2-N1 [minor] commits 1–4 do not compile NOT FIXED range-diff shows commits 1–4 unchanged. 229809ab0…a13955ea4 use HAS_OWN_VISIBILITY with Stylo 973988b9. 229809ab0 still adds about 1.4k lines that later commits delete.
R2-N2 [nit] computed fixture under moli-html2md/tests/fixtures; needed doc rewrite belongs to #1336; #1592 missing from stack list NOT FIXED The fixture is still consumed only by moli/tests/fetch_cli/markdown.rs. d0d841ea0 still rewrites html_fallback.rs:7-8. The PR body stack line still ends at #1380.
Stack #1 [major] UA display reaches has_block_layout, so lists look loose and options get blank lines NOT FIXED L209-214 is now broader: every non-inline display, including UA li/ul/option/optgroup, goes into blocks. See inline.
Stack #2 [major] hidden element → NodeKind::Document breaks tables NOT FIXED visibility.rs:347-351 unchanged; table.rs add_row _ => return None. See inline.
Stack #3 [minor] UA inline-block controls get a boundary; Markdown writes \n, fallback writes a space NOT FIXED L210-211 still matches input/button/select/textarea through the UA inline-block, and the new unit test now asserts that inline-block produces \n.
Stack #4 [minor] README contract out of date NOT FIXED (README edited here) README:5 "implements the interface directly", :19 "four borrowed queries" (the trait has eight), :44-45 "never rescan cell descendants".
Stack #5 [minor] #1336 fix inside #1337 PARTIAL The textarea allowed change moved to #1336 (fixed). The resource_url_attributes html2md API is still added inside renderer commit d0d841ea0.
Stack #6 [minor] production adapter barely tested NOT FIXED render_markdown_document is still a style-less #[cfg(test)] path, and CLI tests still use data: URLs. There is still no live list, select or control test.
Stack #7 [minor] Stylo bump inside an unrelated commit NOT FIXED Still in d0d841ea0. Moot if squash-merged.
Stack #8 [minor] passes per conversion NOT FIXED Unchanged, and f04b0b151 adds a 7th styles pass. See inline.
Stack #9 [low] fragment-link predicate differs NOT FIXED visibility.rs:57-59. See inline.
Stack #10 [low] URL inventory resolves inputs html2md never reads NOT FIXED source srcset/data-srcset (media.rs:14) are resolved, but sources() reads only src and lazy attributes on <source>.
Stack #11 [low] hygiene NOT FIXED Same as R2-N1 and Stack #7.

New findings (all inline)

  1. [major] Live lists and <select> options get blank lines from UA display, and the fallback writes <div><option> (visibility.rs:213). Stack #1, still open. The fallback <div> part was missed earlier.
  2. [major] A hidden table cell shifts later cells under the wrong headers (visibility.rs:350). Stack #2, still open.
  3. [minor] UA inline-block controls become newlines in Markdown, <br> in GFM cells, and spaces in fallback HTML (visibility.rs:211). Stack #3, still open.
  4. [minor] New regression from the R2-2 fix: an uncontrolled tabpanel inside a closed native <dialog> (or an unopened [popover]) is now exported (visibility.rs:88).
  5. [low] fragment_links predicate (visibility.rs:58). Stack #9, still open.
  6. [minor] Live adapter not covered by unit/CLI tests (page_dump.rs:229). Stack #6, still open.
  7. [nit] Duplicated tag list and unread source srcset in resource_url_attributes; misnamed names (media.rs:19).
  8. [nit] README stale lines and unwrapped line (README.md:107). Stack #4, still open.
  9. [minor] Per-conversion passes (page_dump.rs:19). Stack #8, still open.

Not tied to a diff line

  • Commit hygiene (unchanged): commits 1–4 do not build. 229809ab0 adds and a6fd0bdd1/d0d841ea0 remove presentation.rs/mutation_effect.rs/host_environment.rs paint code. The Stylo bump and the html2md resource_url_attributes API sit in a renderer commit. Fix: squash-merge, or squash into one commit with the Stylo bump first and the html2md API in its own commit.
  • Merge state: git merge-tree against #1336 and against current origin/main (91b72e91b) is clean. The stack no longer sits on the origin/main tip, but nothing conflicts.
  • CI at review time: Format, CDP smoke, harness tests and SecTrust pass. Clippy is in progress, and Nextest and both release builds are queued. The PR body claims 30 page-export tests pass, but none of them covers a list, a <select> or a hidden table cell on the live path.

Verdict: changes requested (posted as a comment because GitHub does not allow requesting changes on your own PR).

Automated review by Claude Code.

Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs Outdated
Comment thread moli-renderer-v8/src/runtime/page_dump.rs
Comment thread moli-html2md/src/media.rs Outdated
Comment thread moli-html2md/README.md Outdated
Comment thread moli-renderer-v8/src/runtime/page_dump.rs Outdated
@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Stack-level re-review (#1334–#1337), round 3

Scope. This round covers only cross-layer and combined-state problems. Per-layer issues are left to the per-PR reviews. Refs: origin/main merge-base bd8e0b27a ← #1334 54c1d1cb7 ← #1335 0783f7d93 ← #1336 9c416cf06 ← #1337 f04b0b151. All locations are file:line at f04b0b151 unless marked otherwise. I produced this by reading the code, not by running it. I built nothing and ran no tests. Every output below comes from tracing the named code path by hand.

What changed since round 2: five new commits (54c1d1cb7 in #1334, b94d257c5 and 0783f7d93 in #1335, 9c416cf06 in #1336, f04b0b151 in #1337) and rewrites of 0154b06c3, 014debd29, 099117b81, 9340ad8e1, 3d74ee098, ef514e6f4, c08ba7c3b and 3fb74bccb. No author reply to the round-2 comment was posted on #1337. git merge-tree origin/main f04b0b151 is clean. #1592 has been rebased onto f04b0b151.

Status of the round-2 findings

# Round-2 finding Status Evidence at f04b0b151
1 UA display makes live lists and <select> loose NOT FIXED (and now asserted by a test) see below
2 Hidden element reported as NodeKind::Document breaks tables NOT FIXED see below
3 has_text_boundary on UA inline-block controls; newline in Markdown, space in fallback NOT FIXED (newline now asserted by a test) see below
4 Hooks shipped without producers; README out of date NOT FIXED dom.rs:32 control_value is still in #1334 and dom.rs:39-53 is still in #1336. The only non-test implementation is visibility.rs:388-405 (#1337). README.md:5 still says NativeDom "implements the interface directly", :19 still says "four borrowed queries" (the trait has eight), and :44-45 still says "eligibility checks never rescan cell descendants", which html_fallback.rs:9-131 contradicts.
5 #1336 bug fixed inside a #1337 renderer commit PARTIAL The textarea removal from allowed now lives in #1336 (9c416cf06; html_fallback.rs:410-458 has no textarea), but no regression test for a #id target on a <textarea> came with it. The html2md API addition resource_url_attributes (media.rs:11-28, lib.rs:22) is still inside the renderer commit d0d841ea0. A new instance in the opposite direction appeared; see N1.
6 Production adapter barely tested PARTIAL Fixed: markdown_dump.rs:447-469 now resolves lazy img/video URLs against an https:// base through the real adapter. Still open: page_dump.rs:228-231,264-267 is still the style-less, base-less helper, and its assertions still expect relative URLs (page_dump.rs:347,380,416). The new page_dump.rs:270-309 feeds hand-written display strings ("inline flow", "inline flow-root list-item") instead of Stylo serializations. No live-path test covers ul/ol, select, form controls or a hidden table cell. CLI tests still load data: URLs (moli/tests/fetch_cli/markdown.rs:16).
7 Stylo pinned to an unmerged fork branch inside an unrelated commit NOT FIXED Cargo.toml:115-121 is still e2baa14a…, still bumped inside d0d841ea0. GitHub compare main...e2baa14a reports ahead_by: 38. The commit is the head of codex/moli-style-provenance only, and no tag points at it.
8 Several full-document passes per live conversion NOT FIXED (one pass added) Every round-2 pass remains. visibility.rs:83-89 (f04b0b151) adds another linear pass over all elements, with two attribute probes each plus a content_context ancestor walk per tabpanel/until-found. disclosure_paths (:94-105) now walks to the root for each of those as well.
9 may_have_fragment_links predicate differs from the prepass NOT FIXED visibility.rs:57-59 still uses starts_with('#') over every element, including href="#" and suppressed nodes. anchors.rs:47-50 requires a non-empty fragment.
10 Inconsistent role, URL and label rules; hand-copied inventory PARTIAL Fixed: the inventory now derives from LAZY_SOURCE_ATTRIBUTES (media.rs:19-27). Partly fixed: content.rs:73-80 adds shared primary_role/role_matches, used only inside content.rs. Five role rules remain: first token (content.rs:73), whole value without trim (content.rs:107), trimmed and lowercased (machine.rs:424-429, html_fallback.rs:66-70), HTML-space trim (table.rs:29-32), any token (visibility.rs:318-322). role="presentation none" still counts as presentational for labels and images but not for tables. source[srcset]/data-srcset are still resolved although html2md never reads them (media.rs:265-291). content::image_label (content.rs:82) adds a fourth label rule.
11 Stack hygiene NOT FIXED (more churn) 4 empty commits remain (8663d9a89, 48001fb55, 9bafbdf34, 49890f527). 381ae9c9c is still titled fix(ci) but only moves test code. The 2757784a9/e9dccde1e pair still nets to zero. New: 0783f7d93 is a formatting fixup of b94d257c5. 9c416cf06 (#1336) deletes the README sentence about Moli's exporter, and f04b0b151 (#1337) re-adds it with new wording. It also drops #1335's README text on TeX and MathML. 54c1d1cb7 is titled html2md but rewrites a renderer file (N1).
12 Overlapping PRs unchanged / info #1592 (head 7dbeedb5d) now sits directly on f04b0b151 and keeps the blocks/invisible sets unchanged. It therefore inherits #1, #2, #3 and N2. #1591 is still closed.

1. Lists and <select>: traced at f04b0b151

f04b0b151 changes the classifier, not the semantics. visibility.rs:208-215 now puts every display value other than "", none, contents, inline or inline flow into blocks unless it starts with inline. UA li is still list-item and ul/ol/option/optgroup are still block (ua/html.css:43-47,159-162,484-486), so all of them still reach has_block_layout. html2md applies the hook to every node with no tag exemption (machine.rs:214-216). The #1336 test adapter still models only explicit style="display:block" (9c416cf06:tests/review_semantics.rs:26-28). The new test pins the round-2 behaviour: page_dump.rs:272 expects "flow-root list-item" to give "Before\n\nItem\n\nAfter".

Trace for <ul><li>a</li><li>b</li></ul> (default moli fetch --dump markdown, fast path page_dump.rs:49-58):

  1. visit(li₁): has_block_layout → W_list.boundary(2) (writer.rs:269-277; breaks=2) and Task::Boundary is queued below the item's tasks.
  2. start_item (machine.rs:641-660) captures W_item and pushes EndItem. EndItem (machine.rs:132-156) sees has_blocks=false and calls W_list.block(item, 1, 1). block (writer.rs:291-307) calls boundary(1), which keeps max(2,1). flush_breaks (writer.rs:490-504) writes nothing because the writer is empty. Then breaks=1.
  3. The queued Task::Boundary sets breaks=2, and visit(li₂) sets breaks=2 again. When li₂'s EndItem calls block(), flush_breaks writes two \n.

The live result is - a\n\n- b; source-only gives - a\n- b. For a nested list, visit(ul) calls boundary(2) on the parent item's writer before start_list, so has_blocks is already true when EndList (machine.rs:122-131) saves and restores it. The parent item becomes loose as well. A select: Sort by <select><option>Name</option><option>Date</option></select> asc gives Sort by\n\nName\n\nDate\n\nasc live and Sort by\nName\nDate\nasc from source. Fix as proposed in round 2: make one layer own the meaning. Either the adapter reports blocks only where computed outer display differs from the element's UA default, or html2md ignores has_block_layout for tags it already structures (li, ul, ol, option, optgroup, select, table parts, headings, p, pre, blockquote). Then add live-path tests for a tight list, a nested list and a select.

2. Hidden cells: traced at f04b0b151

visibility.rs:347-351 still returns NodeKind::Document for invisible nodes, and table.rs:99-131 still returns None for any non-td/th row child. Take <table><tr><th>Name</th><th>Status</th><th>Owner</th></tr><tr><td>A</td><td style="visibility:hidden">–</td><td>alice</td></tr></table>. requires_table_structure (html_fallback.rs:87, because of the ths) is true and needed is false. from_dom returns None, has_cells returns true, and render runs. The Document arm (html_fallback.rs:400-402) writes no <td>, and the cell's text is Other (visibility.rs:352-358). The output row is <tr><td>A</td><td>alice</td></tr>, which puts "alice" under "Status". Without th (9c416cf06 narrowed structural_table), the same table drops to boundary(2) and every cell becomes its own paragraph, while the source path gives a GFM table. f04b0b151 adds another producer of Document: every ancestor on a disclosure_paths chain (visibility.rs:160-162). A display:none <li> or <tr> that wraps a tabpanel therefore loses its list marker or row too. The round-2 fix still applies.

3. Inline-block boundaries: traced at f04b0b151

The new classifier sends every inline* value to inline_boundaries (visibility.rs:210-211). page_dump.rs:275 asserts that inline-block gives "Before\nItem\nAfter". The Markdown path calls boundary(1), a newline (machine.rs:217-219), and the fallback writes a space (html_fallback.rs:194-196). Two more effects of the same path:

  • Inside a GFM cell the newline becomes <br> (table.rs:197-199). <td>Qty <input value="3"> <button>Update</button></td> gives Qty<br>3<br>Update.
  • Writer::boundary closes every emitted inline style (writer.rs:273), and the style is reopened on the next text. So an icon-font element inside a link splits the link. Font Awesome sets .fa { display:inline-block }, so <a href="/x">Download <i class="fa fa-file"></i> PDF</a> gives [Download](/x)\n[PDF](/x) live and [Download PDF](/x) from source.

Fix: apply the same UA-default rule as in #1, and map boundary(1) to one separator in both paths: a space, or <br> only where a real line box ends.

New findings

# Severity PR Location Summary
N1 Medium #1334 moli-renderer-v8/src/native_bridge/element/images/src.rs:776-796, moli-html2md/src/media.rs:90-151 An html2md commit replaces the renderer's spec srcset parser, which drives web-observable currentSrc, with a #[doc(hidden)] html2md helper that handles parentheses differently
N2 Medium #1336 × #1337 (missed in round 2) html_fallback.rs:186-197 Computed display boundaries are inserted inside <pre>/<code> HTML fallback and change source text; the raw code path ignores them
N3 Low #1334 × #1337 (missed in round 2) visibility.rs:130-143, form.rs:26-41 Live input values print sanitized defaults (50, #000000) for untouched range and color inputs, replacing their accessible names
N4 Low #1337 (f04b0b151) visibility.rs:208-215 The catch-all classifier makes UA table-column/table-column-group blocks, so fallback tables gain <div>s inside <table>

N1. Medium (#1334): renderer image selection now depends on html2md's srcset parser

54c1d1cb7 ("fix(html2md): keep inline media flow and accessible alternatives", #1334) deletes the renderer's tokenizer and descriptor validator (−220 lines in images/src.rs). parse_srcset_candidates (images/src.rs:776-796) now wraps moli_html2md::parse_srcset. That function is used by selected_srcset_candidate (:688-760), so it decides img.currentSrc and which image is fetched. A Markdown converter's #[doc(hidden)] helper now sets page-visible browser behaviour, and the change sits in a PR and commit titled html2md.

The two parsers differ. The deleted state machine followed HTML's "parse a srcset attribute": ( enters in-parens and the first ) leaves it, with no nesting. media.rs:124-133 counts nesting depth instead.

  • Input: <img src="fallback.png" srcset="a.png ((x), b.png 2x">.
  • Before: (, (, ) returns to in-descriptor, and , ends candidate 1. "((x)" is invalid and is dropped, then b.png 2x is a valid candidate, so currentSrc is b.png.
  • Now: the depth goes 2 → 1 and the , does not terminate, so the descriptor runs to end of string: "((x), b.png 2x". descriptor_score returns None and no candidate is left, so currentSrc falls back to fallback.png.

The remaining renderer tests (images/src.rs:1083-1135) do not cover nested parentheses. Every future html2md tweak to parse_srcset (it already carries a Markdown-only score) now changes browser behaviour too.
Fix. Keep the spec parser in the renderer, or move it to a neutral shared crate with its own WPT-derived tests. Either way, put that change in a renderer commit with a renderer title, not inside an html2md commit. If the parser is shared, use the spec's non-nesting paren state.

N2. Medium (#1336 fallback × #1337 adapter, missed in round 2): computed boundaries rewrite linked code

html_fallback::render computes code (html_fallback.rs:189) but applies has_block_layout and has_text_boundary (:190-197) whatever its value. Inside a <pre>/<code> rendered through the fallback (any linked or targeted code block, machine.rs:349-374), an inline-block span inserts a literal space before and after itself into the source. A display:block span or code (code is not in has_block_tag) inserts <div>…</div>.

  • Input: <pre><code>let v = <span style="display:inline-block">f</span>(<a href="/doc">x</a>);</code></pre>
  • Live: <pre>\n<code>let v = f (<a href="/doc">x</a>);</code></pre>, which adds two characters that are not in the source.
  • Source-only, or the same block without the link (raw path machine.rs:375-389, which never calls visit): let v = f(x);

The text of a code block therefore depends on whether it happens to contain a link. Per-line highlighters that style lines as display:block/table-row (with gutters as table-cell/inline-block) produce <div> wrappers inside <pre> as well.
Fix. In render, skip both computed-boundary hooks while code is true, matching the raw path. If a CSS-only line break inside pre must be kept, emit a literal \n only for a block boundary, never a space.

N3. Low (#1334 html2md × #1337 adapter, missed in round 2): sanitized input defaults leak into the live dump

The adapter overrides value with element.input_value() for every HTML input (visibility.rs:138-143). input_value() returns the control's sanitized value, which is non-empty even when no value attribute exists: range gives the midpoint (moli-dom/src/forms/input_type.rs sanitize_range_value) and color gives #000000 (sanitize_color_value). form::input_text (form.rs:26-41) prefers any non-empty control_value over aria-label/title.

  • Input: <label>Volume <input type="range" aria-label="Volume level"></label> <input type="color" title="Theme">
  • Live: Volume\n50\n#000000 (UA inputs are also inline-block; see Fix flex max-content text wrapping precision #3)
  • Source: Volume Volume level Theme

A browser shows a slider and a swatch, not those strings.
Fix. In html2md, treat range and color like the other non-textual controls (accessible name only), or have the adapter supply control_value only for text-entry types or when input_value_dirty().

N4. Low (#1337, f04b0b151): the catch-all classifier adds <div>s inside fallback tables

The negative match at visibility.rs:209 now also classifies UA col (table-column) and colgroup (table-column-group) as blocks, and the old allow-list excluded both. Neither is in has_block_tag, so html_fallback.rs:191-193 wraps each one in <div>. A fallback table with <colgroup><col><col></colgroup> emits <table>\n<div><div></div><div></div></div>\n<thead>…. Browsers foster-parent those <div>s out of the table, but the output is no longer valid table HTML. -webkit-inline-box, if Stylo parses it, is likewise sent to blocks, because it does not start with inline.
Fix. Use an explicit allow-list again, adding the outer display types f04b0b151 meant to cover (flow-root list-item, inline list-item), or exclude table-column*.

Recommended merge gate

#1 and #2 are still output-correctness regressions on the default moli fetch --dump markdown path, so they should block #1337. N1 should be resolved before #1334 merges, because it changes browser behaviour from a PR titled as html2md-only. N2 and #3 can land together, since they share the boundary-mapping fix. The rest can follow.

Automated review by Claude Code.

@lanyue-llk
lanyue-llk force-pushed the codex/content-rendered-state branch from f04b0b1 to 9272068 Compare October 22, 2026 07:18

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-4 re-review of the updated version (92720683b)

Round-4 re-review (head 92720683b, base #1336 99e374401). This PR makes the renderer's Markdown export read live state (computed display/visibility/baseline-shift, Stylo own-visibility, live control values, base-resolved URLs) through the MarkdownDom adapter over moli-html2md. The new commit 92720683b and the #1336 commit 68cc9e546 beneath it fix both blocking stack findings. Live lists stay tight and <select> options stay on single lines, because content::owns_boundaries makes the converter ignore CSS boundaries on elements it already structures. A visibility:hidden cell keeps its <td>, so later columns no longer shift. The new markdown_live_structure_and_visibility_have_exact_outputs test fails on the round-3 code for each of these cases. I traced the code by hand and did not build or run anything.

Verdict: request changes, small. The two majors are gone. One minor regression came in with the hidden-cell fix: a hidden table that needs the HTML fallback now emits an empty <table> skeleton. Two minor/medium stack items are also still open: CSS boundaries inside fallback <pre>/<code> (#1336 layer) and loose lists from block-styled item children, which the new README sentence says cannot happen. Everything else is low or nit, or is stack hygiene that a squash merge removes.

Previous findings

# Finding Status Evidence at 92720683b
R3-1 / stack #1 [major] UA display makes lists loose and splits <select> FIXED machine.rs:216-219 and html_fallback.rs:192-198 skip both hooks when owns_boundaries(tag) holds (is_structural_block + select/optgroup/option; content.rs:140-142). <ul><li>a</li><li>b</li></ul> → - a\n- b; nested list and select/optgroup are exact-tested (markdown_dump.rs:477-479). No more <div><option>. A residual looseness case is in new #2.
R3-2 / stack #2 [major] hidden element → NodeKind::Document shifts table columns FIXED (one regression, new #1) visibility.rs:358-382 keeps table/list tags. Text under an invisible parent is Other (L383-390). …<td style=visibility:hidden>private</td>… → | A | | alice | (exact test L482).
R3-3 / stack #3 [minor] UA inline-block controls → \n in Markdown, <br> in cells, space in fallback FIXED The adapter skips native controls whose display is exactly inline-block (L213-218). html2md maps has_text_boundary to a space in both paths (Task::InlineSpace, machine.rs:80/221). Search q Go and | Alice Edit | are exact-tested. The Font Awesome link split is gone too, because text(" ") does not close styles. Design nit in new #6.
R3-4 [minor] tabpanel inside closed <dialog>/[popover] exported FIXED content_context rejects the dialog element and any popover attribute (L289-292). The exact test omits both panels.
R3-5 / stack #9 [low] fragment-link predicate FIXED Requires a non-empty fragment (L57-62), matching anchors::referenced. Head links are out of scope now.
R3-6 / stack #6 [minor] production adapter barely tested FIXED 8 exact live cases × strip on/off (markdown_dump.rs:471-492), plus an HTTP FixtureServer CLI test with base-resolved URLs (fetch_cli/markdown.rs:75-95). Pushback accepted on keeping the source-only unit helper.
R3-7 / stack #10 [nit] duplicated tag list, unread source srcset, names FIXED One (standard, lazy) match (media.rs:12-18). source advertises src and lazy inputs only. Renamed to tag.
R3-8 / stack #4 [nit] README stale; R2-S5 export policy undocumented FIXED (wrap nit remains) L5/L20/L48 corrected, and the policy paragraph is added at L163-169. L5 (97 cols) and L20 (99 cols) are still unwrapped, and L169 overclaims (new #2).
R3-9 / stack #8 [minor] many full passes per conversion PARTIAL, pushback accepted for the rest Styles are now collected for <html> + body subtree only (page_dump.rs:19-27). The 7 sets and per-panel ancestor walks remain. Accepted as not worth a redesign without a measured bottleneck.
R2-1 [minor] Stylo pin on unmerged fork PR merge-order item, unchanged lexmount/stylo#2 still OPEN at e2baa14.
R2-S6 [nit] minified tests NOT FIXED New test lines reach ~230 cols (markdown_dump.rs:475-485), and the CLI test args line is ~190.
R2-N1 / stack #11 [minor] commits 1–4 don't build; churn NOT FIXED (more churn) Commits 1–4 are unchanged. New: 040269fd4 adds a blank line between a doc comment and fn needed (clippy empty_line_after_doc_comments) and a double blank in lib.rs, both removed by 92720683b. 3511c199a adds hacker-news-layout-computed.md and 92720683b deletes it. Moot with a squash merge.
R2-N2 [nit] computed fixture in html2md; needed doc in #1337; #1592 missing from stack line PARTIAL Fixture deleted, and html_fallback.rs is no longer in the net diff. The PR body stack line still ends at #1380.
stack #5 / #7 html2md API and Stylo bump inside renderer commit NOT FIXED (moot if squashed) Both still in 040269fd4.
stack N2 [medium] computed boundaries inside fallback <pre>/<code> NOT FIXED html_fallback.rs:190-199 (#1336 file, touched by 68cc9e546 this round) still ignores code. See below.
stack N3 [low] range/color sanitized defaults NOT FIXED See new #4.
stack N4 [low] col/colgroup become <div>s in fallback tables NOT FIXED See new #5.

New / still-open findings

  1. [minor] Regression from the hidden-cell fix: a hidden table that takes the HTML fallback now emits an empty <table> skeleton, and visibility:collapse rows become empty GFM rows (visibility.rs:358).
  2. [minor] Lists are still loose when an item's child is block-styled (li > a{display:block}, common in nav and dropdown menus). The README claim at L169 is false for this case.
  3. [medium, #1336 × #1337, stack N2 still open] Live CSS boundaries change the text of linked code blocks (below, not anchorable in this diff).
  4. [low] Untouched range/color inputs print 50 / #000000 (visibility.rs:145). Stack N3.
  5. [low] UA table-column/table-column-group are still blocks, so fallback tables gain <div>s (visibility.rs:222). Stack N4.
  6. [nit] The control exemption is a tag-and-string special case split across two layers (visibility.rs:217).
  7. [nit] README wrapping and placement (README.md:20).

3. (not anchorable: html_fallback.rs is not in this PR's diff) Computed boundaries rewrite linked code

html_fallback::render computes code (L190) but applies has_block_layout/has_text_boundary without checking it (L192-199). 68cc9e546 added the owns_boundaries guard on these exact lines but did not add !code. The adapter is the only producer of these hooks, so this only happens on the live path.

  • Input: <pre><code>let v = <span style="display:inline-block">f</span>(<a href="/doc">x</a>);</code></pre>
  • Live: <pre>\n<code>let v = f (<a href="/doc">x</a>);</code></pre>, with two characters that are not in the source. A display:block line span produces <div>…</div> inside <pre>.
  • Source path, or the same block without a link (raw path, which never calls visit): let v = f(x);.
    Fix: skip both hooks while code is true (if !code && let NodeKind::Element(tag) = …), and add one live case with a linked <pre> to the new exact-output test.

Other

  • Merge state: git merge-tree against #1336 (99e374401) and against origin/main (45be4e353) is clean.
  • CI: Format, Clippy, CDP smoke, harness tests, SecTrust and Windows storage pass. Nextest and both release builds are still queued, so the new exact-output and HTTP CLI tests have not run in CI yet.

Verdict: changes requested (posted as a comment because GitHub does not allow requesting changes on your own PR).

Automated review by Claude Code.

Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs
Comment thread moli-html2md/README.md Outdated
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs Outdated
Comment thread moli-html2md/README.md Outdated
@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Stack-level re-review (#1334–#1337), round 4

Scope. This round covers only cross-layer and combined-state problems. Refs: origin/main merge-base bd8e0b27a ← #1334 fb466fc6e ← #1335 f3c46b402 ← #1336 99e374401 ← #1337 92720683b. Unless marked otherwise, every location is file:line at 92720683b. I produced this by reading the code, not by running it. I built nothing and ran no tests. Each output below comes from tracing the named code path by hand. Stylo references point to the pinned revision e2baa14 from the local cargo checkout.

Changes since round 3. There are seven new commits: 2ff73d38e and fb466fc6e in #1334, f3c46b402 in #1335, 68cc9e546 and 99e374401 in #1336, and 92720683b in #1337. Five commits were rewritten (266c101fe, ba1f08f9d, 5dbb582f7, 2bc31c4a8, 7735789ca), and 040269fd4 replaces d0d841ea0. Nobody replied to the round-3 comment. git merge-tree origin/main 92720683b is clean.

This round fixes the two round-3 blockers in the cases we wrote down:

  • Tight lists and <select> now render the same on the live path as from source. html2md's new owns_boundaries (content.rs:140-142, used at machine.rs:216-223 and html_fallback.rs:192-201) handles this.
  • Hidden table cells keep their column. The adapter now reports invisible table and list parts as their own elements (visibility.rs:358-382).

Both fixes have exact-output live tests (page_vm/tests/markdown_dump.rs:471-491). The same class of bug is still reachable through display values that Stylo has blockified (N5 below), and the README now claims it is not.

Status of the round-3 findings

# Finding Status Evidence at 92720683b
1 UA display makes live lists and <select> loose FIXED for UA defaults; PARTIAL overall li/ul/ol/option/optgroup/select and table parts now skip the computed hooks (machine.rs:216-223, content.rs:140-142). I re-traced <ul><li>a</li><li>b</li></ul>: visit(li) gets no boundary(2), EndItem sees has_blocks=false, and the output is - a\n- b. The nested-list and select cases also match their tests (markdown_dump.rs:477-479). Residual: a non-structural child with a computed block still marks the item loose, because Writer::boundary(2) sets has_blocks even on an empty writer (writer.rs:277), and EndItem reads that flag (machine.rs:135,155). Example: a nav list styled li a { display:block }, <ul><li><a href="/a">A</a></li><li><a href="/b">B</a></li></ul>, gives - [A](/a)\n\n- [B](/b) live and - [A](/a)\n- [B](/b) from source. That contradicts README.md:168-169 ("do not turn native lists into loose lists"). The bigger remaining path is N5.
2 Hidden element reported as NodeKind::Document breaks tables FIXED visibility.rs:358-382 keeps table/thead/tbody/tfoot/tr/td/th/caption/list parts as elements, and their text children are still Other (:383-390). The round-3 table now gives | A | | alice | (test markdown_dump.rs:482). This also covers display:none <tr>/<li> ancestors on a disclosure_paths chain. Side effect, not a regression: a visibility:collapse row now emits an all-empty GFM row. Before, it broke the table.
3 Inline-block boundary is a newline in Markdown and a space in the fallback FIXED as reported Both paths now insert one space (machine.rs:220-223 Task::InlineSpace, html_fallback.rs:198-201). text(" ") does not close open styles, so <a href="/x">Download <i class="fa"></i> PDF</a> now stays [Download PDF](/x). Qty <input value="3"> <button>Update</button> in a cell has no <br>. Native controls with UA inline-block are exempt (visibility.rs:213-218). Two gaps remain: inside code see N2, and our round-3 suggestion of "a space" is itself wrong for per-character spans (N6).
4 Hooks without producers; README out of date PARTIAL The README is corrected only in #1337 (README.md:5,20-26,48). #1334–#1336 still ship "implements the interface directly", "four borrowed queries" and "eligibility checks never rescan cell descendants" (README.md:5,19,40 at 99e374401), while #1336's dom.rs:25-51 already has eight methods. The only producer is still #1337's adapter.
5 Changes filed in the wrong layer PARTIAL The textarea fix now lives in #1336. Still wrong: resource_url_attributes is added to html2md's public API inside the renderer commit 040269fd4 (media.rs:8-28, lib.rs:21), and the #1337 fetch commit 92720683b changes its semantics. In the other direction, the html2md-titled #1334 commit 2ff73d38e edits moli-renderer-v8/.../images/src.rs:776-792 again (see N1).
6 Production adapter barely tested FIXED (mostly) markdown_dump.rs:471-491 drives real Stylo through document.body.innerHTML and checks exact output for lists, a nested list, select, controls, a GFM cell with a button, a hidden cell, base-URL resolution, and dialog/popover tabpanels, with and without strip. fetch_cli/markdown.rs:75-95 adds an HTTP base-URL CLI test. The CLI layout test now expects live output to equal the source fixture (the -computed.md fixture is deleted). Gaps: no live case where Stylo blockifies an element (N5) and no linked code block (N2). The layout CLI test still loads a data: URL (fetch_cli/markdown.rs:15).
7 Stylo pinned to an unmerged fork branch inside an unrelated commit NOT FIXED Cargo.toml:115-121 is still e2baa14a…, and the bump is still inside renderer commit 040269fd4. GitHub compare main...e2baa14 reports ahead_by: 38. The only branch with it at its head is codex/moli-style-provenance.
8 Several full-document passes per live conversion NOT FIXED visibility.rs:57-108 still makes separate passes for fragment links, ids, aria-controls, tabpanel/until-found (each with a content_context root walk, :287-313, which now also probes popover), the display:none set, and the disclosure-path root walks, before the main pass (:119-261). The only reduction is that style collection now starts at <body> (page_dump.rs:19-27).
9 may_have_fragment_links predicate differs from the prepass FIXED visibility.rs:57-62 now requires a non-empty fragment, the same as anchors.rs:47-52. It still scans suppressed nodes, but that can only cause a false positive on a "may" hint.
10 Inconsistent role, URL and label rules PARTIAL Fixed: the resolution inventory now matches what html2md reads (source no longer resolves srcset; media.rs:11-18). Open: there are still five role parsers. First token: content.rs:73-74. Whole value, untrimmed: content.rs:112. Trim plus lowercase: machine.rs:429-434, html_fallback.rs:66-71. HTML-space trim: table.rs:29-32. Any token: visibility.rs:329-333. So role="presentation none" still makes an image presentational but leaves a table as data.
11 Stack hygiene NOT FIXED (more churn) There are now four empty commits (8663d9a89, 48001fb55 in #1334; aa5fafed8, 6ba81c991 in #1335). 957d1be5b is still titled fix(ci) but only moves tests, and the 2757784a9/e9dccde1e pair still nets to zero. New: 040269fd4 (#1337) adds a blank line after a doc comment and a double blank line in html2md, and 92720683b removes them again. 68cc9e546 adds format!("{}\n", …) to the CLI assertion, and 99e374401 reverts it within the same PR.
12 Overlapping PRs / #1592 CHANGED See "#1592" below. It no longer applies to the stack.
N1 Renderer srcset selection depends on html2md's parser NOT FIXED (now part of html2md's public API) parse_srcset_candidates (images/src.rs:776-792) still wraps moli_html2md::parse_srcset. 2ff73d38e removes its #[doc(hidden)] (lib.rs:21) and adds public width/height/density fields (media.rs:83-90), so browser image selection is now a documented html2md API. Parentheses still nest (media.rs:133-135). Input: srcset="a.png ((x), b.png 2x" with src="fallback.png". Under the spec's non-nesting paren state, b.png is selected. Here the comma is swallowed, the single descriptor ((x), b.png 2x is rejected, and currentSrc becomes fallback.png. The new renderer test (images/src.rs:1079) covers only Unicode spaces.
N2 Computed boundaries rewrite linked code in the HTML fallback NOT FIXED html_fallback.rs:190-201 still applies both hooks whatever code is. <pre><code>let v = <span style="display:inline-block">f</span>(<a href="/doc">x</a>);</code></pre> still gives let v = f (<a href="/doc">x</a>); live, while the unlinked raw path (machine.rs:379-392) gives let v = f(x);. A display:block span inside such a pre still becomes <div>.
N3 Sanitized range/color defaults leak into the live dump NOT FIXED (now more visible) The adapter still sets control_value from input_value() for every HTML input (visibility.rs:141-146). form.rs:28-47 now drops aria-label/title for non-button inputs. For <input type="range" aria-label="Volume level"> <input type="color" title="Theme">, source output is now empty and live output is 50 #000000.
N4 Catch-all classifier puts <div> inside fallback tables NOT FIXED visibility.rs:212 still classifies UA table-column/table-column-group as blocks. colgroup/col are in neither has_block_tag (html_fallback.rs:500-530) nor owns_boundaries, so a fallback table with <colgroup><col><col></colgroup> still emits <div><div></div><div></div></div> inside <table>. Adding colgroup/col to owns_boundaries fixes it.

New findings

# Severity PR Location Summary
N5 High #1337 × #1336 (missed in rounds 2–3) visibility.rs:211-224; machine.rs:216-223; Stylo style_adjuster.rs:221-256 Stylo blockifies flex/grid items, floats and absolutely positioned elements. The adapter then reports them as blocks, which brings the round-2 #1 bug back for buttons, search bars, list rows and screen-reader text
N6 Low #1336 hook mapping × #1337 classifier machine.rs:220-223, html_fallback.rs:198-201, visibility.rs:219-220 A space for every inline-* element splits words that are rendered one character per span

N5. High: blockified computed display defeats this round's fix

computed_style_export_values_for_document_snapshot reads the computed display (styles.rs:554-580), and that value comes after Stylo's style adjustment. StyleAdjuster::blockify_if_necessary (stylo e2baa14 style/style_adjuster.rs:221-256) turns any child of a flex or grid container, any floated element, and any position:absolute|fixed element into its block equivalent: inline becomes block, inline-block becomes block, and inline-flex becomes flex. Browsers report the same value from getComputedStyle. The classifier (visibility.rs:211-224) then puts the element in blocks:

  • The native-control exemption only matches the literal string inline-block (:217).
  • owns_boundaries only covers structural tags.

So machine.rs:217-219 emits boundary(2), and Writer::boundary closes every open inline style (writer.rs:279). Four common patterns trigger this on the default moli fetch --dump markdown path:

  • Search bar in a flex form. Input: <form role="search" style="display:flex"><input name="q" value="rust"> <button>Search</button></form>. Both controls compute to block, and the exemption misses them. Live output is rust\n\nSearch. Source output is rust Search.
  • Tailwind or shadcn button with an icon. Input: <p>Click <button class="inline-flex"><svg></svg><span>Save</span></button> to continue.</p>. The button gets a space, but the <span> is a flex item, so it computes to block. Live output is Click\n\nSave\n\nto continue. Source output is Click Save to continue.
  • Screen-reader text inside a link. Bootstrap .visually-hidden and Tailwind .sr-only both use position:absolute. Input: <a href="/p/1">Read more<span class="sr-only"> about Rust 2.0</span></a>. Live output is [Read more](/p/1)\n\n[about Rust 2.0](/p/1), with the link split and the paragraph broken. Source output is [Read more about Rust 2.0](/p/1).
  • Flex list rows and cells.
    • Input: <li style="display:flex"><a href="/a">Title</a><span>3 comments</span></li>. Each child gets boundary(2) on the item writer, which sets has_blocks (writer.rs:277), so the whole list becomes loose (machine.rs:155).
    • Input: <td><div style="display:flex"><span>Alice</span><button>Edit</button></div></td>. Live output is Alice<br><br>Edit (table.rs:197-199), where the matching unwrapped test (markdown_dump.rs:481) expects Alice Edit.

These four cases also contradict the new README claim at README.md:168-169. The fix belongs in the adapter's classifier, because only it can see why Stylo blockified an element:

  • Treat blockification caused by float or absolute positioning as inline. Stylo keeps the pre-adjustment value in original_display (properties.mako.rs:1436-1444). An equivalent check is to read position/float together with display.
  • Treat an item of a row-direction flex container (the parent's style is already in the map; read flex-direction as well) as an inline boundary, meaning a space. Only a column-direction flex container or a grid container should produce a block.
  • Separately, have html2md own the control boundary by adding input/button/textarea to owns_boundaries, so it no longer depends on one serialized string.
  • Add the four inputs above to markdown_dump.rs:476-485.

N6. Low: an inline-block space splits per-character text

has_text_boundary now always inserts a literal space (machine.rs:220-223, html_fallback.rs:198-201) for any inline* display (visibility.rs:219-220). Text-animation libraries such as GSAP SplitText and Splitting.js wrap each character in display:inline-block, and so do odometer counters.

  • Input: <h1><span style="display:inline-block">H</span><span style="display:inline-block">i</span></h1>
  • Live: # H i
  • Source and browser: # Hi

Our round-3 advice to use one space in both paths was incomplete. An inline-level box does not create a word boundary, and only margins or padding make one visible.

Fix. Insert the separator only when the boundary would not land between two non-space characters of a single word, for example when the element's text is a single grapheme with no adjacent source whitespace. Alternatively, limit inline_boundaries to replaced elements and controls, and treat other inline-* elements as inline.

#1592

#1592 (head 86ed30b95) is still stacked on the round-3 head f04b0b151, plus one new commit. It does not apply to 92720683b. Replaying only its own delta (git merge-tree --merge-base f04b0b151 92720683b 86ed30b95) conflicts in two places:

When rebasing, apply the body-only collection to each frame document, so that every frame's <html> and <body> stay in document_roots. #1592 also inherits N2–N6 through the shared MarkdownDom.

Merge readiness

Automated review by Claude Code.

@lanyue-llk
lanyue-llk force-pushed the codex/content-rendered-state branch from 9272068 to 6d0a254 Compare October 22, 2026 08:46

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-5 re-review of the updated version (6d0a2549a)

Round-5 re-review (head 6d0a2549a, base #1336 39ae6b32e). This PR makes the renderer's Markdown export read live state (computed display/visibility/baseline-shift, Stylo own-visibility, live control values, base-resolved URLs) through the MarkdownDom adapter over moli-html2md. The new commit 6d0a2549a and the #1336 changes beneath it fix the hidden-table skeleton, collapsed rows, single-block list items, col/colgroup and the control special case. Each fix has an exact live test that fails on the round-4 code. I traced the code by hand and did not build or run anything.

Verdict: request changes. The top stack item, N5 (high), is not fixed. The classifier still reads only the computed display (page_dump.rs still requests just display/visibility/baseline-shift). Stylo blockifies flex/grid items, floats and abspos elements (stylo e2baa14 style_adjuster.rs:221-256), so they still produce paragraph breaks. N6 is also open, and the range/color fix still leaks defaults when value="".

Previous findings

# Finding Status Evidence at 6d0a2549a
R4-1 [minor] hidden fallback table emits an empty <table> skeleton; collapse rows become empty GFM rows FIXED A reverse pre-order pass (visibility.rs:266-283) suppresses an invisible table/ul/ol/dl that has no visible descendant. visibility:collapse tr/thead/tbody/tfoot is suppressed (L199-214). Exact tests L486-487, plus a restored-descendant test at L500-511. Residual nit: col/colgroup in that list do nothing (new #5).
R4-2 [minor] loose list from a block-styled item child; README claim FIXED for the reported case #1336 adds Writer::layout_boundary, which does not set has_blocks (writer.rs:276-283). flush_breaks sets it only when a break of 2 or more lands between content (writer.rs:504). Exact test L485. The README now claims only the single-block case (L119-121). Two flex items in an item are still loose (N5).
R4-3 / stack N3 [low] range/color sanitized defaults PARTIAL Absent value is fixed and tested (L488-489, L505). value="" or an invalid authored value still prints 50/#000000 (new #3).
R4-4 / stack N4 [low] col/colgroup become <div> FIXED (in #1336) owns_boundaries now includes col/colgroup (#1336 machine.rs). Exact test L490.
R4-6 [nit] control inline-block exemption FIXED Branch deleted (L221-228). Controls get a collapsible space, and the existing exact control and cell tests are unchanged.
R4-7 [nit] README wrap/placement MOSTLY FIXED Policy is under ## Content policy (L107) and the duplicate is gone. L20 and L113 are rewrapped. L6 is now 94 cols (new #6).
stack N2 [medium] CSS boundaries inside fallback <pre>/<code> FIXED (in #1336) html_fallback.rs now guards both hooks with !code.
stack N5 [high] blockified flex/grid items, floats, abspos NOT FIXED See new #1.
stack N6 [low] per-character inline-block spans split NOT FIXED See new #2.
R2-1 [minor] Stylo pin on unmerged fork PR merge-order item, unchanged lexmount/stylo#2 is still OPEN at e2baa14.
R3-9 / stack #8 full passes per conversion pushback accepted This commit adds one more pass plus a node_order Vec. Still acceptable.
R2-S6 [nit] minified tests NOT FIXED New lines reach 237 cols (L490) and 329 cols (L505).
R2-N1 / stack #5/#7/#11 commit hygiene; html2md API and Stylo bump in the renderer commit NOT FIXED (moot if squashed) Commits 1-8 are unchanged (range-diff =). 6d0a2549a is titled fix(renderer) but mostly rewraps html2md README.
R2-N2 PR body stack line omits #1592 NOT FIXED It still ends at #1380.

New / still-open findings

  1. [high] N5 still open: blockified display turns flex children, floats and .sr-only/abspos spans into paragraph breaks (visibility.rs:222).
  2. [low] N6 still open: every inline-* box gets a space, so per-character spans split words (visibility.rs:223).
  3. [low] range/color: value="" or an invalid authored value still leaks the synthetic default. The type is re-parsed from the attribute (visibility.rs:142).
  4. [low] The new range/color and hidden-structure test uses contains assertions, not exact output (markdown_dump.rs:505).
  5. [nit] col/colgroup in collapsed_table_part are no-ops. Collapsed columns still export their cells, though the author reply says columns are removed (visibility.rs:202).
  6. [nit] README L6 is 94 cols. "wholly invisible tables" also covers lists (README.md:6, :120).

Other

  • #1592 is not rebased. Its head is still 86ed30b95 on the round-3 base f04b0b151. git merge-tree --merge-base f04b0b151 6d0a2549a 86ed30b95 conflicts in page_dump.rs and page_dump/visibility.rs. When it is rebased, it must apply body-only collection per frame document and run the new node_order reverse pass over each frame's styles.
  • Merge state: clean against #1336 (39ae6b32e) and against origin/main.
  • CI: Format, Clippy, CDP smoke, harness, SecTrust and Windows storage pass. Nextest and both release builds are QUEUED, so none of the new exact tests have run in CI.

Verdict: changes requested (posted as a comment because GitHub does not allow requesting changes on your own PR).

Automated review by Claude Code.

Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs Outdated
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs Outdated
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs Outdated
Comment thread moli-renderer-v8/src/runtime/page_vm/tests/markdown_dump.rs Outdated
Comment thread moli-renderer-v8/src/runtime/page_dump/visibility.rs Outdated
Comment thread moli-html2md/README.md Outdated
@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Stack-level re-review (#1334–#1337), round 5

Scope. This round covers only cross-layer and combined-state problems. Refs: merge-base bd8e0b27a ← #1334 97ff3aeae ← #1335 396960e42 ← #1336 39ae6b32e ← #1337 6d0a2549a. These match the current PR heads on GitHub. Unless marked otherwise, every location is file:line at 6d0a2549a. I produced this by reading the code, not by running it. I built nothing and ran no tests. Each output below comes from tracing the named code path by hand. The per-commit buildability section relies on the Stylo sources in the local cargo checkout (973988b, e2baa14) and on symbol searches, not on a compiler.

Changes since round 4. Four new commits: 97ff3aeae in #1334, 396960e42 in #1335, 39ae6b32e in #1336 and 6d0a2549a in #1337. Everything above 97ff3aeae was replayed with identical patches, except 404a9f6a1 and d199e9eb5…eb3378657, which changed. Nobody has replied on #1334–#1337 since round 4. All comments there come from the review account or github-actions. git merge-tree origin/main 6d0a2549a (main 14d3c08cc) is clean.

Status of the round-4 findings

# Finding Status Evidence at 6d0a2549a / remaining input → wrong output
1 (residual) One computed block child makes a tight list loose FIXED Writer::layout_boundary (writer.rs:276-283) no longer sets has_blocks. flush_breaks sets it only when a blank line actually lands between emitted content (writer.rs:504). <ul><li><a style=display:block>A</a></li>… now gives - [A](…)\n- [B](…). Live tests: markdown_dump.rs:485, review_semantics.rs "computed_boundaries_supplement_structure…".
4 Hooks without producers; README out of date PARTIAL (unchanged) #1337's README is now accurate (README.md:5-31). #1334, #1335 and #1336 still ship "implements the interface directly", "four borrowed queries" and "never rescan cell descendants" (README.md:5,19,40 at 97ff3aeae, 396960e42 and 39ae6b32e), while dom.rs:25-55 at 39ae6b32e has eight methods. The corrected README arrives through a 94-line html2md README rewrite inside the renderer commit 6d0a2549a.
5 Changes filed in the wrong layer NOT FIXED (one new instance) The new commit 97ff3aeae is titled fix(renderer), but 6 of its 7 files are html2md. It changes image_label and poster semantics and the public parse_srcset, and the renderer side is one 7-line test (images/src.rs:1078-1083). 6d0a2549a (renderer) rewrites the html2md README. resource_url_attributes still enters html2md's public API through the renderer commit 9d9933e9e (media.rs:8-28, lib.rs).
7 Stylo pinned to an unmerged fork branch inside an unrelated commit NOT FIXED Cargo.toml:115-121 is still e2baa14a…, and the bump is still inside 9d9933e9e. The only branch with that commit at its head is still codex/moli-style-provenance. The pin now also breaks the build of earlier commits (see B1).
8 Several full-document passes per live conversion NOT FIXED (one more pass) 6d0a2549a adds a node_order copy of every styled node (visibility.rs:119) and a reverse pass with another HashSet (:266-283), on top of the passes listed in round 4.
10 Inconsistent role, URL and label rules PARTIAL Improved: three table-role sites now share the HTML-whitespace trim (machine.rs:432-436, html_fallback.rs:66-70, table.rs:29-31), and image_label/fallback_text share excluded_from_naming (content.rs:77-87). Four rules remain: first token (content.rs:68-69), exact untrimmed (content.rs:115-116), trimmed whole value (the three table sites) and any token (visibility.rs:351-355). role="presentation none" still makes an <img aria-label=X> presentational (label dropped) while the same value on a table leaves it a data table. role=" button" is not a button in content.rs:116 but would match under any other rule.
11 Stack hygiene NOT FIXED There are still four empty commits: 8663d9a89, 48001fb55 (#1334) and 2f69186f5, 0abaab870 (#1335; rebased copies of aa5fafed8/6ba81c991). cfa2e9917 is still titled fix(ci) but only moves tests. The 2757784a9/e9dccde1e pair is unchanged. New churn: 97ff3aeae switches the inline poster lookup in machine.rs to direct_attribute, and 39ae6b32e applies the same change again to media::poster after #1336 moved the code. 39ae6b32e reflows README paragraphs, and 6d0a2549a reflows them again. Two commit ranges do not build on their own (B1).
N1 Renderer srcset selection depends on html2md's parser FIXED (behavior); dependency remains parse_srcset now uses a non-nesting in_parentheses flag (media.rs:130-138), which matches the spec's paren state. a.png ((x), b.png 2x now yields only b.png, asserted on the renderer side (images/src.rs:1078-1083) and in html2md. The renderer still selects images through html2md's public parser. That is acceptable now that both sides pin the behavior with tests, but it should be stated as a deliberate API.
N2 Computed boundaries rewrite code in the HTML fallback FIXED (one low residual, B4) html_fallback.rs:194-205 now gates both hooks on !code. <pre><code>let v = <span style=display:inline-block>f</span>(<a href=/doc>x</a>);</code></pre> gives let v = f(<a href="/doc">x</a>); with no <div>, for both inline-block and block (test in review_semantics.rs).
N3 Sanitized range/color defaults leak into the live dump PARTIAL visibility.rs:142-145 skips the override only when the value attribute is absent. An empty or invalid attribute still exposes the sanitized default. <input type=range value=""> and <input type=color value=""> give 50 #000000 live and nothing from source (form.rs drops empty values). <input type=color value="red"> gives #000000 live and red from source. Fix: like textarea, override value only when input_value_dirty() is set (or when the sanitized value equals the attribute). A clean input's value is just its sanitized default attribute.
N4 colgroup/col become <div> inside fallback tables FIXED col/colgroup are now in owns_boundaries (machine.rs:828-830). Live exact test at markdown_dump.rs:490.
N5 Blockified computed display (flex/grid items, floats, absolute positioning) NOT FIXED The classifier is unchanged apart from dropping the native-control exemption (visibility.rs:222-228), and nothing reads position, float or the parent's display/flex-direction. layout_boundary stops a single block child from loosening a list, but every round-4 input still splits. <form style=display:flex><input value=rust> <button>Search</button></form> gives rust\n\nSearch (source: rust Search). <a href=/p/1>Read more<span class=sr-only> about Rust 2.0</span></a> gives [Read more](/p/1)\n\n[about Rust 2.0](/p/1). <li style=display:flex><a href=/a>Title</a><span>3 comments</span></li> still gives a loose list: the second block child's breaks flush onto content (writer.rs:504). <td><div style=display:flex><span>Alice</span><button>Edit</button></div></td> gives Alice<br><br>Edit. None of these cases has a live test. The fix is the same as in round 4.
N6 An inline-block space splits per-character text NOT FIXED (now asserted) There is no reply, and review_semantics.rs now asserts <span style=display:inline-block>A</span><span style=display:inline-block>B</span> → A B. <h1> with SplitText-style per-letter spans still gives # H i live and # Hi from source and in the browser. Removing the control exemption makes the space rule apply to more elements (B3).
12 #1592 NOT APPLICABLE See the #1592 section below.

New findings

# Severity PR Location Summary
B1 High (bisectability, missed in earlier rounds) #1337, #1336 style_engine/computed.rs:309-313 at 3de6dee7f; html_table.rs:245 at d6144d075 Nine commits do not compile on their own
B2 Medium (new this round) #1337 visibility.rs:199-213 Suppressing visibility:collapse rows moves rowspan cells into the wrong row
B3 Low (new this round) #1337 visibility.rs:199-213, :183-193 The collapse rule ignores disclosure veils and own_visibility, so tables inside retained panels lose their rows
B4 Low (new this round) #1336 html_fallback.rs:192-205 vs machine.rs:219-226 The code gate also covers the <code> element itself, so the two output paths treat a styled <code> differently
B5 Info #1337 visibility.rs:199-203 The col/colgroup entries in the collapse rule do nothing; collapsed columns still emit their cells

B1. High: nine commits do not compile on their own

  1. Stylo flag used before the pin bump (fix(renderer): export live rendered content through shared markdown conversion #1337). 3de6dee7f, 6959b3731, 64c3cc6b5 and 78fdd3545 call ComputedValueFlags::HAS_OWN_VISIBILITY (moli-renderer-v8/src/style_engine/computed.rs:312 at 3de6dee7f, imported from Stylo at :9). Cargo.toml and Cargo.lock still pin Stylo 973988b9c… in those commits, and 973988b/style/properties/computed_value_flags.rs has no such constant. It first appears at e2baa14 (computed_value_flags.rs:148). The pin moves only in 9d9933e9e, the fifth fix(renderer): export live rendered content through shared markdown conversion #1337 commit.
  2. Call to a deleted helper (fix(html2md): retain complex tables and referenced fragments #1336). d6144d075, 493582327, 7aa8ad9ad, d199e9eb5 and 499b5f96a call crate::content::has_readable_content (moli-html2md/src/html_table.rs:245). That function was added in 8432da1f5 and deleted in e9dccde1e, both in fix(html2md): preserve media resources and readable controls #1334. Nothing in the stack defines it again. The call goes away only in 60063abd1.

Both problems were already present in round 4 (90a2e5992 and 5dbb582f7 carried the same patches), and earlier rounds missed them. The PR heads themselves look consistent. Fix: move the Stylo bump into 3de6dee7f, or into a commit before it, ideally after the fork change has merged (#7). Fold the html_table.rs:245 change from 60063abd1 into d6144d075. Otherwise, require squash-merge for #1336 and #1337. A rebase merge would put commits on main that do not build.

Other per-commit checks found no further breaks. I resolved crate::module::item paths, Task::* variants, writer() methods, the arity of the math::spans tuple, ExportStyle fields and moli_html2md::* re-exports at every commit. This was a symbol search, not a compiler.

B2. Medium: collapsed rows shift rowspan cells (new in 6d0a2549a)

collapsed_table_part now puts a visibility:collapse tr/thead/tbody/tfoot into suppressed (visibility.rs:199-213), which removes it from html2md's view entirely. In round 4 the row stayed in place as an empty row. CSS 2 §17.5.5 and browsers handle collapse differently: the row's space is removed, but the table grid does not change, and a rowspan that reaches into the collapsed row is clipped. It does not extend into the next row.

  • Input: <table><tr><th>K</th><th>V</th></tr><tr><td rowspan=2>A</td><td>B</td></tr><tr style="visibility:collapse"><td>C</td></tr><tr><td>X</td><td>Y</td></tr></table>
  • Browser: X under K and Y under V.
  • Live dump: html2md sees three rows. A's rowspan=2 now covers the X row, so the HTML fallback gives <tr><td rowspan="2">A</td><td>B</td></tr><tr><td>X</td><td>Y</td></tr>. A reader sees X under V, and Y falls into a third column the header does not have.
  • Source path: correct.

This is the same class of misplaced cells as round-2 #2. display:none rows are a different case, because they really do leave the grid in browsers, so their suppression is correct.

Fix: handle collapsed rows the way hidden cells are handled. Mark them invisible (keep the elements, drop their text) instead of suppressed, or suppress them only when no rowspan from an earlier row reaches into them.

B3. Low: the collapse rule overrides disclosure veils (new in 6d0a2549a)

visibility is inherited, and collapsed_table_part reads the computed value without checking own_visibility, disclosure_root, disclosure_path or visibility_mode (visibility.rs:199-213). The display:none branch beside it does exempt disclosure roots and paths.

  • Input: <button aria-expanded=false aria-controls=spec>Specs</button><section id=spec style="visibility:collapse"><p>Dimensions</p><table><tr><th>Weight</th><td>2 kg</td></tr></table></section>. Tailwind's collapse utility is visibility: collapse.
  • Expected: the section is a disclosure root, so it gets DisclosureVeil (:183). Its <p> inherits the veil and is kept (:189-193).
  • Actual: the parser-generated <tbody> inherits collapse and is suppressed. The live dump keeps Dimensions but drops the spec table, while the source dump keeps both.

Fix: apply the rule only when values.own_visibility is set and the node is not under a disclosure veil. That is the same condition the VisibilityMode code at :187-193 already uses.

B4. Low: styled <code> is treated differently by the two output paths (new in 39ae6b32e)

html_fallback.rs:192 updates code before checking the hooks, so a <code> element skips its own has_block_layout/has_text_boundary. Machine::visit (machine.rs:219-226) applies both hooks to the same element. In a live dump, with code { display:inline-block } or display:block (a common blog style):

  • Input: a<code>x</code>b.
  • Markdown path: a `x` b for inline-block, or a separate paragraph for block.
  • HTML fallback (for example inside a spanned table cell): a<code>x</code>b, with no separation.

Fix: use the parent's code flag for the hook checks, and update code after them.

B5. Info: collapsed columns

col/colgroup in collapsed_table_part (visibility.rs:202) have no content, so suppressing them changes nothing. Cells are not children of col, so they do not inherit its visibility. <col style="visibility:collapse"> therefore leaves that column's cells in the live dump, while the browser hides them. The README claims only that collapsed rows are omitted, so this is a known limit, not a contradiction. Either drop the two tags from the match or implement column collapse.

Checked this round, no cross-layer problem found

  • Native-control exemption removed (6d0a2549a). UA inline-block controls now get collapsible spaces on both paths. select/option still own their boundaries. Code is gated on both paths. Outside N5 and N6, this adds only separation that the source path also produces.
  • The visible_subtrees pass (visibility.rs:266-283). The reverse pre-order keeps a table or list whose descendant restores visibility, including disclosure roots, and drops wholly invisible ones. It matches the new tests at markdown_dump.rs:486,501-510.
  • flush_breaks sets has_blocks (writer.rs:504). On the source path this changes only items that already contain blank-line-separated blocks (for example <li>a<ul><li>b</li></ul>c</li>), which CommonMark already parses as loose.
  • Math changes in fix(html2md): preserve mathematical and inline formatting semantics #1335 (396960e42: <!-- --> between adjacent spans, {} after a trailing control space). These are html2md only and do not touch the hooks or the adapter.

#1592

#1592 is unchanged (head 86ed30b95). It is still stacked on the round-3 head f04b0b151 and does not apply to 6d0a2549a. git merge-tree --merge-base f04b0b151 6d0a2549a 86ed30b95 conflicts in the same two hunks as in round 4:

  • page_dump.rs:26-46: body-only collection versus collection of every frame document with per-document bases.
  • visibility.rs:259-265: the base_url parameter and tag versus document_bases and names.

When rebasing, apply the <html>-plus-<body> collection to each frame document. Also note that document_visibility_is_hidden (visibility.rs:53-56) and the new visible_subtrees pass look only at the top document's <html>/<body>, so a frame's root visibility veil will not be recognised. #1592 also inherits N3, N5, N6 and B2–B4 through the shared MarkdownDom.

Merge readiness

Automated review by Claude Code.

@lanyue-llk
lanyue-llk force-pushed the codex/content-rendered-state branch from 6d0a254 to 4e58ef8 Compare October 22, 2026 10:41

@lanyue-llk lanyue-llk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final re-review (P0/P1 only) (4e58ef889)

Round-6 (final) re-review. Head is 4e58ef889 and base is #1336 5df2dd7f7. I traced the code by hand and did not build or run anything.

Verdict: request changes for one remaining P0, and only if this PR is rebase-merged. The new commit 4e58ef889 fixes N5 for all five reported inputs, and each has an exact live test. Column-flex and grid children still become blocks, as they should. N3 and N6 are fixed too. B1 has not changed: four commits do not compile on their own. The repository allows rebase merge (allow_rebase_merge: true), so if this PR is squash-merged, B1 does not matter and nothing at P0/P1 remains.

Previous P0/P1-level findings

# Finding Status Evidence at 4e58ef889
N5 [high] Stylo-blockified flex/grid items, floats and abspos elements produce paragraph breaks FIXED page_dump.rs:36-44 now also reads position, float and flex-direction, plus Stylo's original_display (computed.rs:288-290). visibility.rs:126-137 builds item_blocks from the parent's display: row flex/inline-flex and inline-grid children get a space boundary; column flex and grid children become blocks. Abspos/fixed elements, and floats that are not flex items, add no boundary (:244-271). Because set_adjusted_display(.., false) keeps original_display for floats and abspos (Stylo properties.mako.rs:1436-1446), a non-item inline-flex button keeps its inline outer type. Structural tags (div, li, td, form…) still own their boundaries (machine.rs:222, owns_boundaries), so floated or abspos divs are still paragraphs. Exact live tests at markdown_dump.rs:491-506 cover the search form (rust Search), inline-flex button, abspos .sr-only, flex li (tight list), flex cell (Alice Edit), floats, column flex (A\n\nB), grid and inline-grid. A CSSOM/DOM invariance test is at :536-547.
N6 [low, user-visible] per-character inline-block spans split words FIXED Plain atomic inlines get no space now. Only input/textarea/select/button and inline list-items do (visibility.rs:259-268). <h1> with per-letter spans gives # Hi (test :498).
N3 range/color sanitized defaults leak when value="" or the value is invalid FIXED Range/color use the live value only when dirty (visibility.rs:161-163, using element.input_type()). Tests: value='' gives Volume and invalid values give abc invalid (:500-501). A script-set value is asserted exactly (:519-532).
B1 [P0 if rebase-merged] #1337 commits call HAS_OWN_VISIBILITY before the Stylo pin bump NOT FIXED Commits 1-4 are unchanged (range-diff =). 5f4a1e855, f6342b5d8, aa9b30ced and 60a926f2e use ComputedValueFlags::HAS_OWN_VISIBILITY (computed.rs:312 at 5f4a1e855), while Cargo.toml/Cargo.lock pin Stylo 973988b9, which has no such constant. The bump arrives only in cc4aa24e1. Squash-merge, or move the bump into the first commit.
B2 collapsed rows are removed, so a rowspan shifts cells NOT FIXED (P2 for this round) visibility.rs:217-223 still suppresses a visibility:collapse tr/thead/tbody/tfoot. The author says this is intended. It needs a collapsed row with a rowspan from an earlier row reaching into it, which is rare on real pages, so I do not block on it.
R2-1 Stylo pinned to an unmerged fork PR merge-order item, unchanged lexmount/stylo#2 is still OPEN at e2baa14.

New P0/P1 findings

No new P0/P1 issues in 4e58ef889 or 85969dc48. Residual edge cases, below P1 and not blocking:

  • The flex-item lookup uses the DOM parent, while Stylo uses the layout parent. A display:contents wrapper between a row-flex container and its items still gives paragraph breaks, because original_display is blockified for items.
  • An abspos inline badge with no source whitespace now glues to its neighbour, for example Cart<span style=position:absolute>3</span> → Cart3.

Other

  • #1592 is rebased onto 4e58ef889 (head 8a423e752, merge-base = this head). git merge-tree is clean, and it now collects body-only styles per frame document.
  • CI: Format, Clippy (so the head compiles), CDP smoke, Node/Python harness, SecTrust and Windows storage pass. Nextest and both release builds are still pending, so the new exact tests have not run in CI yet.

Verdict: changes requested (posted as a comment because GitHub does not allow requesting changes on your own PR).

Automated review by Claude Code.

pub(crate) fn has_own_visibility_value(&self) -> bool {
self.primary
.flags
.contains(ComputedValueFlags::HAS_OWN_VISIBILITY)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P0 if rebase-merged] Still open (stack B1): the first four commits of this PR do not compile. 5f4a1e855, f6342b5d8, aa9b30ced and 60a926f2e reference ComputedValueFlags::HAS_OWN_VISIBILITY (here; computed.rs:312 at 5f4a1e855). In those commits Cargo.toml/Cargo.lock still pin Stylo 973988b9c…, whose computed_value_flags.rs has no such constant. It first appears at e2baa14, and the pin moves only in cc4aa24e1. The repo has allow_rebase_merge: true, so a rebase merge would put four non-building commits on main. Fix: squash-merge, or move the Cargo.toml/Cargo.lock Stylo bump into 5f4a1e855 (ideally after lexmount/stylo#2 lands).

@lanyue-llk
lanyue-llk force-pushed the codex/content-rendered-state branch from 4e58ef8 to 19bfc78 Compare October 22, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant