purifai 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLAIMER.md +34 -0
- package/LICENSE +1 -28
- package/README.md +193 -151
- package/benchmark/results/v3.json +1334 -1334
- package/docs/benchmarks/v3.md +45 -45
- package/package.json +15 -5
package/DISCLAIMER.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Purifai — security disclaimer
|
|
2
|
+
|
|
3
|
+
Moved verbatim out of `LICENSE`, where the appended text prevented GitHub
|
|
4
|
+
from detecting the MIT license. The MIT terms are unchanged and remain in
|
|
5
|
+
`LICENSE`; nothing below alters them.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
ADDITIONAL SECURITY DISCLAIMER:
|
|
9
|
+
|
|
10
|
+
WHILE THIS SOFTWARE HAS BEEN TESTED AGAINST KNOWN XSS ATTACK VECTORS, NO
|
|
11
|
+
SECURITY LIBRARY CAN GUARANTEE COMPLETE PROTECTION AGAINST ALL POSSIBLE
|
|
12
|
+
ATTACKS. NEW ATTACK VECTORS MAY BE DISCOVERED THAT COULD BYPASS CURRENT
|
|
13
|
+
PROTECTIONS.
|
|
14
|
+
|
|
15
|
+
THE AUTHORS AND CONTRIBUTORS SPECIFICALLY DISCLAIM ANY LIABILITY FOR:
|
|
16
|
+
- SECURITY BREACHES OR INCIDENTS
|
|
17
|
+
- DATA LOSS OR CORRUPTION
|
|
18
|
+
- FINANCIAL OR BUSINESS LOSSES
|
|
19
|
+
- ANY DAMAGES RESULTING FROM THE USE OF THIS SOFTWARE
|
|
20
|
+
|
|
21
|
+
USERS ARE RESPONSIBLE FOR:
|
|
22
|
+
- TESTING THE SOFTWARE IN THEIR SPECIFIC ENVIRONMENT
|
|
23
|
+
- IMPLEMENTING ADDITIONAL SECURITY MEASURES
|
|
24
|
+
- CONDUCTING THEIR OWN SECURITY ASSESSMENTS
|
|
25
|
+
- KEEPING THE SOFTWARE UPDATED
|
|
26
|
+
|
|
27
|
+
THIS SOFTWARE IS ONE COMPONENT OF A COMPREHENSIVE SECURITY STRATEGY AND
|
|
28
|
+
SHOULD BE USED IN CONJUNCTION WITH OTHER SECURITY BEST PRACTICES INCLUDING
|
|
29
|
+
BUT NOT LIMITED TO CONTENT SECURITY POLICY (CSP), INPUT VALIDATION, OUTPUT
|
|
30
|
+
ENCODING, AND DEFENSE-IN-DEPTH APPROACHES.
|
|
31
|
+
|
|
32
|
+
BY USING THIS SOFTWARE, YOU ACKNOWLEDGE THAT YOU HAVE READ AND UNDERSTOOD
|
|
33
|
+
THESE DISCLAIMERS AND AGREE TO USE THE SOFTWARE AT YOUR OWN RISK.
|
|
34
|
+
```
|
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2025
|
|
3
|
+
Copyright (c) 2025 Mojtaba Beheshti
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -19,30 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
-
|
|
23
|
-
ADDITIONAL SECURITY DISCLAIMER:
|
|
24
|
-
|
|
25
|
-
WHILE THIS SOFTWARE HAS BEEN TESTED AGAINST KNOWN XSS ATTACK VECTORS, NO
|
|
26
|
-
SECURITY LIBRARY CAN GUARANTEE COMPLETE PROTECTION AGAINST ALL POSSIBLE
|
|
27
|
-
ATTACKS. NEW ATTACK VECTORS MAY BE DISCOVERED THAT COULD BYPASS CURRENT
|
|
28
|
-
PROTECTIONS.
|
|
29
|
-
|
|
30
|
-
THE AUTHORS AND CONTRIBUTORS SPECIFICALLY DISCLAIM ANY LIABILITY FOR:
|
|
31
|
-
- SECURITY BREACHES OR INCIDENTS
|
|
32
|
-
- DATA LOSS OR CORRUPTION
|
|
33
|
-
- FINANCIAL OR BUSINESS LOSSES
|
|
34
|
-
- ANY DAMAGES RESULTING FROM THE USE OF THIS SOFTWARE
|
|
35
|
-
|
|
36
|
-
USERS ARE RESPONSIBLE FOR:
|
|
37
|
-
- TESTING THE SOFTWARE IN THEIR SPECIFIC ENVIRONMENT
|
|
38
|
-
- IMPLEMENTING ADDITIONAL SECURITY MEASURES
|
|
39
|
-
- CONDUCTING THEIR OWN SECURITY ASSESSMENTS
|
|
40
|
-
- KEEPING THE SOFTWARE UPDATED
|
|
41
|
-
|
|
42
|
-
THIS SOFTWARE IS ONE COMPONENT OF A COMPREHENSIVE SECURITY STRATEGY AND
|
|
43
|
-
SHOULD BE USED IN CONJUNCTION WITH OTHER SECURITY BEST PRACTICES INCLUDING
|
|
44
|
-
BUT NOT LIMITED TO CONTENT SECURITY POLICY (CSP), INPUT VALIDATION, OUTPUT
|
|
45
|
-
ENCODING, AND DEFENSE-IN-DEPTH APPROACHES.
|
|
46
|
-
|
|
47
|
-
BY USING THIS SOFTWARE, YOU ACKNOWLEDGE THAT YOU HAVE READ AND UNDERSTOOD
|
|
48
|
-
THESE DISCLAIMERS AND AGREE TO USE THE SOFTWARE AT YOUR OWN RISK.
|
package/README.md
CHANGED
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
|
|
4
4
|
# Purifai
|
|
5
5
|
|
|
6
|
+
[](https://www.npmjs.com/package/purifai)
|
|
7
|
+
[](https://github.com/moji2002/purifai/actions/workflows/ci.yml)
|
|
8
|
+
[](docs/benchmarks/v3.md)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
**Readable text from hostile HTML—without a DOM.**
|
|
12
|
+
|
|
13
|
+
[Live site](https://purifai.worksonmy.dev) ·
|
|
6
14
|
[npm](https://www.npmjs.com/package/purifai) ·
|
|
7
15
|
[Project notes](https://worksonmy.dev/projects/purifai) ·
|
|
8
|
-
[Runnable examples](https://github.com/moji2002/purifai/tree/main/examples) ·
|
|
9
16
|
[Issues](https://github.com/moji2002/purifai/issues)
|
|
10
17
|
|
|
11
|
-
Purifai is a fixed-policy HTML-to-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
paragraphs, lists, links, image alternatives, code, and simple tables.
|
|
18
|
+
Purifai is a fixed-policy HTML-to-text converter for servers, browsers, and
|
|
19
|
+
edge runtimes. It keeps useful document structure, drops non-reader bodies, and
|
|
20
|
+
enforces input, output, nesting, and retained-token limits while scanning.
|
|
15
21
|
|
|
16
22
|
```ts
|
|
17
23
|
import { toText } from 'purifai';
|
|
@@ -19,6 +25,8 @@ import { toText } from 'purifai';
|
|
|
19
25
|
const text = toText(
|
|
20
26
|
'<script>alert(1)</script><h2>Release</h2><ul><li>Fast</li></ul>',
|
|
21
27
|
);
|
|
28
|
+
|
|
29
|
+
console.log(text);
|
|
22
30
|
// Release
|
|
23
31
|
//
|
|
24
32
|
// - Fast
|
|
@@ -27,32 +35,16 @@ const text = toText(
|
|
|
27
35
|
A flat tag remover can leak `alert(1)` from the script body and collapse the
|
|
28
36
|
remaining text. Purifai drops that body and formats the reader content.
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
sinks:
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
import { escapeHtmlText, toText } from 'purifai';
|
|
35
|
-
|
|
36
|
-
element.textContent = toText(untrustedHtml);
|
|
37
|
-
element.innerHTML = escapeHtmlText(toText(untrustedHtml));
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Prefer `textContent`. `escapeHtmlText` exists for an HTML text context only; it
|
|
41
|
-
does not make a value safe for an attribute, URL, JavaScript, CSS, or template
|
|
42
|
-
source.
|
|
38
|
+
## Choose Purifai when
|
|
43
39
|
|
|
44
|
-
|
|
40
|
+
- HTML may be large, malformed, or hostile.
|
|
41
|
+
- You want readable plain text—not preserved markup or a browser DOM.
|
|
42
|
+
- Conversion must have deterministic resource limits.
|
|
43
|
+
- The same implementation must run in Node, Bun, Deno, Workers, and browsers.
|
|
44
|
+
- Streaming should produce the same result regardless of chunk boundaries.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- readable extraction instead of flat deletion;
|
|
49
|
-
- deterministic input, output, nesting, and retained-token limits;
|
|
50
|
-
- chunk-invariant Web `TransformStream` conversion;
|
|
51
|
-
- no DOM, tree, Node built-in, or runtime dependency; and
|
|
52
|
-
- one side-effect-free artifact across server, browser, and edge runtimes.
|
|
53
|
-
|
|
54
|
-
It does not preserve markup and does not classify a user's intent. If either is
|
|
55
|
-
your requirement, use a tool designed for that different job.
|
|
46
|
+
If you need selector-driven formatting, complex table layout, or allow-listed
|
|
47
|
+
safe HTML, jump to [Which tool should you choose?](#which-tool-should-you-choose).
|
|
56
48
|
|
|
57
49
|
## Install
|
|
58
50
|
|
|
@@ -60,57 +52,70 @@ your requirement, use a tool designed for that different job.
|
|
|
60
52
|
npm install purifai
|
|
61
53
|
```
|
|
62
54
|
|
|
63
|
-
Purifai v3 requires Node.js 22 or newer when used in Node.
|
|
64
|
-
|
|
65
|
-
## API
|
|
55
|
+
Purifai v3 requires Node.js 22 or newer when used in Node. It ships ESM and
|
|
56
|
+
CommonJS exports and has zero runtime dependencies.
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Converts one string and returns readable text. A breached limit throws a
|
|
70
|
-
`PurifaiLimitError`.
|
|
58
|
+
## Quick start
|
|
71
59
|
|
|
72
60
|
```ts
|
|
73
61
|
import { toText } from 'purifai';
|
|
74
62
|
|
|
75
|
-
const text = toText('<h1>Guide</h1><p>Start here
|
|
63
|
+
const text = toText('<h1>Guide</h1><p>Start <strong>here</strong>.</p>', {
|
|
76
64
|
layout: 'readable',
|
|
77
|
-
links: 'label
|
|
65
|
+
links: 'label',
|
|
78
66
|
images: 'alt',
|
|
79
|
-
baseUrl: 'https://docs.example/',
|
|
80
|
-
limits: { input: 1_000_000, output: 250_000, depth: 64, token: 65_536 },
|
|
81
67
|
});
|
|
68
|
+
|
|
69
|
+
// Guide
|
|
70
|
+
//
|
|
71
|
+
// Start here.
|
|
82
72
|
```
|
|
83
73
|
|
|
84
|
-
|
|
74
|
+
`toText` returns a JavaScript string. It does not return safe HTML.
|
|
75
|
+
|
|
76
|
+
## Safe output
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
that can deliberately return a bounded prefix instead of throwing.
|
|
78
|
+
Prefer a text sink:
|
|
88
79
|
|
|
89
80
|
```ts
|
|
90
|
-
|
|
81
|
+
element.textContent = toText(untrustedHtml);
|
|
82
|
+
```
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
limits: { output: 20_000 },
|
|
94
|
-
overflow: 'truncate',
|
|
95
|
-
});
|
|
84
|
+
If the only available sink is an HTML text node, escape the text explicitly:
|
|
96
85
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
result.outputCodeUnits;
|
|
102
|
-
result.droppedContainers; // e.g. { script: 2, style: 1 }
|
|
86
|
+
```ts
|
|
87
|
+
import { escapeHtmlText, toText } from 'purifai';
|
|
88
|
+
|
|
89
|
+
element.innerHTML = escapeHtmlText(toText(untrustedHtml));
|
|
103
90
|
```
|
|
104
91
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
92
|
+
`escapeHtmlText` is only for an HTML text context. It does not make a value safe
|
|
93
|
+
for an attribute, URL, JavaScript, CSS, or template source. A displayed URL is
|
|
94
|
+
also still text; moving it into `href` requires a separate URL-policy decision.
|
|
95
|
+
|
|
96
|
+
## Why Purifai
|
|
108
97
|
|
|
109
|
-
|
|
98
|
+
Most HTML-to-text tools optimize for either minimal tag removal or broad
|
|
99
|
+
formatting control. Purifai targets a narrower intersection:
|
|
110
100
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
| Requirement | Purifai behavior |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| Reader-friendly output | Preserves headings, paragraphs, lists, quotes, code, simple tables, links, and image alternatives |
|
|
104
|
+
| Non-reader content | Drops bodies such as `script`, `style`, `template`, `iframe`, `svg`, and `math` |
|
|
105
|
+
| Hostile-input bounds | Enforces input, output, depth, and aggregate retained-token limits during scanning |
|
|
106
|
+
| Streaming | Uses a native Web `TransformStream` with chunk-invariant output |
|
|
107
|
+
| Portability | Uses no DOM, document tree, Node built-in, or runtime dependency |
|
|
108
|
+
| Predictability | Fixed policy, validated options, explicit overflow behavior, and frozen reports |
|
|
109
|
+
|
|
110
|
+
That fixed scope is the reason to choose Purifai. It deliberately does not
|
|
111
|
+
preserve markup, reconstruct CSS layout, expose custom formatters, or classify a
|
|
112
|
+
user's intent.
|
|
113
|
+
|
|
114
|
+
## Streaming
|
|
115
|
+
|
|
116
|
+
`createTextTransform` converts incrementally using the same state machine as
|
|
117
|
+
`toText`. Joining its output produces exactly the same text for every possible
|
|
118
|
+
input chunking.
|
|
114
119
|
|
|
115
120
|
```ts
|
|
116
121
|
import { createTextTransform } from 'purifai';
|
|
@@ -122,147 +127,184 @@ const transform = createTextTransform({ links: 'label-and-url' });
|
|
|
122
127
|
const readable = response.body
|
|
123
128
|
.pipeThrough(new TextDecoderStream())
|
|
124
129
|
.pipeThrough(transform);
|
|
125
|
-
const reader = readable.getReader();
|
|
126
130
|
|
|
127
|
-
for (
|
|
128
|
-
|
|
129
|
-
if (done) break;
|
|
130
|
-
consumeText(value);
|
|
131
|
+
for await (const chunk of readable) {
|
|
132
|
+
consumeText(chunk);
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
const report = await transform.result;
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
Stream conversion always throws
|
|
137
|
-
been enqueued when `readable` and `transform.result` reject, so
|
|
138
|
-
output unless your application
|
|
139
|
-
does not buffer the whole result to make an error transactional.
|
|
138
|
+
Stream conversion always throws when a limit is breached. Some output may
|
|
139
|
+
already have been enqueued when `readable` and `transform.result` reject, so
|
|
140
|
+
discard partial output unless your application explicitly accepts it.
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
## Bounded conversion
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`innerHTML`.
|
|
146
|
-
|
|
147
|
-
### `PurifaiLimitError`
|
|
148
|
-
|
|
149
|
-
Extends `RangeError` and exposes `kind`, `limit`, and `observed`.
|
|
144
|
+
`toText` throws a `PurifaiLimitError` when any configured limit is exceeded.
|
|
145
|
+
Use `convert` only when a bounded prefix is an acceptable result:
|
|
150
146
|
|
|
151
147
|
```ts
|
|
152
|
-
import {
|
|
148
|
+
import { convert } from 'purifai';
|
|
153
149
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
150
|
+
const result = convert(largeHtml, {
|
|
151
|
+
limits: { input: 1_000_000, output: 20_000, depth: 64, token: 65_536 },
|
|
152
|
+
overflow: 'truncate',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
result.text;
|
|
156
|
+
result.truncatedBy; // 'input', 'output', 'depth', 'token', or null
|
|
157
|
+
result.scanComplete; // false after truncation
|
|
158
|
+
result.consumedInputCodeUnits;
|
|
159
|
+
result.outputCodeUnits;
|
|
160
|
+
result.droppedContainers; // e.g. { script: 2, style: 1 }
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
Truncation is explicit and deterministic, and never emits half of a UTF-16
|
|
164
|
+
surrogate pair. `toText` and `createTextTransform` never truncate silently.
|
|
165
|
+
|
|
166
|
+
## Options
|
|
164
167
|
|
|
165
|
-
Unknown keys and invalid values throw `TypeError`; Purifai does not
|
|
166
|
-
|
|
168
|
+
Unknown keys and invalid values throw `TypeError`; Purifai does not guess around
|
|
169
|
+
configuration mistakes.
|
|
167
170
|
|
|
168
171
|
| Option | Type | Default | Meaning |
|
|
169
172
|
| --- | --- | --- | --- |
|
|
170
|
-
| `layout` | `'readable' \| 'compact'` | `'readable'` | Structural
|
|
171
|
-
| `links` | `'label' \| 'label-and-url' \| 'drop'` | `'label'` | Keep label, append an accepted display URL, or drop the link body |
|
|
173
|
+
| `layout` | `'readable' \| 'compact'` | `'readable'` | Structural boundaries, or normalized single-space text |
|
|
174
|
+
| `links` | `'label' \| 'label-and-url' \| 'drop'` | `'label'` | Keep the label, append an accepted display URL, or drop the link body |
|
|
172
175
|
| `images` | `'alt' \| 'drop'` | `'alt'` | Emit decoded non-empty `alt` text, or omit images |
|
|
173
176
|
| `baseUrl` | `string \| URL` | none | Resolve relative display URLs against a credential-free HTTP(S) base |
|
|
174
177
|
| `limits.input` | non-negative safe integer | `1_000_000` | Maximum input UTF-16 code units consumed |
|
|
175
178
|
| `limits.output` | non-negative safe integer | `250_000` | Maximum output UTF-16 code units emitted |
|
|
176
179
|
| `limits.depth` | non-negative safe integer | `64` | Maximum live structural nesting |
|
|
177
|
-
| `limits.token` | non-negative safe integer | `65_536` | Maximum aggregate retained token
|
|
180
|
+
| `limits.token` | non-negative safe integer | `65_536` | Maximum aggregate retained token and attribute code units |
|
|
178
181
|
| `overflow` | `'throw' \| 'truncate'` | `'throw'` | `convert` only; other APIs always throw |
|
|
179
182
|
|
|
180
|
-
All four limits are enforced
|
|
181
|
-
|
|
182
|
-
bytes.
|
|
183
|
+
All four limits are enforced before unbounded caller-controlled state can
|
|
184
|
+
accumulate. Values measure JavaScript UTF-16 code units, not encoded bytes.
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
### Display URL policy
|
|
185
187
|
|
|
186
|
-
`label-and-url` emits
|
|
188
|
+
`label-and-url` emits destinations as display text, never as active links. It
|
|
187
189
|
accepts absolute `http:`, `https:`, and `mailto:` URLs. Relative URLs require a
|
|
188
|
-
validated HTTP(S) `baseUrl`.
|
|
189
|
-
protocol-relative inputs, leading backslashes,
|
|
190
|
-
|
|
190
|
+
validated HTTP(S) `baseUrl`. Credentials, controls, ambiguous schemes,
|
|
191
|
+
protocol-relative inputs, leading backslashes, unsupported schemes, and invalid
|
|
192
|
+
URLs are omitted while their visible label remains.
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
separate URL-policy decision at that sink.
|
|
194
|
+
## Extraction policy
|
|
194
195
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
literal `xmp`, and treats `plaintext` as text through end of input.
|
|
196
|
+
Purifai removes source and non-reader bodies including `script`, `style`,
|
|
197
|
+
`template`, `iframe`, `noscript`, `noembed`, `noframes`, `svg`, and `math`. It
|
|
198
|
+
preserves selected fallback and form text, decodes the complete pinned WHATWG
|
|
199
|
+
character-reference set, preserves literal `xmp`, and treats `plaintext` as text
|
|
200
|
+
through end of input.
|
|
201
201
|
|
|
202
202
|
This is a bounded extraction grammar, not browser tree construction. It does not
|
|
203
203
|
recreate CSS layout, browser `innerText`, complex `rowspan`/`colspan` tables,
|
|
204
204
|
SVG/MathML semantics, selector rules, custom formatters, or browser-equivalent
|
|
205
|
-
malformed-markup recovery.
|
|
206
|
-
line boundaries.
|
|
207
|
-
|
|
208
|
-
## Which tool should you choose?
|
|
209
|
-
|
|
210
|
-
| Need | Choice |
|
|
211
|
-
| --- | --- |
|
|
212
|
-
| Fixed-policy readable text, hostile-input bounds, and portable Web streaming | Choose Purifai |
|
|
213
|
-
| Selectors, custom formatters, advanced tables, wrapping, and broader formatting control | Choose `html-to-text` |
|
|
214
|
-
| The smallest flat tag-removal operation | Choose stable `striptags` |
|
|
215
|
-
| Preserve an allow-listed safe HTML fragment | Choose DOMPurify or `sanitize-html` |
|
|
216
|
-
|
|
217
|
-
These tools are not interchangeable. In particular, DOMPurify and
|
|
218
|
-
`sanitize-html` are the right category when safe markup must survive.
|
|
205
|
+
malformed-markup recovery.
|
|
219
206
|
|
|
220
207
|
## Benchmarks
|
|
221
208
|
|
|
222
209
|
The checked category benchmark pins `striptags@3.2.0` and
|
|
223
|
-
`html-to-text@10.0.0`. It measures
|
|
224
|
-
isolated warm median and p95
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
the product claim.
|
|
210
|
+
`html-to-text@10.0.0`. It measures reviewed readability and body-removal
|
|
211
|
+
fixtures, isolated warm median and p95 latency, and fresh-process peak RSS.
|
|
212
|
+
|
|
213
|
+
On the recorded Apple M1 / Node 24 run, Purifai passed all 11 category gates:
|
|
228
214
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
memory corpora. `striptags` remains faster on some flat-strip cases, which is not
|
|
233
|
-
Purifai's claim.
|
|
215
|
+
- 8/8 readability fixtures and all 5 non-reader-body fixtures;
|
|
216
|
+
- lower hostile-input p95 than `html-to-text` on four hostile corpora; and
|
|
217
|
+
- lower streaming peak RSS than `html-to-text` on all five memory corpora.
|
|
234
218
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
`pnpm run bench:check`.
|
|
219
|
+
`striptags` remains faster on some flat-strip cases. That is not Purifai's
|
|
220
|
+
claim. Results are machine-, runtime-, and corpus-specific.
|
|
238
221
|
|
|
239
|
-
|
|
222
|
+
See the [complete methodology, raw results, and tables](docs/benchmarks/v3.md).
|
|
223
|
+
Reproduce measurements with `pnpm run bench`; check the recorded release gates
|
|
224
|
+
with `pnpm run bench:check`.
|
|
240
225
|
|
|
241
|
-
|
|
242
|
-
data—is gated at 25 KiB using deterministic `gzip -9`. The recorded artifact is
|
|
243
|
-
23,689 bytes. `pnpm run test:size` also checks the packed exports, zero runtime
|
|
244
|
-
dependencies, cold import time, and retained import heap.
|
|
226
|
+
## Size, portability, and release proof
|
|
245
227
|
|
|
246
|
-
The
|
|
228
|
+
The complete minified ESM runtime—including all 2,231 pinned WHATWG entity
|
|
229
|
+
names—is 23,689 bytes with deterministic `gzip -9`. The release gate also checks
|
|
230
|
+
packed exports, zero runtime dependencies, cold import time, and retained import
|
|
231
|
+
heap.
|
|
247
232
|
|
|
248
|
-
|
|
233
|
+
The same packed artifact is tested in:
|
|
234
|
+
|
|
235
|
+
| Runtime | Release coverage |
|
|
249
236
|
| --- | --- |
|
|
250
237
|
| Node.js | 22, 24, and 26; ESM and CommonJS |
|
|
251
|
-
| Bun | ESM and CommonJS
|
|
252
|
-
| Deno | ESM
|
|
253
|
-
| Cloudflare Workers |
|
|
238
|
+
| Bun | ESM and CommonJS |
|
|
239
|
+
| Deno | ESM |
|
|
240
|
+
| Cloudflare Workers | Real `workerd`, without Node compatibility |
|
|
254
241
|
| Browsers | Chromium, Firefox, and WebKit |
|
|
255
242
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
243
|
+
Release qualification also includes 10,000 seeded malformed-input cases,
|
|
244
|
+
adversarial scaling checks, safe-sink tests with a positive control, package
|
|
245
|
+
smoke tests, and npm OIDC provenance bound to the tagged GitHub source commit.
|
|
246
|
+
|
|
247
|
+
## API reference
|
|
248
|
+
|
|
249
|
+
### `toText(html, options?) → string`
|
|
250
|
+
|
|
251
|
+
Converts one HTML string into readable text. Throws `TypeError` for invalid
|
|
252
|
+
input or options and `PurifaiLimitError` for a breached limit.
|
|
253
|
+
|
|
254
|
+
### `convert(html, options?) → ConversionResult`
|
|
255
|
+
|
|
256
|
+
Returns text plus a frozen report containing completion, truncation, consumed
|
|
257
|
+
input, output length, and dropped-container counts. It is the only API that can
|
|
258
|
+
return a deliberately truncated prefix.
|
|
259
|
+
|
|
260
|
+
### `createTextTransform(options?) → TextTransform`
|
|
261
|
+
|
|
262
|
+
Returns a native `TransformStream<string, string>` with a `result` promise for
|
|
263
|
+
the frozen conversion report. Limit failures reject both the stream and the
|
|
264
|
+
promise with the same error object.
|
|
265
|
+
|
|
266
|
+
### `escapeHtmlText(text) → string`
|
|
267
|
+
|
|
268
|
+
Losslessly encodes `&`, `<`, `>`, `"`, and `'` for an HTML text-node context.
|
|
269
|
+
|
|
270
|
+
### `PurifaiLimitError`
|
|
271
|
+
|
|
272
|
+
Extends `RangeError` and exposes `kind`, `limit`, and `observed`.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { PurifaiLimitError, toText } from 'purifai';
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
toText(html, { limits: { input: 10_000 } });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error instanceof PurifaiLimitError) {
|
|
281
|
+
console.error(error.kind, error.limit, error.observed);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Which tool should you choose?
|
|
287
|
+
|
|
288
|
+
| Need | Choice |
|
|
289
|
+
| --- | --- |
|
|
290
|
+
| Fixed-policy readable text, hostile-input bounds, and portable Web streaming | Choose Purifai |
|
|
291
|
+
| Selectors, custom formatters, advanced tables, wrapping, and broad formatting control | Choose `html-to-text` |
|
|
292
|
+
| The smallest flat tag-removal operation | Choose stable `striptags` |
|
|
293
|
+
| Preserve an allow-listed safe HTML fragment | Choose DOMPurify or `sanitize-html` |
|
|
294
|
+
|
|
295
|
+
These categories are not interchangeable. DOMPurify and `sanitize-html` are
|
|
296
|
+
the correct category when safe markup must survive.
|
|
259
297
|
|
|
260
298
|
## Migration and development
|
|
261
299
|
|
|
262
300
|
V3 is a clean break. See the [v3 migration guide](docs/migration-v3.md) for every
|
|
263
|
-
removed export and option.
|
|
264
|
-
|
|
301
|
+
removed export and option.
|
|
302
|
+
|
|
303
|
+
- [Runnable examples](examples)
|
|
304
|
+
- [Contributor guide](CONTRIBUTING.md)
|
|
305
|
+
- [Project notes](https://worksonmy.dev/projects/purifai)
|
|
306
|
+
- [Issues](https://github.com/moji2002/purifai/issues)
|
|
265
307
|
|
|
266
308
|
## License
|
|
267
309
|
|
|
268
|
-
MIT
|
|
310
|
+
[MIT](LICENSE)
|