diff --git a/packages/adapter-wordpress/package.json b/packages/adapter-wordpress/package.json index 3adf55a..0df0253 100644 --- a/packages/adapter-wordpress/package.json +++ b/packages/adapter-wordpress/package.json @@ -18,6 +18,10 @@ }, "dependencies": { "@sf/contracts": "workspace:*", + "sanitize-html": "^2", "saxes": "^6.0.0" + }, + "devDependencies": { + "@types/sanitize-html": "^2.16.2" } } diff --git a/packages/adapter-wordpress/src/index.ts b/packages/adapter-wordpress/src/index.ts index 45d666b..cb02e08 100644 --- a/packages/adapter-wordpress/src/index.ts +++ b/packages/adapter-wordpress/src/index.ts @@ -2,3 +2,4 @@ export * from './limits.js' export * from './parse/php-unserialize.js' export * from './parse/wxr-stream.js' export * from './map/item-to-row.js' +export * from './sanitize/html.js' diff --git a/packages/adapter-wordpress/src/sanitize/html.ts b/packages/adapter-wordpress/src/sanitize/html.ts new file mode 100644 index 0000000..f35c4d3 --- /dev/null +++ b/packages/adapter-wordpress/src/sanitize/html.ts @@ -0,0 +1,263 @@ +// WordPress 正文 HTML 白名单清洗(Plan 2 Task 5,ADR-009):清洗本体交 sanitize-html +// (锁 ^2,MIT),本模块只做两件事——集中导出白名单配置 {@link SANITIZE_CONFIG}, +// 与"清洗 + 差异收集"纯函数 {@link sanitizeBodyHtml}。原文永远保留在 Source Snapshot +// 是调用方的职责(ADR-009),本模块不碰存储;白名单升级后可对原文重跑本函数。 +// +// 差异收集方案取舍(选定 onOpenTag 观察钩子,JSDoc 钉死): +// - sanitize-html 无原生"移除回调"。候选方案: +// a) onOpenTag(name, attribs)——官方类型支持的观察钩子,对**每个**开标签事件触发 +// (含未批准标签),携带**未过滤**的原始属性表;纯观察、零输出影响(测试钉死 +// 与无仪表化运行的字节等价)。 +// b) transformTags——同样先于白名单判定触发,但具变换语义(须原样回显 +// {tagName, attribs}),仪表化借道变换机制过重。 +// c) exclusiveFilter——只见"存活"元素(关闭时回调、属性已过滤),看不到移除。 +// d) before/after 字符串对比——等价变形(属性重排序/自闭合规范化/实体转义) +// 制造假差异,不可靠。 +// e) 正则预扫描——非 tokenizer 精度(注释/引号内 '>'/大小式变形),仅可作为 +// 下策。onOpenTag 复用 sanitize-html 自身解析器(htmlparser2)的事件流, +// 精度与清洗本体同源,故选 a。 +// - 镜像逻辑(判断"什么会被拒")从 SANITIZE_CONFIG 单源推导,不另立策略表: +// 标签 ∈ allowedTags?属性 ∈ allowedAttributes[tag] ∪ allowedAttributes['*']? +// URL 属性(allowedSchemesAppliedToAttributes)协议 ∈ allowedSchemes +// (含 allowedSchemesByTag 覆盖与协议相对 URL 规则,语义镜像 launder.naughtyHref)。 +// - 不镜像的清洗路径( exotic,报告中不体现;清洗本体不受影响): +// 属性名合法性校验、空值非布尔属性删除、meta refresh content、SVG animate 名单、 +// allowedClasses 过滤、srcset 部分候选重写(仅"全部候选被拒→整属性删除"记账, +// 候选切分为近似逗号分割)、空 srcset 删除。 +// - 记账粒度:按**白名单拒绝的开标签事件**逐次记录(文档序;属性按声明序)—— +// 不按"最终 DOM 差异"去重:discard 剥壳语义下未批准容器的子内容会被解包保留, +// 其属性过滤真实发生,同样值得留痕;宁多报不漏报(审计定位用途)。 +// 顺序确定 → 同输入必得同 diffs(纯函数,无共享状态,测试钉死)。 +// +// 白名单范围决策(冻结;改动须评审,测试钉死值域): +// - h1 不入白名单:标题层级归主题层(文章标题由主题渲染 h1),正文从 h2 起; +// 出现 h1 会进 diff(removed-tag),文本按 discard 语义保留。 +// - 布局语义标签(section/article/header/footer/nav/aside/main)不入白名单: +// discard 剥壳保留子内容,布局由阶段 3 主题层重建,不透传源站布局。 +// - script/style/iframe/form/svg/math 等一律拒绝;iframe 无宿主白名单通道—— +// "allowIframe: false"的等价物就是 allowedTags 缺席(本库无此独立开关)。 +// - style 属性与

s

') + expect(result.html).toBe('

s

') + expect(result.diffs).toStrictEqual([{ type: 'removed-tag', tag: 'style', excerpt: '