Skip to content

fix(html2md): preserve mathematical and inline formatting semantics - #1335

Open
lanyue-llk wants to merge 4 commits into
codex/content-media-formsfrom
codex/content-math-formatting
Open

lanyue-llk wants to merge 4 commits into
codex/content-media-formsfrom
codex/content-math-formatting

Conversation

@lanyue-llk

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

Copy link
Copy Markdown
Contributor

问题与方案

原转换会把 TeX 的反斜杠、下标和矩阵分隔符当成普通 Markdown 转义;公式跨越无格式的内联包装时还会被拆断。上下标、MathML 结构和部分嵌套格式也会丢失语义。

本 PR 在真正的格式边界处处理连续文字,保留完整 TeX;货币、未闭合公式和代码仍按各自语义处理。MathML 保留结构,只输出一份语义公式,排除其视觉副本。上下标使用对应 HTML 元素。代码文字排除嵌入的复制按钮及脚本,图片标签和链接标题的换行不再意外变成 Markdown 标题。

根因、方案与验收

问题 根因 方案依据 泛化验收
公式命令和下标被改变 普通 Markdown 转义先于公式识别;逐文本节点识别割裂同一公式 在真实格式边界处处理连续作者文字,保留有效 TeX 透明包装与注释、标题/段落/列表/表格;货币、未闭合公式、文字链接伪装与代码的反向控制
数学结构丢失或重复 把 MathML 展平成文字,同时输出语义与视觉副本 保留数学树和上下标,只输出一份语义表达 分式、根式、多层上下标、替代编码、结构属性及需要转义的文字
Markdown 意外产生标题或错误格式 属性换行进入块语法;嵌套格式定界符闭合有歧义 保护属性值,在必要位置用 HTML 保留格式 多行图片标签及链接标题、段落末尾的嵌套样式、词内格式、纯代码与复制按钮

本组基于 #1334,作为原生 stack 的第二层。公式与格式修复有独立的场景测试;本 PR 的 diff 只包含这组增量。

修复机制图

连续文字流与真实语义边界

flowchart LR
  A[连续文字与透明内联包装] --> B[收集同一文字流]
  B --> C{完整且有效的公式边界?}
  C -->|是| D[保留 TeX 语义]
  C -->|否| E[普通 Markdown 转义]
  F[MathML 与视觉副本] --> G[保留唯一语义结构]
  D --> H[Markdown 输出]
  E --> H
  G --> H
Loading

范围与依赖

这是 #919 的公式与格式部分,基于 #1334 的媒体与表单规则。#1336 的复杂表格复用这两组规则,#1337 提供运行时 CSS 适配。

验证

  • cargo fmt --all --check:通过。
  • cargo test -p moli-html2md --locked:独立基线的 314 个场景通过。
  • cargo clippy -p moli-html2md --all-targets --locked -- -D warnings:通过。
  • 原生 DOM 导出专项:独立基线的 21 个场景通过,包括属性换行。
  • PR 集成 CI 对应调整依赖前的提交 8928f56ab92b550b7cbfcb0048ff1781ac5c1cfc,集成快照已包含 main 的 gamepad 与 credential 测试修复。
  • 红绿验证:科学上下标在旧转换中退化为普通文字,本修改保留 <sup> / <sub>。

场景覆盖透明包装中的公式、段落/标题/列表/普通表格、货币与代码、嵌套上下标、MathML 分式和根式、重复视觉表达、恶意文字与嵌套格式。完整仓库测试交由 CI。

与 #1305 的关系

#1305 直接基于 main,负责执行调用者明确指定的脚本,等待 Promise 和导航后走标准导出路径。这四组修复负责导出内容的完整性与语义。双方没有代码前置依赖;组合后,交互后的 DOM 会使用这些共享转换规则。

默认只读采集和明确交互采集是不同的测量协议。报告中的历史成绩仍绑定原调用协议;不能给单一方法补交互后替换同一行成绩。

报告

完整 WebMainBench 报告 保留 #919 已发布的运行,源码为 721bd2e329f96c06573db6e6b257a149cf17a6fe;该测量不对应本拆分分支。

最新 main 的 gamepad 测试编译问题已由独立的 #1343 修复并合入。该测试文件不在本组差异中。

PR stack

GitHub 原生依赖链:#1334 → #1335 → #1336 → #1337 → #1380 → #1428 → #1431 → #1432。

每个 PR 的 GitHub base 都是左侧紧邻分支,因此 Files changed 只显示本层增量。按从左到右的顺序合入。#919 是保留的历史完整组合,不是这条链的依赖;#1429 独立面向 main,只提供通用 CI 评测入口。

@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: 189 / 240 rows; 39 / 48 sites produced rows; 9 unexpected empty sites.

✅ Deterministic fixture contract is clean.

Common ancestor 7c4e73234642 → HEAD cd0193d92881; benchmark order: head-first; workflow: success.

Public 48-site run · informational

Metric Base HEAD Δ
Run status success success —
Extracted rows / contract 184 / 240 189 / 240 +5 (+2.72%)
Contract fill 76.67% 78.75% +2.08 pp (+2.71%)
Sites with rows 38 / 48 39 / 48 +1 (+2.63%)
Sites meeting row contract 36 / 48 37 / 48 +1 (+2.78%)
Unexpected empty sites 10 9 -1 (-10.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: 37; HTTP error + rows: 2; empty: 6; HTTP error + empty: 1; snapshot error + empty: 2 —
Observed duration 52.37 s 72.84 s +20.47 s (+39.09%)
Estimated CPU 51.14 s 58.16 s +7.03 s (+13.74%)
Peak PSS 285.80 MiB 291.06 MiB +5.27 MiB (+1.84%)
Peak RSS 288.90 MiB 294.77 MiB +5.86 MiB (+2.03%)
Peak threads 27 25 -2 (-7.41%)
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 32 / 40 +5
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.564, 2.566, 3.067, 4.569, 6.069, 7.572, 9.073, 10.075, 11.577, 13.079, 14.581, 16.084, 17.584, 18.585, 19.086, 20.588, 22.09, 23.591, 25.093, 26.595, 27.597, 29.098, 30.599, 31.601, 32.102, 33.604, 35.106, 36.607, 38.108, 39.609, 41.11, 42.612, 44.114, 45.115, 46.616, 48.118, 49.62, 51.122]
    y-axis "CPU percent" 0 --> 500
    line [25.96, 119.78, 127.78, 115.85, 191.97, 159.56, 91.96, 147.65, 155.82, 113.89, 135.65, 119.87, 8, 0, 3.99, 0, 1.99, 115.86, 15.99, 55.95, 125.84, 123.82, 139.85, 107.79, 107.79, 133.85, 29.96, 127.84, 143.88, 95.92, 5.99, 117.89, 91.89, 13.99, 22, 129.96, 25.96, 101.82]
Loading
xychart-beta
    title "Base memory: RSS then PSS"
    x-axis "Elapsed seconds" [0.062, 1.564, 2.566, 3.067, 4.569, 6.069, 7.572, 9.073, 10.075, 11.577, 13.079, 14.581, 16.084, 17.584, 18.585, 19.086, 20.588, 22.09, 23.591, 25.093, 26.595, 27.597, 29.098, 30.599, 31.601, 32.102, 33.604, 35.106, 36.607, 38.108, 39.609, 41.11, 42.612, 44.114, 45.115, 46.616, 48.118, 49.62, 51.122, 52.367]
    y-axis "Memory MiB" 0 --> 500
    line [12.44, 82.72, 118.51, 134.78, 130.39, 166.22, 171.58, 179.73, 182.87, 166.68, 187.49, 201.75, 164.07, 151.59, 147.31, 147.7, 147.59, 148.57, 177.2, 143.65, 153.55, 181.42, 164.26, 174.8, 288.9, 235.82, 207.65, 168.54, 182.72, 179.8, 149.19, 154.55, 243.81, 275.88, 217.42, 182.84, 208.5, 156.93, 194.16, 0]
    line [9.18, 79.11, 115.05, 131.32, 126.75, 162.44, 168.2, 175.99, 179.32, 162.98, 183.95, 196.06, 160.53, 148.08, 143.7, 144.1, 143.98, 144.97, 173.71, 140.09, 150.05, 178.08, 157.4, 171.34, 285.8, 231.91, 204.14, 165.03, 179.21, 176.32, 143.62, 151.04, 240.48, 272.44, 213.91, 178.92, 204.81, 153.31, 190.54, 0]
Loading

HEAD

xychart-beta
    title "HEAD CPU"
    x-axis "Elapsed seconds" [2.045, 4.049, 5.551, 7.553, 9.554, 11.557, 13.06, 15.062, 17.066, 19.07, 20.571, 21.572, 22.573, 24.576, 26.578, 28.081, 30.082, 31.084, 32.084, 34.088, 35.589, 37.592, 39.593, 41.596, 43.156, 45.16, 47.161, 54.794, 56.293, 58.297, 60.299, 62.301, 63.803, 64.804, 65.806, 67.806, 69.808, 71.311]
    y-axis "CPU percent" 0 --> 500
    line [133.71, 121.91, 143.86, 165.79, 157.9, 149.78, 9.97, 155.8, 47.88, 155.55, 57.98, 0, 2, 0, 101.91, 85.87, 155.83, 103.87, 109.86, 157.59, 161.87, 145.92, 87.99, 57.9, 116.14, 79.81, 31.95, 99.85, 32.01, 35.93, 40, 7.99, 5.99, 203.81, 75.92, 0, 0, 25.97]
Loading
xychart-beta
    title "HEAD memory: RSS then PSS"
    x-axis "Elapsed seconds" [0.042, 2.045, 4.049, 5.551, 7.553, 9.554, 11.557, 13.06, 15.062, 17.066, 19.07, 20.571, 21.572, 22.573, 24.576, 26.578, 28.081, 30.082, 31.084, 32.084, 34.088, 35.589, 37.592, 39.593, 41.596, 43.156, 45.16, 47.161, 54.794, 56.293, 58.297, 60.299, 62.301, 63.803, 64.804, 65.806, 67.806, 69.808, 71.311, 72.839]
    y-axis "Memory MiB" 0 --> 500
    line [12.41, 124.45, 148.25, 145.15, 191.13, 177.18, 168.69, 156.55, 171.15, 143.77, 165.37, 152.99, 147.96, 146.67, 146.71, 155.32, 145.17, 174.79, 294.77, 215.98, 159.66, 166.59, 176.77, 166.94, 163.68, 185.17, 168.63, 155.39, 192.45, 179.88, 181.64, 182.19, 139.65, 144.34, 163.17, 149.91, 157.34, 153.93, 153.95, 0]
    line [9.2, 121, 144.97, 141.68, 187.68, 173.91, 165.23, 152.89, 167.5, 140.08, 161.68, 149.3, 144.27, 142.98, 143.02, 151.7, 141.92, 171.1, 291.06, 212.28, 156.06, 163.02, 172.84, 163.33, 159.84, 181.3, 164.83, 151.53, 188.59, 176.02, 177.83, 178.35, 135.83, 140.52, 161.86, 146.25, 153.67, 150.25, 150.37, 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.96 s 19.88 s -0.07 s (-0.37%)
Estimated CPU 0.53 s 0.49 s -0.04 s (-7.88%)
Peak PSS 66.30 MiB 65.89 MiB -0.40 MiB (-0.61%)
Peak RSS 69.78 MiB 69.24 MiB -0.54 MiB (-0.77%)
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.011718%
Frontend differential ✅ 1,020/1,020 cases matched; 0 issues; 1 Chromium reference recoveries
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.011718%

Package and image size

Metric Base HEAD Delta Delta %
Raw binary 148.83 MiB 148.85 MiB +17.86 KiB +0.011718%
Stripped binary 102.48 MiB 102.50 MiB +16.00 KiB +0.015246%
gzip binary 51.06 MiB 51.07 MiB +12.91 KiB +0.024694%
Rootfs 155.73 MiB 155.75 MiB +20.00 KiB +0.012542%
gzip rootfs 59.95 MiB 59.96 MiB +4.63 KiB +0.007540%

Startup latency and PSS

Case Base p50 HEAD p50 Delta Base PSS HEAD PSS Delta
Serve ready 27.32 ms 27.23 ms −0.09 ms 3.74 MiB 3.77 MiB +27.50 KiB
CDP first page 50.21 ms 53.34 ms +3.13 ms 4.47 MiB 4.14 MiB −342.50 KiB
CDP warm page 11.28 ms 12.56 ms +1.28 ms 49.55 MiB 48.94 MiB −634.50 KiB
CLI about:blank 15.32 ms 16.81 ms +1.49 ms 2.59 MiB 2.56 MiB −27.50 KiB
CLI local JS 19.66 ms 20.20 ms +0.54 ms 2.30 MiB 2.99 MiB +707.50 KiB

Concurrency matrix

Concurrency Base p50 HEAD p50 Delta Base PSS HEAD PSS Delta HEAD failures Unstable B/H
1 18.91 ms 18.33 ms −0.58 ms 607.00 KiB 677.00 KiB +70.00 KiB 0 0/0
5 22.38 ms 21.42 ms −0.97 ms 27.46 MiB 27.41 MiB −57.00 KiB 0 0/0
10 22.04 ms 21.43 ms −0.60 ms 13.61 MiB 27.80 MiB +14.19 MiB 0 0/0
25 21.86 ms 21.46 ms −0.40 ms 27.52 MiB 13.15 MiB −14.37 MiB 0 0/0
100 21.97 ms 20.84 ms −1.13 ms 27.70 MiB 13.38 MiB −14.32 MiB 0 0/0
Frontend differential — ✅ 1,020/1,020 cases matched; 0 issues; 1 Chromium reference recoveries
Match DOM mismatch Diagnostic mismatch Moli error Reference error Infrastructure error Mismatched frames Duration
1,020 0 0 0 0 0 0 7m 14.74s

Recovered Chromium reference infrastructure failures

  • react/browsing-context-boundaries/same-to-cross-origin-transition — 1 failed attempt(s) before recovery · unknown
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 4.16 ms 25.92 ms
Chromium 8 8 47/47 263.94 ms 94.21 ms
Operation p50 Chromium Moli Delta Delta %
navigate 48.93 ms 6.08 ms −42.85 ms −87.57%
observe 0.82 ms 0.90 ms +0.07 ms +8.72%
fill 3.80 ms 3.63 ms −0.17 ms −4.50%
click 5.28 ms 4.23 ms −1.05 ms −19.92%
Idle-resource episode Peak RSS Peak PSS Average CPU Peak CPU Peak processes Sampler
Moli 65.34 MiB 62.01 MiB 0.85% 17.96% 1 ✅
Chromium 1289.50 MiB 417.96 MiB 2.55% 89.95% 13 ⚠️ PSS partial
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 18.64 ms 19.43 ms 675.00 KiB
Long-lived CDP session 13 0 18.92 ms 19.54 ms —
CDP smoke — ✅ 52/52 groups passed; 621 scenarios

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

Slowest group Scenarios Duration Status
scrollbar-visibility 22 30.57 s ✅
target-lifecycle 24 15.98 s ✅
dom-input 27 12.87 s ✅
chromium-cdp 56 12.09 s ✅
multi-page 45 10.56 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 force-pushed the codex/content-math-formatting branch 2 times, most recently from c57d52b to f3cceb4 Compare October 9, 2026 12:18
@lanyue-llk
lanyue-llk changed the base branch from codex/content-media-forms to main October 9, 2026 12:18
@github-actions

github-actions Bot commented Oct 10, 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 7c4e732346 → HEAD cd0193d928; benchmark order: head-first; workflow: success.

Session resilience

Metric Base HEAD
Attempted / planned 200 / 200 200 / 200
Direct observable pass 200 / 200 198 / 200
Failures / recovered 0 / 0 2 / 2
Recovery failures 0 0
Lifecycle order violations 0 0
Network order violations 0 0
Wall time 503.4 s 549.5 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 264.6 MiB 273.4 MiB +8.8 MiB
Periodic peak PSS 260.8 MiB 269.3 MiB +8.5 MiB
RSS · first 10 avg 180.3 MiB 175.7 MiB -4.6 MiB
RSS · last 10 avg 186.2 MiB 189.3 MiB +3.1 MiB
RSS · last − first window +5.8 MiB +13.6 MiB +7.7 MiB
RSS · warm slope / 100 nav +5.2 MiB +7.2 MiB +2.0 MiB
PSS · first 10 avg 177.2 MiB 172.0 MiB -5.1 MiB
PSS · last 10 avg 182.3 MiB 186.1 MiB +3.7 MiB
PSS · last − first window +5.2 MiB +14.0 MiB +8.8 MiB
PSS · warm slope / 100 nav +5.1 MiB +7.5 MiB +2.4 MiB
Peak file descriptors 58 59 1
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 180.0 MiB 157.1 MiB 236.9 MiB 176.4 MiB 153.1 MiB 55
51–100 182.4 MiB 180.7 MiB 232.4 MiB 178.8 MiB 177.4 MiB 54
101–150 188.7 MiB 165.7 MiB 239.1 MiB 185.6 MiB 162.5 MiB 53
151–200 191.8 MiB 207.7 MiB 244.1 MiB 188.4 MiB 204.4 MiB 54

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.

@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Closing after the consolidation audit. This belongs to an earlier experimental Markdown stack and was not part of the measured fixed build. Current failure evidence does not justify carrying its broad formatting expansion into #1501, so it is intentionally not merged.

@lanyue-llk lanyue-llk closed this Oct 13, 2026
@lanyue-llk lanyue-llk reopened this Oct 14, 2026
@lanyue-llk

Copy link
Copy Markdown
Contributor Author

Reopened after correcting the consolidation scope: this is an independent pre-existing stack and should not have been closed as part of #1501 cleanup. Its original scope and base relationship are preserved; this does not add it to #1501.

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