@v1nvn/readability 0.45.0 → 0.45.1

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.
Files changed (64) hide show
  1. package/README.md +3 -3
  2. package/package.json +4 -4
  3. package/src/cli.js +99 -0
  4. package/src/config.js +29 -0
  5. package/src/dev.js +55 -0
  6. package/src/errors.js +16 -0
  7. package/src/handler.js +4 -0
  8. package/src/host-sampling.js +57 -0
  9. package/src/index.js +23 -0
  10. package/src/index.ts +0 -0
  11. package/src/logger.js +2 -0
  12. package/src/output/format.js +92 -0
  13. package/src/pipeline/cell-text.js +51 -0
  14. package/src/pipeline/context.js +1 -0
  15. package/src/pipeline/dom.js +16 -0
  16. package/src/pipeline/footnotes.js +148 -0
  17. package/src/pipeline/math.js +113 -0
  18. package/src/pipeline/normalize.js +507 -0
  19. package/src/pipeline/readability.js +10 -0
  20. package/src/pipeline/resolver.js +113 -0
  21. package/src/pipeline/sanitize.js +26 -0
  22. package/src/pipeline/tables.js +239 -0
  23. package/src/pipeline/turndown.js +153 -0
  24. package/src/pipeline/urls.js +14 -0
  25. package/src/policy/chunk.js +365 -0
  26. package/src/policy/diagnostics.js +53 -0
  27. package/src/policy/explain.js +104 -0
  28. package/src/policy/fallback.js +68 -0
  29. package/src/policy/gating.js +90 -0
  30. package/src/policy/grid-detector.js +207 -0
  31. package/src/policy/identifiers.js +62 -0
  32. package/src/policy/images.js +49 -0
  33. package/src/policy/list-detector.js +232 -0
  34. package/src/policy/lost-signal.js +44 -0
  35. package/src/policy/markdown.js +58 -0
  36. package/src/policy/metadata.js +216 -0
  37. package/src/policy/outline-chains.js +165 -0
  38. package/src/policy/outline.js +71 -0
  39. package/src/policy/pagination.js +79 -0
  40. package/src/policy/presets.js +100 -0
  41. package/src/policy/section.js +111 -0
  42. package/src/policy/selector-lint.js +143 -0
  43. package/src/policy/sibling-scan.js +92 -0
  44. package/src/policy/text.js +18 -0
  45. package/src/policy/truncate.js +45 -0
  46. package/src/preset-cache.js +165 -0
  47. package/src/resources.js +227 -0
  48. package/src/sampling.js +45 -0
  49. package/src/server.js +73 -0
  50. package/src/tools/chunk_text.js +40 -0
  51. package/src/tools/explain.js +200 -0
  52. package/src/tools/extract.js +263 -0
  53. package/src/tools/extract_grid.js +56 -0
  54. package/src/tools/extract_links.js +98 -0
  55. package/src/tools/extract_list.js +66 -0
  56. package/src/tools/extract_metadata.js +67 -0
  57. package/src/tools/extract_section.js +56 -0
  58. package/src/tools/extract_tables.js +59 -0
  59. package/src/tools/html-source.js +30 -0
  60. package/src/tools/html_to_markdown.js +113 -0
  61. package/src/tools/outline.js +47 -0
  62. package/src/tools/output-schema.js +504 -0
  63. package/src/tools/schemas.js +288 -0
  64. package/src/tools/suggest-preset.js +504 -0
@@ -0,0 +1,288 @@
1
+ import { z } from 'zod';
2
+ export const formatSchema = z.enum(['html', 'json', 'markdown', 'text']);
3
+ export const metadataModeSchema = z.enum(['json', 'none', 'yaml']);
4
+ export const extractionSchema = z.enum([
5
+ 'aggressive',
6
+ 'balanced',
7
+ 'conservative',
8
+ ]);
9
+ export const headingStyleSchema = z.enum(['atx', 'setext']);
10
+ export const codeBlockStyleSchema = z.enum(['fenced', 'indented']);
11
+ export const imageModeSchema = z.enum([
12
+ 'drop',
13
+ 'keep',
14
+ 'reference',
15
+ 'src-only',
16
+ ]);
17
+ export const tableFormatSchema = z.enum(['csv', 'gfm', 'json']);
18
+ export const localPathField = z
19
+ .string()
20
+ .describe('Absolute or relative path to a file holding the already-rendered (post-JavaScript) HTML to process, e.g. `document.documentElement.outerHTML` written to disk by a browser/devtools capture. Read by the server so the page bytes never enter the model context — only this path string does. Resolved relative to the server process working directory; the server makes no outbound requests.');
21
+ export const selectorsSchema = z
22
+ .object({
23
+ include: z
24
+ .string()
25
+ .optional()
26
+ .describe('CSS selector restricting extraction to a matching subtree (e.g. "main", "article", ".post"). The first match replaces the document body before processing.'),
27
+ exclude: z
28
+ .array(z.string())
29
+ .optional()
30
+ .describe('CSS selectors for boilerplate to remove before extraction (e.g. ["nav", "footer", "[role=banner]"]).'),
31
+ })
32
+ .optional()
33
+ .describe('Scope the extracted/converted content by CSS selector before processing.');
34
+ export const readabilityOverridesSchema = z
35
+ .record(z.string(), z.unknown())
36
+ .optional()
37
+ .describe('Escape hatch: a record spread verbatim into the Readability options. Unstable and unvalidated; overrides the extraction/keepClasses/maxNodes/minArticleLength knobs.');
38
+ export const chunkStrategySchema = z.enum(['char', 'semantic']);
39
+ export const chunkOptionsSchema = z
40
+ .object({
41
+ maxTokens: z
42
+ .number()
43
+ .int()
44
+ .min(1)
45
+ .describe('Per-chunk token budget. Each chunk.text is sized so Math.round(text.length/4) stays within this bound (hard cap; oversized blocks are split by line, then hard-split).'),
46
+ overlap: z
47
+ .number()
48
+ .int()
49
+ .min(0)
50
+ .describe('Tokens to overlap between consecutive chunks (>=0). The trailing overlapChars of chunk N becomes the leading context of chunk N+1, preserving cross-chunk coherence at a cost of redundant tokens.')
51
+ .default(0),
52
+ strategy: chunkStrategySchema
53
+ .describe("Chunking strategy. 'semantic' (default) breaks on heading/section boundaries and never splits a fenced code block; 'char' greedily groups blank-line-separated blocks under a chars/4 token budget (may split a code block).")
54
+ .default('semantic'),
55
+ })
56
+ .describe('Token-bounded chunking options for splitting the extracted markdown into RAG/embedding-ready slices.');
57
+ export const turndownOptionsShape = {
58
+ debug: z
59
+ .boolean()
60
+ .describe('Emit per-stage timings (normalize, readability, sanitize, turndown, metadata) under diagnostics.trace. Debug-only — leaves trace absent by default.')
61
+ .default(false),
62
+ cleanChrome: z
63
+ .boolean()
64
+ .describe('Strip browser chrome (scrollbars, consent/cookie banners, fixed nav and overlays) before conversion. These elements poison Readability density scoring and clutter fragment output.')
65
+ .default(true),
66
+ codeBlockStyle: codeBlockStyleSchema
67
+ .describe("Markdown code-block style: 'fenced' (triple backticks) or 'indented' (four-space).")
68
+ .default('fenced'),
69
+ format: formatSchema
70
+ .describe("Returned payload format: 'markdown' (default), 'html', 'text', or 'json' (emits {metadata, content, diagnostics}).")
71
+ .default('markdown'),
72
+ gfm: z
73
+ .boolean()
74
+ .describe('Enable GitHub-Flavored Markdown: tables, strikethrough, and task lists.')
75
+ .default(true),
76
+ headingStyle: headingStyleSchema
77
+ .describe("Markdown heading style: 'atx' (#) or 'setext' (underlining with = / -).")
78
+ .default('atx'),
79
+ images: imageModeSchema
80
+ .describe("Image handling: 'keep' (inline ![alt](url)), 'drop', 'src-only' (bare URL text), or 'reference' (link-reference style).")
81
+ .default('keep'),
82
+ maxChars: z
83
+ .number()
84
+ .int()
85
+ .min(0)
86
+ .describe('Truncate markdown/text output at a block boundary; never splits a fenced code block. Ignored for html/json formats.')
87
+ .optional(),
88
+ metadataMode: metadataModeSchema
89
+ .describe("Prepend a metadata block to the markdown/text payload: 'none' (default), 'yaml', or 'json'.")
90
+ .default('none'),
91
+ sanitize: z
92
+ .boolean()
93
+ .describe('Run DOMPurify over the extracted/fragment HTML before conversion (strips scripts, event handlers, and iframes).')
94
+ .default(true),
95
+ tables: tableFormatSchema
96
+ .describe('Render <table> elements via a rowspan/colspan-aware matrix: "gfm" (default native GFM table), "csv" (RFC-4180-ish code block), or "json" (array of row objects keyed by the header). When unset, tables pass through Turndown\'s native rule unchanged.')
97
+ .optional(),
98
+ // zod v4 renamed `z.string().url()` to `z.url()`.
99
+ baseUrl: z
100
+ .url()
101
+ .describe('Base URL for absolutizing relative links and images. NEVER fetched — origin context only.')
102
+ .optional(),
103
+ wordsPerMinute: z
104
+ .number()
105
+ .int()
106
+ .min(1)
107
+ .describe('Reading speed (words per minute) used to compute metadata.readingTimeMin.')
108
+ .default(200),
109
+ };
110
+ export const extractInputShape = {
111
+ localPath: localPathField,
112
+ ...turndownOptionsShape,
113
+ cache: z
114
+ .boolean()
115
+ .describe('When true, cache the result keyed by a normalized hash of the HTML plus the output-affecting options; repeat calls with the same normalized HTML hit the cache and report diagnostics.cache (hit/miss + both hashes). The cache is in-memory and bounded; entries are also exposed as readability://page/{hash} resources.')
116
+ .default(false),
117
+ extraction: extractionSchema
118
+ .describe("Readability scoring aggressiveness: 'balanced' (default), 'aggressive', or 'conservative'. Maps to Readability's scorer knobs.")
119
+ .default('balanced'),
120
+ keepClasses: z
121
+ .boolean()
122
+ .describe('Retain all CSS classes on extracted nodes. Defaults false, which strips non-language classes.')
123
+ .default(false),
124
+ maxNodes: z
125
+ .number()
126
+ .int()
127
+ .min(0)
128
+ .describe('Hard cap on elements parsed (Readability maxElemsToParse). Safety/perf guard for very large documents.')
129
+ .optional(),
130
+ minArticleLength: z
131
+ .number()
132
+ .int()
133
+ .min(0)
134
+ .describe('Minimum article character length below which extraction falls back to the selector cascade (Readability charThreshold).')
135
+ .optional(),
136
+ readabilityOverrides: readabilityOverridesSchema,
137
+ selectors: selectorsSchema,
138
+ chunk: chunkOptionsSchema
139
+ .optional()
140
+ .describe('Split the extracted markdown into token-bounded chunks (RAG/embedding-ready). When set, structuredContent.chunks is populated. Only applies to format:"markdown" | "text"; HTML/JSON payloads carry no markdown body to slice and leave chunks unset.'),
141
+ imageInventory: z
142
+ .boolean()
143
+ .describe('Emit structuredContent.images: a list of {src (absolute, resolved), alt, width?, height?, caption} for every <img> in the extracted article. Independent of the `images` option (which governs inline rendering). Placeholders are skipped.')
144
+ .default(false),
145
+ };
146
+ export const extractInputSchema = z.object(extractInputShape);
147
+ export const htmlToMarkdownInputShape = {
148
+ localPath: localPathField,
149
+ ...turndownOptionsShape,
150
+ selectors: selectorsSchema,
151
+ };
152
+ export const htmlToMarkdownInputSchema = z.object(htmlToMarkdownInputShape);
153
+ export const outlineInputShape = {
154
+ localPath: localPathField,
155
+ baseUrl: z
156
+ .url()
157
+ .describe('Base URL, carried through to metadata.baseUrl and used to absolutize links. NEVER fetched — origin context only.')
158
+ .optional(),
159
+ selectors: selectorsSchema,
160
+ };
161
+ export const outlineInputSchema = z.object(outlineInputShape);
162
+ export const extractMetadataInputShape = {
163
+ localPath: localPathField,
164
+ baseUrl: z
165
+ .url()
166
+ .describe('Base URL, carried through to metadata.baseUrl and used to absolutize links. NEVER fetched — origin context only.')
167
+ .optional(),
168
+ };
169
+ export const extractMetadataInputSchema = z.object(extractMetadataInputShape);
170
+ export const extractLinksInputShape = {
171
+ localPath: localPathField,
172
+ baseUrl: z
173
+ .url()
174
+ .describe('Base URL for absolutizing relative hrefs and computing isExternal. NEVER fetched — origin context only.')
175
+ .optional(),
176
+ sameOriginOnly: z
177
+ .boolean()
178
+ .describe('Drop cross-origin links; keep same-origin, relative, and fragment links.')
179
+ .default(false),
180
+ selectors: selectorsSchema,
181
+ };
182
+ export const extractLinksInputSchema = z.object(extractLinksInputShape);
183
+ export const chunkTextInputShape = {
184
+ text: z
185
+ .string()
186
+ .describe('Already-extracted text to split (e.g. markdown from `extract` or any plain text). No HTML parsing or Readability scoring is applied — the input is chunked verbatim.'),
187
+ maxTokens: z
188
+ .number()
189
+ .int()
190
+ .min(1)
191
+ .describe('Per-chunk token budget. Each chunk.text is sized so Math.round(text.length/4) stays within this bound (hard cap; oversized blocks are split by line, then hard-split).')
192
+ .default(500),
193
+ overlap: z
194
+ .number()
195
+ .int()
196
+ .min(0)
197
+ .describe('Tokens to overlap between consecutive chunks (>=0). The trailing overlapChars of chunk N becomes the leading context of chunk N+1.')
198
+ .default(0),
199
+ strategy: chunkStrategySchema
200
+ .describe("Chunking strategy. 'semantic' (default) breaks on heading/section boundaries and never splits a fenced code block; 'char' greedily groups blank-line-separated blocks under a chars/4 token budget.")
201
+ .default('semantic'),
202
+ };
203
+ export const chunkTextInputSchema = z.object(chunkTextInputShape);
204
+ export const extractSectionInputShape = {
205
+ localPath: localPathField,
206
+ baseUrl: z
207
+ .url()
208
+ .describe('Base URL for absolutizing relative links and images. NEVER fetched — origin context only.')
209
+ .optional(),
210
+ selector: z
211
+ .string()
212
+ .describe('CSS selector scoping extraction to one subtree; passed straight through as selectors.include. Provide exactly one of selector/heading.')
213
+ .optional(),
214
+ heading: z
215
+ .string()
216
+ .describe('Heading text selecting one section; the section spans from this heading to the next same-or-higher-level heading. Case-insensitive; first match wins. Provide exactly one of selector/heading.')
217
+ .optional(),
218
+ };
219
+ export const extractSectionInputSchema = z
220
+ .object(extractSectionInputShape)
221
+ .superRefine((value, ctx) => {
222
+ const hasSelector = value.selector !== undefined;
223
+ const hasHeading = value.heading !== undefined;
224
+ if (hasSelector === hasHeading) {
225
+ ctx.addIssue({
226
+ code: 'custom',
227
+ message: 'Provide exactly one of `selector` or `heading` (both set or both unset is invalid).',
228
+ path: ['selector'],
229
+ });
230
+ }
231
+ });
232
+ export const extractTablesInputShape = {
233
+ localPath: localPathField,
234
+ baseUrl: z
235
+ .url()
236
+ .describe('Base URL, carried through to metadata.baseUrl. NEVER fetched — origin context only.')
237
+ .optional(),
238
+ format: tableFormatSchema
239
+ .describe('Output format for every table: "gfm" (default — native GFM table with a delimiter row), "csv" (RFC-4180-ish, quoted fields), or "json" (array of row objects keyed by the header row).')
240
+ .default('gfm'),
241
+ selectors: selectorsSchema,
242
+ };
243
+ export const extractTablesInputSchema = z.object(extractTablesInputShape);
244
+ export const extractListInputShape = {
245
+ localPath: localPathField,
246
+ baseUrl: z
247
+ .url()
248
+ .describe('Base URL for absolutizing item hrefs against. NEVER fetched — origin context only.')
249
+ .optional(),
250
+ selectors: selectorsSchema,
251
+ };
252
+ export const extractListInputSchema = z.object(extractListInputShape);
253
+ export const extractGridInputShape = {
254
+ localPath: localPathField,
255
+ baseUrl: z
256
+ .url()
257
+ .describe('Base URL, carried through to metadata.baseUrl. NEVER fetched — origin context only.')
258
+ .optional(),
259
+ format: tableFormatSchema
260
+ .describe('Output format for the grid: "gfm" (default — native GFM table with a delimiter row), "csv" (RFC-4180-ish, quoted fields), or "json" (array of row objects keyed by the header row).')
261
+ .default('gfm'),
262
+ selectors: selectorsSchema,
263
+ rowSelector: z
264
+ .string()
265
+ .describe('CSS selector for repeating row containers. When set WITH cellSelector, selector mode is used (no auto-detection). Example: \'[class*="estimate-row"]\'.')
266
+ .optional(),
267
+ cellSelector: z
268
+ .string()
269
+ .describe('CSS selector for cells within each row (scoped to the row subtree). Required together with rowSelector for selector mode. Example: \'[class*="cell"]\'.')
270
+ .optional(),
271
+ };
272
+ // rowSelector and cellSelector are both-or-neither: both set routes through
273
+ // selector mode, neither set routes through auto-detection. Exactly one set is
274
+ // ambiguous (which mode?), so it is rejected up front rather than silently
275
+ // falling back.
276
+ export const extractGridInputSchema = z
277
+ .object(extractGridInputShape)
278
+ .superRefine((value, ctx) => {
279
+ const hasRow = value.rowSelector !== undefined;
280
+ const hasCell = value.cellSelector !== undefined;
281
+ if (hasRow !== hasCell) {
282
+ ctx.addIssue({
283
+ code: 'custom',
284
+ message: 'Provide both `rowSelector` and `cellSelector` for selector mode, or neither for auto-detection (setting only one is invalid).',
285
+ path: ['rowSelector'],
286
+ });
287
+ }
288
+ });