moji2002/purifai
GitHub: moji2002/purifai
一个零依赖、跨运行时的固定策略 HTML 转纯文本工具,能从可能恶意的 HTML 中提取可读文本并提供确定性的资源限制。
Stars: 0 | Forks: 0
# Purifai
[](https://www.npmjs.com/package/purifai)
[](https://github.com/moji2002/purifai/actions/workflows/ci.yml)
[](docs/benchmarks/v3.md)
[](LICENSE)
**从恶意 HTML 中提取可读文本——无需 DOM。**
Purifai 是一个固定策略的 HTML 转文本转换器,适用于服务器、浏览器和
边缘 runtime。它保留有用的文档结构,丢弃非阅读正文,并在扫描时强制执行输入、输出、嵌套和保留 token 的限制。
```
import { toText } from 'purifai';
const text = toText(
'`,带有用于
冻结转换报告的 `result` promise。限制失败会使用相同的错误对象同时拒绝流和
promise。
### `escapeHtmlText(text) → string`
为 HTML 文本节点上下文无损地编码 `&`、`<`、`>`、`"` 和 `'`。
### `PurifaiLimitError`
继承 `RangeError` 并暴露 `kind`、`limit` 和 `observed`。
```
import { PurifaiLimitError, toText } from 'purifai';
try {
toText(html, { limits: { input: 10_000 } });
} catch (error) {
if (error instanceof PurifaiLimitError) {
console.error(error.kind, error.limit, error.observed);
}
}
```
## 你应该选择哪个工具?
| 需求 | 选择 |
| --- | --- |
| 固定策略的可读文本、恶意输入边界和可移植的 Web 流式传输 | 选择 Purifai |
| 选择器、自定义格式化器、高级表格、换行和广泛的格式控制 | 选择 `html-to-text` |
| 最小的简单标签移除操作 | 选择稳定的 `striptags` |
| 保留允许列表的安全 HTML 片段 | 选择 DOMPurify 或 `sanitize-html` |
这些类别不能互换。DOMPurify 和 `sanitize-html` 是
当必须保留安全标记时正确的类别。
## 迁移和开发
V3 是一个全新的开始。有关每一个
已移除的导出和选项,请参阅 [v3 迁移指南](docs/migration-v3.md)。
- [可运行示例](examples)
- [贡献者指南](CONTRIBUTING.md)
- [项目笔记](https://worksonmy.dev/projects/purifai)
- [问题](https://github.com/moji2002/purifai/issues)
## 许可证
[MIT](LICENSE)
Release
- Fast
Guide
Start here.
', { layout: 'readable', links: 'label', images: 'alt', }); // Guide // // Start here. ``` `toText` 返回一个 JavaScript 字符串。它不返回安全 HTML。 ## 安全输出 推荐使用文本 sink: ``` element.textContent = toText(untrustedHtml); ``` 如果唯一可用的 sink 是 HTML 文本节点,请显式转义文本: ``` import { escapeHtmlText, toText } from 'purifai'; element.innerHTML = escapeHtmlText(toText(untrustedHtml)); ``` `escapeHtmlText` 仅适用于 HTML 文本上下文。它不能使值在 属性、URL、JavaScript、CSS 或模板源中变得安全。显示的 URL 也 仍然是文本;将其移动到 `href` 需要单独的 URL 策略决定。 ## 为什么选择 Purifai 大多数 HTML 转文本工具要么针对最小化的标签移除进行优化,要么针对广泛的 格式化控制进行优化。Purifai 针对的是一个更狭窄的交集: | 需求 | Purifai 行为 | | --- | --- | | 对读者友好的输出 | 保留标题、段落、列表、引用、代码、简单表格、链接和图片替代文本 | | 非阅读内容 | 丢弃如 `script`、`style`、`template`、`iframe`、`svg` 和 `math` 等 body | | 恶意输入边界 | 在扫描期间强制执行输入、输出、深度和聚合保留 token 限制 | | 流式传输 | 使用原生 Web `TransformStream` 实现不随数据块变化的输出 | | 可移植性 | 不使用 DOM、文档树、Node 内置模块或 runtime 依赖 | | 可预测性 | 固定策略、经过验证的选项、明确的溢出行为和冻结的报告 | 这种固定的范围正是选择 Purifai 的原因。它故意不 保留标记、重建 CSS 布局、暴露自定义格式化器或分类 用户的意图。 ## 流式传输 `createTextTransform` 使用与 `toText` 相同的状态机进行增量转换。合并其 输出对于所有可能的输入分块方式都会产生完全相同的文本。 ``` import { createTextTransform } from 'purifai'; const response = await fetch('https://example.test/article'); if (response.body === null) throw new Error('Response has no body'); const transform = createTextTransform({ links: 'label-and-url' }); const readable = response.body .pipeThrough(new TextDecoderStream()) .pipeThrough(transform); for await (const chunk of readable) { consumeText(chunk); } const report = await transform.result; ``` 当违反限制时,流式转换总是会抛出异常。当 `readable` 和 `transform.result` 被拒绝时,某些输出可能 已经进入队列,因此除非你的应用程序明确接受,否则请丢弃部分输出。 ## 有界转换 当超过任何配置的限制时,`toText` 会抛出 `PurifaiLimitError`。 仅当有界前缀是可接受的结果时才使用 `convert`: ``` import { convert } from 'purifai'; const result = convert(largeHtml, { limits: { input: 1_000_000, output: 20_000, depth: 64, token: 65_536 }, overflow: 'truncate', }); result.text; result.truncatedBy; // 'input', 'output', 'depth', 'token', or null result.scanComplete; // false after truncation result.consumedInputCodeUnits; result.outputCodeUnits; result.droppedContainers; // e.g. { script: 2, style: 1 } ``` 截断是显式且确定性的,绝不会发出半个 UTF-16 代理对。`toText` 和 `createTextTransform` 从不进行静默截断。 ## 选项 未知的键和无效的值会抛出 `TypeError`;Purifai 不会随意猜测 配置错误。 | 选项 | 类型 | 默认值 | 含义 | | --- | --- | --- | --- | | `layout` | `'readable' \| 'compact'` | `'readable'` | 结构边界,或规范化的单空格文本 | | `links` | `'label' \| 'label-and-url' \| 'drop'` | `'label'` | 保留标签、追加已接受的显示 URL 或丢弃链接正文 | | `images` | `'alt' \| 'drop'` | `'alt'` | 发出解码后的非空 `alt` 文本,或忽略图片 | | `baseUrl` | `string \| URL` | 无 | 根据无凭证的 HTTP(S) 基础地址解析相对显示 URL | | `limits.input` | 非负安全整数 | `1_000_000` | 消耗的最大输入 UTF-16 代码单元 | | `limits.output` | 非负安全整数 | `250_000` | 发出的最大输出 UTF-16 代码单元 | | `limits.depth` | 非负安全整数 | `64` | 最大实时结构嵌套 | | `limits.token` | 非负安全整数 | `65_536` | 最大聚合保留 token 和属性代码单元 | | `overflow` | `'throw' \| 'truncate'` | `'throw'` | 仅限 `convert`;其他 API 总是抛出异常 | 所有四个限制都在无界的调用者控制状态累积之前强制执行。这些值 测量的是 JavaScript UTF-16 代码单元,而不是编码后的字节。 ### 显示 URL 策略 `label-and-url` 将目的地作为显示文本发出,从不作为活动链接。它 接受绝对的 `http:`、`https:` 和 `mailto:` URL。相对 URL 需要一个 经过验证的 HTTP(S) `baseUrl`。凭证、控制字符、有歧义的模式、 协议相对输入、前导反斜杠、不支持的模式和无效的 URL 在其可见标签保留的同时会被省略。 ## 提取策略 Purifai 移除源和非阅读正文,包括 `script`、`style`、 `template`、`iframe`、`noscript`、`noembed`、`noframes`、`svg` 和 `math`。它 保留选定的回退和表单文本,解码完整的固定的 WHATWG 字符引用集,保留字面量 `xmp`,并将 `plaintext` 视为文本 直到输入结束。 这是一种有界提取语法,而不是浏览器树构造。它不 重建 CSS 布局、浏览器 `innerText`、复杂的 `rowspan`/`colspan` 表格、 SVG/MathML 语义、选择器规则、自定义格式化器或浏览器等价的 畸形标记恢复。 ## 基准测试 已检查的类别基准测试固定了 `striptags@3.2.0` 和 `html-to-text@10.0.0`。它测量经过审查的可读性和正文移除 测试用例、隔离的预热中位数和 p95 延迟,以及全新进程的峰值 RSS。 在记录的 Apple M1 / Node 24 运行中,Purifai 通过了所有 11 个类别门控: - 8/8 的可读性测试用例和所有 5 个非阅读正文测试用例; - 在四个恶意语料库上的恶意输入 p95 低于 `html-to-text`;且 - 在所有五个内存语料库上的流式峰值 RSS 低于 `html-to-text`。 `striptags` 在一些简单的剥离情况下仍然更快。这并不是 Purifai 的 宣称。结果因机器、runtime 和语料库而异。 有关[完整的方法论、原始结果和表格](docs/benchmarks/v3.md),请参见相应内容。 使用 `pnpm run bench` 复现测量结果;使用 `pnpm run bench:check` 检查记录的发布门控。 ## 大小、可移植性和发布证明 完整的压缩 ESM runtime——包括所有 2,231 个固定的 WHATWG 实体 名称——在使用确定性的 `gzip -9` 压缩后为 23,689 字节。发布门控还检查 打包的导出、零 runtime 依赖、冷导入时间和保留的导入 堆。 相同的打包制品已在以下环境中进行测试: | Runtime | 发布覆盖率 | | --- | --- | | Node.js | 22、24 和 26;ESM 和 CommonJS | | Bun | ESM 和 CommonJS | | Deno | ESM | | Cloudflare Workers | 真实的 `workerd`,无 Node 兼容性 | | 浏览器 | Chromium、Firefox 和 WebKit | 发布资格认证还包括 10,000 个带种子的畸形输入案例、 对抗性扩展检查、带有阳性对照的安全 sink 测试、包 冒烟测试,以及绑定到标记的 GitHub 源提交的 npm OIDC 来源。 ## API 参考 ### `toText(html, options?) → string` 将一个 HTML 字符串转换为可读文本。对于无效的 输入或选项抛出 `TypeError`,对于违反的限制抛出 `PurifaiLimitError`。 ### `convert(html, options?) → ConversionResult` 返回文本以及一个冻结的报告,其中包含完成情况、截断情况、消耗的 输入、输出长度和丢弃的容器计数。它是唯一可以 返回故意截断的前缀的 API。 ### `createTextTransform(options?) → TextTransform` 返回一个原生的 `TransformStream标签:GNU通用公共许可证, HTML转文本, Node.js, 净化器, 数据可视化, 自定义脚本, 零依赖