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 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 Purifai
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
+ [![npm version](https://img.shields.io/npm/v/purifai.svg)](https://www.npmjs.com/package/purifai)
7
+ [![CI](https://github.com/moji2002/purifai/actions/workflows/ci.yml/badge.svg)](https://github.com/moji2002/purifai/actions/workflows/ci.yml)
8
+ [![gzip: 23.7 KiB](https://img.shields.io/badge/gzip-23.7_KiB-2f855a)](docs/benchmarks/v3.md)
9
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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-readable-text converter for servers, browsers,
12
- and edge runtimes. It incrementally removes non-reader bodies, decodes the full
13
- WHATWG character-reference set, and preserves useful structure such as headings,
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
- The output is a JavaScript string, not safe HTML. Use one of these supported
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
- ## Why it exists
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
- Purifai targets one narrow intersection:
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
- ### `toText(html, options?)`
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.</p>', {
63
+ const text = toText('<h1>Guide</h1><p>Start <strong>here</strong>.</p>', {
76
64
  layout: 'readable',
77
- links: 'label-and-url',
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
- ### `convert(html, options?)`
74
+ `toText` returns a JavaScript string. It does not return safe HTML.
75
+
76
+ ## Safe output
85
77
 
86
- Returns the text plus a frozen conversion report. It is the only entry point
87
- that can deliberately return a bounded prefix instead of throwing.
78
+ Prefer a text sink:
88
79
 
89
80
  ```ts
90
- import { convert } from 'purifai';
81
+ element.textContent = toText(untrustedHtml);
82
+ ```
91
83
 
92
- const result = convert(largeHtml, {
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
- result.text;
98
- result.truncatedBy; // 'output' or null
99
- result.scanComplete; // false after truncation
100
- result.consumedInputCodeUnits;
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
- Truncation is explicit, deterministic, and never emits half of a UTF-16
106
- surrogate pair. When multiple limits meet at the same point, the first observed
107
- limit is reported.
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
- ### `createTextTransform(options?)`
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
- Returns a native `TransformStream<string, string>` with a `result` promise. The
112
- stream uses the same state machine and produces exactly the same joined text as
113
- `toText`, regardless of chunk boundaries.
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
- const { done, value } = await reader.read();
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 on a breached limit. Output may already have
137
- been enqueued when `readable` and `transform.result` reject, so discard partial
138
- output unless your application has deliberately defined it as useful. Purifai
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
- ### `escapeHtmlText(text)`
142
+ ## Bounded conversion
142
143
 
143
- Encodes `&`, `<`, `>`, `"`, and `'` for an HTML text node. It is lossless and is
144
- for plain text—including `toText` output—when the only available sink is
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 { PurifaiLimitError, toText } from 'purifai';
148
+ import { convert } from 'purifai';
153
149
 
154
- try {
155
- toText(html, { limits: { input: 10_000 } });
156
- } catch (error) {
157
- if (error instanceof PurifaiLimitError) {
158
- console.error(error.kind, error.limit, error.observed);
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
- ## Options and defaults
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 silently
166
- guess around configuration mistakes.
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 newlines/lists/tables, or normalized single-space text |
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/attribute code units |
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 while scanning, before unbounded caller-controlled
181
- state can accumulate. Values measure JavaScript UTF-16 code units, not encoded
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
- ## Link policy
186
+ ### Display URL policy
185
187
 
186
- `label-and-url` emits a destination as display text, never as an active link. It
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`. Control characters, whitespace-split schemes,
189
- protocol-relative inputs, leading backslashes, credentials, unsupported schemes,
190
- and invalid URLs are omitted while their visible label remains.
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
- The returned URL string is still only text. Do not move it into `href` without a
193
- separate URL-policy decision at that sink.
194
+ ## Extraction policy
194
195
 
195
- ## Extraction fidelity
196
-
197
- Purifai intentionally removes source and non-reader bodies including `script`,
198
- `style`, `template`, `iframe`, `noscript`, `noembed`, `noframes`, `svg`, and
199
- `math`. It preserves selected fallback/form text, decodes `textarea`, preserves
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. Simple rows and cells are represented with tabs and
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 exact readability/body-removal fixtures,
224
- isolated warm median and p95 one-shot latency, and fresh-process peak RSS. The
225
- throughput path gives every package the same materialized string; the memory path
226
- lets Purifai consume lazy 16,384-code-unit chunks because streaming ingestion is
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
- On the recorded Apple M1 / Node 24 run, Purifai passed all 11 category gates: all
230
- readability and body-removal fixtures, lower hostile-input p95 than
231
- `html-to-text` on four hostile corpora, and lower streaming peak RSS on all five
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
- See the [complete methodology, raw-result link, and tables](docs/benchmarks/v3.md).
236
- Reproduce it with `pnpm run bench`; re-check the saved gates with
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
- ## Size and runtime matrix
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
- The complete minified ESM runtime—including the full 2,231-name WHATWG entity
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 release matrix exercises the same packed ESM artifact:
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
- | Runtime | Required release coverage |
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 consumers |
252
- | Deno | ESM consumer |
253
- | Cloudflare Workers | real `workerd`, without Node compatibility |
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
- Browser qualification also reparses escaped output in a real DOM with a working
257
- positive control. This validates the documented sinks; it is not a universal
258
- claim about every output context.
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. Contributor setup and the full verification commands
264
- are in [CONTRIBUTING.md](CONTRIBUTING.md).
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)