fix(render): pydoc 的 L2 启发式只在 pydoc 模式生效(#281)
`detectL2IndentedPatterns()` 的两条 pydoc 规则(` class Name(` 与
` name(`)没有模式门,于是 perldoc/man 正文里恰好 4 空格缩进、以
`name(` 开头的行会被提升为 L2 小节:
chomp( LIST ) → ### chomp
nasty_break() is ... → ### nasty_break
perldoc 三页语料共 58 行受影响:正文被当成标题,TOC 里冒出假条目。
给两条规则加上 `$mode === "pydoc"` 的门;man 页不受影响(see-also 是 7 空格)。
语料门先红后绿:perldoc-perlfunc/perlvar/DBI 的黄金在 8.3 上重新生成,
差异仅为这些行从 `### xxx` 变回正文,已逐行核对。
Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
test(corpus): 黄金/文本完整性/结构/JSON 四条检查(#255) Co-Authored-By: Claude Code <noreply@anthropic.com>
test(corpus): 采集六页真实语料与黄金输出(#255) Task 8 收尾:在生产机(man 2.12.0 / groff 1.23.0 / perl 5.38.2)冻结 六页真实工具输出(man-ls、man-tar、man-ssh_config、perldoc-perlfunc、 perldoc-perlvar、perldoc-DBI,共 922KB),黄金由生产 /usr/local/php83 (PHP 8.3.36)对冻结 raw 重渲染生成——raw 字节是唯一事实源,黄金只是 它的确定性投影。 - test/corpus/raw/:六页冻结输出;test/corpus/golden/:6×.md + 6×.fingerprint;test/corpus/VERSIONS.md:工具版本 + 每页字节数/sha256。 - test/corpus/normalize.php(Ruling 22):corpusNormalizeText() 把 邮箱改写为渲染器混淆后的 "user AT host" 形式,raw 与 HTML 两侧 token 才可比——内容保持归一化,不放松断言。 - test/unit/test_corpus_normalize.php:补 5 条邮箱归一化断言。 审阅:六页 .md 无 ESC/\x08/?(0lines)/空标题/超长行/控制字符; ad59aab、d2086fe 两轮修复后 markdown 无 _** 交错(man-tar 28 行、 man-ssh_config 4 行均呈 **_ 正确形式)。 门禁:本地 PHP 8.5 make test-corpus 六页 [OK] + [PASS] xhtml (6 页 dtd-valid);生产 PHP 8.3.36 check 六页 [OK],xhtml 因该机 无 xmllint 按设计 [SKIP](DTD 校验由本地对同样字节完成)。token multiset 六页 missing=0。make test-unit 949 passed, 0 failed。 Co-Authored-By: Claude code 2.1.311 with glm-5.3-flash <noreply@taotoken.net>
test(corpus): 语料清单、归一化库与结构指纹(#255) - test/corpus/pages.txt:6 页语料清单(tab 分隔,# 注释,空 section) - test/corpus/normalize.php:corpusReadManifest / corpusStripFormatting / corpusNormalizeText / corpusTokens / corpusMissingTokens / corpusTextFromHtml - test/corpus/fingerprint.php:jsonStructureFingerprint 从 structure_regression.php 原样移出,供后续 corpus 工具复用 - test/unit/test_corpus_normalize.php:21 断言;run_all.php 登记后 853 passed, 0 failed Co-Authored-By: Claude code 2.1.311 with deepseek-flash <noreply@taotoken.net>
test(corpus): 有清单但 raw 为空时必须失败,不再静默 SKIP(#255) Co-Authored-By: Claude Code <noreply@anthropic.com>
test(corpus): xhtml 断言整体置于 xmllint 存在性守卫内(#255) 审查发现:干净语料子进程的 [PASS] xhtml 断言在无 xmllint 的机器上会 把套件打红(check.php 届时输出 [SKIP] xhtml),违反 Step 1 的跳过要求。 将三节 xhtml 测试统一收进 command -v xmllint 守卫:无 xmllint → 打印 [SKIP] xhtml、exit 0;清空 PATH 一节移入守卫后语义不变(有 xmllint 时 模拟缺失,真缺失时输出同样是 [SKIP])。 已验证无 xmllint 场景:PATH 只留 php 重跑整个测试文件 → [SKIP] xhtml、 20 passed 0 failed、exit 0。 Co-Authored-By: Claude code 2.1.311 with glm-5.3-flash <noreply@taotoken.net>
test(corpus): XHTML 整页校验,DTD 随仓库离线解析(#255) - check.php 新增第五条检查 xhtml:整页(showHeader + formatManPerlDoc + showFooter)经 xmllint --noout --nonet --dtdvalid 校验,非 0 即 FAIL (stderr 前 5 行);本机无 xmllint 记 [SKIP] xhtml 不 fail - W3C 四文件 byte-for-byte 收录 test/corpus/dtd/(下载于 2026-11-06), 外加 phpman 自己的 override DTD:声明 script/@nonce(#273 CSP)与 script/@async(GA)——2002 年的 DTD 没有这两个属性,不加则每页必挂 - 校验中途 header() 的 E_WARNING 会混入捕获的页面字节(多页时第二页起 必现),组装期以 set_error_handler 吞掉并转发 STDERR,保证校验的就是 渲染器真正输出的字节 - 测试:干净语料 [PASS] xhtml;清空 PATH 模拟无 xmllint → [SKIP]; 进程内篡改渲染页(去掉 </body></html>)→ xmllint 拒绝 Co-Authored-By: Claude code 2.1.311 with glm-5.3-flash <noreply@taotoken.net>
fix(render): SGR 状态机替换交错正则 + 语料抽文行内标签不再变空格 语料门(#255)在真实 man 页上暴露两处缺陷: 1. src/format_html.php:两条非贪婪正则把 ESC[1m…ESC[22m / ESC[4m…ESC[24m 各自配对,遇到下划线与加粗在同一点交接的真实序列 (man-tar.txt:ESC[1m-f ESC[4mESC[22mARCHIVE ESC[24m)产出交错的 <b>-f <u></b>ARCHIVE</u>,xmllint 判 invalid。改为小状态机 sgrToHtmlTags():识别 1/22/4/24/0,其余 ESC[...m 丢弃;固定 <b><u> 顺序开、逆序关;行尾补齐闭合;连续码合并为一次状态迁移 (不产生空 <u></u>)。主转换管线拆为两段 preg_replace,状态机 在其间运行(转义之后、链接化之前),pass-1 条目不再重复执行。 2. test/corpus/normalize.php:corpusTextFromHtml() 原先把所有标签 替换为空格,[<u>OPTIONS</u>] 抽成 [ OPTIONS ],与 raw 的 [OPTIONS] 词元对不上。改为块级标签(p/div/br/li/ul/ol/pre/ blockquote/h1-h6/tr/td/th/table/hr/section/article)→ 空格, 行内及其余标签 → 空串;实体解码、空白折叠、trim 不变。 测试:test_overstrike.php 新增 T9–T15(真实 tar 片段断言精确输出 且无 <u></b> 交错、状态机五类输入、行尾补齐、颜色码丢弃);test_ corpus_normalize.php 新增行内/块级断言。全量 php test/run_all.php 940 passed, 0 failed(原基线 925)。全部 6 个 raw 语料渲染结果 xmllint well-formed。 Co-Authored-By: Claude code 2.1.311 with glm-5.3-flash <noreply@taotoken.net>
fix(markdown): cleanTerminalOutput 换用共享 SGR 状态机,消除交错标记 ruling 23:markdown 路径的 cleanTerminalOutput() 仍留着 Task 8a 已从 format_html.php 移除的两条非贪婪 SGR 配对正则,语料门在 man-tar 第 11 行 (ESC[1m-f ESC[4mESC[22mARCHIVE ESC[24m)再次暴露同类损坏: "**-f _**ARCHIVE_"(32 行 golden 受影响,线上 /markdown 同样)。 - 把 Task 8a 的状态机从 format_html.php 上移到 format_common.php,改名 sgrToTags() 并把发射器参数化:默认 HTML <b>/<u>,markdown 传入 \x01/\x02 与 \x03/\x04 标记对——两个渲染路径共享同一份逻辑,不复制。 - cleanTerminalOutput() 同样拆两段 preg_replace:pass1(overstrike→标记) → 状态机 → pass2(颜色兜底剥离、OSC-8、残余 OSC),pass1 条目不重复 执行;标记→**/_ 转换与 \x02\x01、\x04\x03 相邻清理保留不变。 - 真实交接片段(连续码合并为一次迁移)现输出 "**-f **_ARCHIVE_": "**" 先闭合、"_" 后打开,嵌套合法。 测试:test_overstrike.php 新增 T16–T18(真实片段与 man-tar 第 11 行全文 精确断言、io-cancel 钉住旧实现输出 **io___cancel**、行尾未闭合补齐); T10–T15 随函数改名指向 sgrToTags。全量 php test/run_all.php 949 passed, 0 failed。本地复渲染 6 页 golden(未提交,Task 8 重跑):man-tar 第 11 行 "**tar -A **[_OPTIONS_] **-f **_ARCHIVE_ _ARCHIVE_...",全部页面零 "_**" 交错相邻。 Co-Authored-By: Claude code 2.1.311 with glm-5.3-flash <noreply@taotoken.net>
feat(tldr): 增加 PHPMAN_TLDR_DISABLE,供语料渲染离线运行(#255) Co-Authored-By: Claude code 2.1.311 with glm-5.3-flash <noreply@taotoken.net>