@v1nvn/readability 0.44.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 (132) hide show
  1. package/README.md +3 -3
  2. package/package.json +7 -9
  3. package/src/cli.js +99 -0
  4. package/src/cli.ts +134 -0
  5. package/src/config.js +29 -0
  6. package/src/config.ts +42 -0
  7. package/src/dev.js +55 -0
  8. package/src/dev.ts +77 -0
  9. package/src/errors.js +16 -0
  10. package/src/errors.ts +19 -0
  11. package/src/handler.js +4 -0
  12. package/src/handler.ts +8 -0
  13. package/src/host-sampling.js +57 -0
  14. package/src/host-sampling.ts +86 -0
  15. package/src/index.js +23 -0
  16. package/src/index.ts +27 -0
  17. package/src/logger.js +2 -0
  18. package/src/logger.ts +3 -0
  19. package/src/output/format.js +92 -0
  20. package/src/output/format.ts +132 -0
  21. package/src/pipeline/cell-text.js +51 -0
  22. package/src/pipeline/cell-text.ts +56 -0
  23. package/src/pipeline/context.js +1 -0
  24. package/src/pipeline/context.ts +88 -0
  25. package/src/pipeline/dom.js +16 -0
  26. package/src/pipeline/dom.ts +23 -0
  27. package/src/pipeline/footnotes.js +148 -0
  28. package/src/pipeline/footnotes.ts +181 -0
  29. package/src/pipeline/math.js +113 -0
  30. package/src/pipeline/math.ts +130 -0
  31. package/src/pipeline/normalize.js +507 -0
  32. package/src/pipeline/normalize.ts +594 -0
  33. package/src/pipeline/readability.js +10 -0
  34. package/src/pipeline/readability.ts +36 -0
  35. package/src/pipeline/resolver.js +113 -0
  36. package/src/pipeline/resolver.ts +143 -0
  37. package/src/pipeline/sanitize.js +26 -0
  38. package/src/pipeline/sanitize.ts +42 -0
  39. package/src/pipeline/tables.js +239 -0
  40. package/src/pipeline/tables.ts +286 -0
  41. package/src/pipeline/turndown.js +153 -0
  42. package/src/pipeline/turndown.ts +191 -0
  43. package/src/pipeline/urls.js +14 -0
  44. package/src/pipeline/urls.ts +13 -0
  45. package/src/policy/chunk.js +365 -0
  46. package/src/policy/chunk.ts +482 -0
  47. package/src/policy/diagnostics.js +53 -0
  48. package/src/policy/diagnostics.ts +96 -0
  49. package/src/policy/explain.js +104 -0
  50. package/src/policy/explain.ts +182 -0
  51. package/src/policy/fallback.js +68 -0
  52. package/src/policy/fallback.ts +110 -0
  53. package/src/policy/gating.js +90 -0
  54. package/src/policy/gating.ts +106 -0
  55. package/src/policy/grid-detector.js +207 -0
  56. package/src/policy/grid-detector.ts +313 -0
  57. package/src/policy/identifiers.js +62 -0
  58. package/src/policy/identifiers.ts +75 -0
  59. package/src/policy/images.js +49 -0
  60. package/src/policy/images.ts +64 -0
  61. package/src/policy/list-detector.js +232 -0
  62. package/src/policy/list-detector.ts +308 -0
  63. package/src/policy/lost-signal.js +44 -0
  64. package/src/policy/lost-signal.ts +69 -0
  65. package/src/policy/markdown.js +58 -0
  66. package/src/policy/markdown.ts +76 -0
  67. package/src/policy/metadata.js +216 -0
  68. package/src/policy/metadata.ts +312 -0
  69. package/src/policy/outline-chains.js +165 -0
  70. package/src/policy/outline-chains.ts +215 -0
  71. package/src/policy/outline.js +71 -0
  72. package/src/policy/outline.ts +89 -0
  73. package/src/policy/pagination.js +79 -0
  74. package/src/policy/pagination.ts +104 -0
  75. package/src/policy/presets.js +100 -0
  76. package/src/policy/presets.ts +142 -0
  77. package/src/policy/section.js +111 -0
  78. package/src/policy/section.ts +127 -0
  79. package/src/policy/selector-lint.js +143 -0
  80. package/src/policy/selector-lint.ts +186 -0
  81. package/src/policy/sibling-scan.js +92 -0
  82. package/src/policy/sibling-scan.ts +99 -0
  83. package/src/policy/text.js +18 -0
  84. package/src/policy/text.ts +36 -0
  85. package/src/policy/truncate.js +45 -0
  86. package/src/policy/truncate.ts +59 -0
  87. package/src/preset-cache.js +165 -0
  88. package/src/preset-cache.ts +221 -0
  89. package/src/resources.js +227 -0
  90. package/src/resources.ts +317 -0
  91. package/src/sampling.js +45 -0
  92. package/src/sampling.ts +74 -0
  93. package/src/server.js +73 -0
  94. package/src/server.ts +85 -0
  95. package/src/tools/chunk_text.js +40 -0
  96. package/src/tools/chunk_text.ts +65 -0
  97. package/src/tools/explain.js +200 -0
  98. package/src/tools/explain.ts +299 -0
  99. package/src/tools/extract.js +263 -0
  100. package/src/tools/extract.ts +368 -0
  101. package/src/tools/extract_grid.js +56 -0
  102. package/src/tools/extract_grid.ts +90 -0
  103. package/src/tools/extract_links.js +98 -0
  104. package/src/tools/extract_links.ts +138 -0
  105. package/src/tools/extract_list.js +66 -0
  106. package/src/tools/extract_list.ts +97 -0
  107. package/src/tools/extract_metadata.js +67 -0
  108. package/src/tools/extract_metadata.ts +102 -0
  109. package/src/tools/extract_section.js +56 -0
  110. package/src/tools/extract_section.ts +84 -0
  111. package/src/tools/extract_tables.js +59 -0
  112. package/src/tools/extract_tables.ts +101 -0
  113. package/src/tools/html-source.js +30 -0
  114. package/src/tools/html-source.ts +40 -0
  115. package/src/tools/html_to_markdown.js +113 -0
  116. package/src/tools/html_to_markdown.ts +180 -0
  117. package/src/tools/outline.js +47 -0
  118. package/src/tools/outline.ts +77 -0
  119. package/src/tools/output-schema.js +504 -0
  120. package/src/tools/output-schema.ts +715 -0
  121. package/src/tools/schemas.js +288 -0
  122. package/src/tools/schemas.ts +442 -0
  123. package/src/tools/suggest-preset.js +504 -0
  124. package/src/tools/suggest-preset.ts +708 -0
  125. package/src/types/jsdom.d.ts +15 -0
  126. package/src/types/turndown-plugin-gfm.d.ts +11 -0
  127. package/dist/assets/cli-Dfjg7dOQ.js +0 -76
  128. package/dist/assets/cli-Dfjg7dOQ.js.map +0 -1
  129. package/dist/assets/extract-CdDp-KwX.js +0 -2880
  130. package/dist/assets/extract-CdDp-KwX.js.map +0 -1
  131. package/dist/index.js +0 -2267
  132. package/dist/index.js.map +0 -1
@@ -0,0 +1,365 @@
1
+ // Token-bounded chunking of extracted markdown for RAG/embedding. Chunk
2
+ // `tokenCount` uses the shared chars/4 estimator (policy/text.ts), keeping it
3
+ // directly comparable to `metadata.tokenEstimate`. This is the server-side
4
+ // chunking path for large documents; transport-level streaming was rejected —
5
+ // MCP tool-result progress isn't universally host-supported, and this option
6
+ // already keeps a multi-MB page from returning as one payload.
7
+ import { hardSplitLines, headingText, parseBlocks } from "./markdown.js";
8
+ import { estimateTokens } from "./text.js";
9
+ const HEADING_FIRST_LINE = /^#{1,6}\s/;
10
+ function splitBlocks(markdown) {
11
+ const parts = markdown.split(/\n{2,}/);
12
+ const blocks = [];
13
+ let heading = '';
14
+ for (const part of parts) {
15
+ const text = part.trim();
16
+ if (!text) {
17
+ continue;
18
+ }
19
+ if (HEADING_FIRST_LINE.test(text)) {
20
+ heading = headingText(text);
21
+ }
22
+ blocks.push({ headingContext: heading, text });
23
+ }
24
+ return blocks;
25
+ }
26
+ // Char strategy may break a code block — the semantic strategy avoids that.
27
+ function splitOversizedBlock(block, maxChars) {
28
+ return hardSplitLines(block.text, maxChars).map(text => ({
29
+ headingContext: block.headingContext,
30
+ text,
31
+ }));
32
+ }
33
+ function toUnits(blocks, maxChars) {
34
+ const units = [];
35
+ for (const block of blocks) {
36
+ if (block.text.length <= maxChars) {
37
+ units.push(block);
38
+ }
39
+ else {
40
+ units.push(...splitOversizedBlock(block, maxChars));
41
+ }
42
+ }
43
+ return units;
44
+ }
45
+ function chunkMarkdownChar(markdown, options) {
46
+ if (!markdown) {
47
+ return [];
48
+ }
49
+ const maxTokens = Math.max(1, Math.floor(options.maxTokens));
50
+ const maxChars = maxTokens * 4;
51
+ const overlapTokens = Math.max(0, Math.floor(options.overlap));
52
+ // Overlap must be strictly less than maxChars — otherwise a chunk's tail
53
+ // could consume the entire budget and we'd loop forever re-emitting it.
54
+ const overlapChars = Math.min(overlapTokens * 4, maxChars - 1);
55
+ const blocks = splitBlocks(markdown);
56
+ if (blocks.length === 0) {
57
+ return [];
58
+ }
59
+ const units = toUnits(blocks, maxChars);
60
+ if (units.length === 0) {
61
+ return [];
62
+ }
63
+ const chunks = [];
64
+ let i = 0;
65
+ let overlapText = '';
66
+ let overlapHeading = '';
67
+ while (i < units.length) {
68
+ const spans = [];
69
+ let chunkText = '';
70
+ if (overlapText) {
71
+ chunkText = overlapText;
72
+ spans.push({
73
+ end: overlapText.length,
74
+ headingContext: overlapHeading,
75
+ start: 0,
76
+ });
77
+ }
78
+ // Force at least one new unit per chunk so the loop terminates. If carrying
79
+ // the overlap would push the first new unit past maxChars, drop the overlap
80
+ // for this chunk (the unit was pre-split to <= maxChars so it always fits alone).
81
+ const firstUnit = units[i];
82
+ const sepLen = chunkText ? 2 : 0;
83
+ if (chunkText &&
84
+ chunkText.length + sepLen + firstUnit.text.length > maxChars) {
85
+ chunkText = '';
86
+ spans.length = 0;
87
+ }
88
+ {
89
+ const sep = chunkText ? '\n\n' : '';
90
+ const start = chunkText.length + sep.length;
91
+ chunkText = chunkText + sep + firstUnit.text;
92
+ spans.push({
93
+ end: chunkText.length,
94
+ headingContext: firstUnit.headingContext,
95
+ start,
96
+ });
97
+ i += 1;
98
+ }
99
+ while (i < units.length) {
100
+ const unit = units[i];
101
+ const candidate = `${chunkText}\n\n${unit.text}`;
102
+ if (candidate.length > maxChars) {
103
+ break;
104
+ }
105
+ const start = chunkText.length + 2;
106
+ chunkText = candidate;
107
+ spans.push({
108
+ end: chunkText.length,
109
+ headingContext: unit.headingContext,
110
+ start,
111
+ });
112
+ i += 1;
113
+ }
114
+ const text = chunkText.trim();
115
+ if (text) {
116
+ chunks.push({
117
+ headingContext: spans[0]?.headingContext ?? '',
118
+ index: chunks.length,
119
+ text,
120
+ tokenCount: estimateTokens(text).tokenEstimate,
121
+ });
122
+ }
123
+ if (overlapChars > 0 && i < units.length) {
124
+ const overlapStart = Math.max(0, chunkText.length - overlapChars);
125
+ const carrier = spans.find(span => overlapStart >= span.start && overlapStart < span.end) ?? spans.find(span => span.start >= overlapStart);
126
+ overlapHeading = carrier?.headingContext ?? '';
127
+ overlapText = chunkText.slice(overlapStart).replace(/^\n+/, '');
128
+ }
129
+ else {
130
+ overlapText = '';
131
+ overlapHeading = '';
132
+ }
133
+ }
134
+ return chunks;
135
+ }
136
+ function parseSemanticUnits(markdown) {
137
+ const units = [];
138
+ const stack = [];
139
+ function context() {
140
+ return stack.map(h => h.text).join(' > ');
141
+ }
142
+ for (const block of parseBlocks(markdown)) {
143
+ const text = markdown.slice(block.start, block.end);
144
+ if (block.kind === 'heading') {
145
+ while (stack.length > 0 && stack[stack.length - 1].level >= block.depth) {
146
+ stack.pop();
147
+ }
148
+ stack.push({ level: block.depth, text: headingText(text) });
149
+ units.push({ headingContext: context(), kind: 'heading', text });
150
+ continue;
151
+ }
152
+ units.push({
153
+ headingContext: context(),
154
+ kind: block.kind === 'code' ? 'code' : 'text',
155
+ text,
156
+ });
157
+ }
158
+ return units;
159
+ }
160
+ // A section = a heading unit plus its following non-heading units until the
161
+ // next heading. Pre-first-heading content forms its own section with an empty
162
+ // hierarchy path.
163
+ function groupSections(units) {
164
+ const sections = [];
165
+ let current = [];
166
+ let currentContext = '';
167
+ function flush() {
168
+ if (current.length > 0) {
169
+ sections.push({ headingContext: currentContext, units: current });
170
+ current = [];
171
+ }
172
+ }
173
+ for (const unit of units) {
174
+ if (unit.kind === 'heading') {
175
+ flush();
176
+ current = [unit];
177
+ currentContext = unit.headingContext;
178
+ }
179
+ else {
180
+ if (current.length === 0) {
181
+ currentContext = unit.headingContext;
182
+ }
183
+ current.push(unit);
184
+ }
185
+ }
186
+ flush();
187
+ return sections;
188
+ }
189
+ function joinedLength(units) {
190
+ if (units.length === 0) {
191
+ return 0;
192
+ }
193
+ let total = units[0].text.length;
194
+ for (let i = 1; i < units.length; i++) {
195
+ total += 2 + units[i].text.length;
196
+ }
197
+ return total;
198
+ }
199
+ // Hard-split a text unit that alone exceeds the budget — line-aware first, then
200
+ // hard-split any oversized line. Only ever applied to paragraphs; code blocks
201
+ // are never passed through here.
202
+ function splitOversizedTextUnit(unit, maxChars) {
203
+ return hardSplitLines(unit.text, maxChars).map(text => ({
204
+ headingContext: unit.headingContext,
205
+ kind: 'text',
206
+ text,
207
+ }));
208
+ }
209
+ // Pack sections into chunk groups <= maxChars, preferring heading boundaries.
210
+ // A section that fits is never split; an oversized section is split into
211
+ // sub-chunks by body unit (paragraphs may split; code blocks never do).
212
+ function buildBaseGroups(sections, maxChars) {
213
+ const groups = [];
214
+ let current = [];
215
+ let currentLen = 0;
216
+ function emit() {
217
+ if (current.length > 0) {
218
+ groups.push(current);
219
+ current = [];
220
+ currentLen = 0;
221
+ }
222
+ }
223
+ function append(unit) {
224
+ const sep = current.length > 0 ? 2 : 0;
225
+ current.push(unit);
226
+ currentLen += sep + unit.text.length;
227
+ }
228
+ for (const section of sections) {
229
+ const sectionLen = joinedLength(section.units);
230
+ const sep = current.length > 0 ? 2 : 0;
231
+ if (currentLen + sep + sectionLen <= maxChars) {
232
+ for (const unit of section.units) {
233
+ append(unit);
234
+ }
235
+ continue;
236
+ }
237
+ // Doesn't fit merged — close the current group and retry the section alone
238
+ // so it has first dibs on the next group (it may still merge with a later
239
+ // section that fits behind it).
240
+ emit();
241
+ if (sectionLen <= maxChars) {
242
+ for (const unit of section.units) {
243
+ append(unit);
244
+ }
245
+ continue;
246
+ }
247
+ // Oversized section: pack body units under the heading; split oversized
248
+ // paragraphs, emit oversized code blocks whole.
249
+ const [headingUnit, ...body] = section.units;
250
+ current.push(headingUnit);
251
+ currentLen = headingUnit.text.length;
252
+ for (const unit of body) {
253
+ if (unit.kind === 'code' && unit.text.length > maxChars) {
254
+ // Semantic never splits a code fence: a fence cut mid-way is
255
+ // unrecoverable, so the block is emitted as its own chunk even though
256
+ // it exceeds the token budget. Deliberate tradeoff vs. the char cap.
257
+ emit();
258
+ groups.push([unit]);
259
+ continue;
260
+ }
261
+ const sepNow = current.length > 0 ? 2 : 0;
262
+ if (unit.text.length <= maxChars &&
263
+ currentLen + sepNow + unit.text.length <= maxChars) {
264
+ append(unit);
265
+ continue;
266
+ }
267
+ const pieces = unit.text.length > maxChars
268
+ ? splitOversizedTextUnit(unit, maxChars)
269
+ : [unit];
270
+ for (const piece of pieces) {
271
+ const pieceSep = current.length > 0 ? 2 : 0;
272
+ if (current.length > 0 &&
273
+ currentLen + pieceSep + piece.text.length > maxChars) {
274
+ emit();
275
+ }
276
+ append(piece);
277
+ }
278
+ }
279
+ // Leave the trailing sub-chunk in `current` so a following section can
280
+ // merge into it if it fits.
281
+ }
282
+ emit();
283
+ return groups;
284
+ }
285
+ // Carry the trailing text (non-code) units from the previous group as the
286
+ // prefix of the next, up to overlapChars. Never carry into or out of a code
287
+ // block: a trailing code run is skipped, then contiguous text units before it
288
+ // are carried (stop at the first heading or code block encountered).
289
+ function applySemanticOverlap(groups, overlapChars) {
290
+ const result = [];
291
+ for (let i = 0; i < groups.length; i++) {
292
+ const group = groups[i];
293
+ const carrier = [];
294
+ if (i > 0 && overlapChars > 0) {
295
+ const prev = groups[i - 1];
296
+ let end = prev.length;
297
+ while (end > 0 && prev[end - 1].kind === 'code') {
298
+ end -= 1;
299
+ }
300
+ let carrierLen = 0;
301
+ for (let j = end - 1; j >= 0; j--) {
302
+ const unit = prev[j];
303
+ if (unit.kind !== 'text') {
304
+ break;
305
+ }
306
+ const sep = carrier.length > 0 ? 2 : 0;
307
+ if (carrierLen + sep + unit.text.length > overlapChars) {
308
+ break;
309
+ }
310
+ carrier.unshift(unit);
311
+ carrierLen += sep + unit.text.length;
312
+ }
313
+ }
314
+ const units = carrier.length === 0 ? group : [...carrier, ...group];
315
+ result.push({
316
+ // The chunk's heading context follows its first non-carrier unit, so a
317
+ // heading-leading chunk keeps its heading's path even with overlap.
318
+ headingContext: group[0]?.headingContext ?? '',
319
+ units,
320
+ });
321
+ }
322
+ return result;
323
+ }
324
+ function chunkMarkdownSemantic(markdown, options) {
325
+ if (!markdown) {
326
+ return [];
327
+ }
328
+ const maxTokens = Math.max(1, Math.floor(options.maxTokens));
329
+ const maxChars = maxTokens * 4;
330
+ const overlapTokens = Math.max(0, Math.floor(options.overlap));
331
+ const overlapChars = Math.min(overlapTokens * 4, maxChars - 1);
332
+ const units = parseSemanticUnits(markdown);
333
+ if (units.length === 0) {
334
+ return [];
335
+ }
336
+ const sections = groupSections(units);
337
+ const baseGroups = buildBaseGroups(sections, maxChars);
338
+ const groups = applySemanticOverlap(baseGroups, overlapChars);
339
+ const chunks = [];
340
+ for (const group of groups) {
341
+ if (group.units.length === 0) {
342
+ continue;
343
+ }
344
+ const text = group.units
345
+ .map(unit => unit.text)
346
+ .join('\n\n')
347
+ .trim();
348
+ if (!text) {
349
+ continue;
350
+ }
351
+ chunks.push({
352
+ headingContext: group.headingContext,
353
+ index: chunks.length,
354
+ text,
355
+ tokenCount: estimateTokens(text).tokenEstimate,
356
+ });
357
+ }
358
+ return chunks;
359
+ }
360
+ export function chunkMarkdown(markdown, options) {
361
+ if (options.strategy === 'semantic') {
362
+ return chunkMarkdownSemantic(markdown, options);
363
+ }
364
+ return chunkMarkdownChar(markdown, options);
365
+ }