@bluefields/extractor 0.2.0
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/LICENSE +661 -0
- package/dist/cache-key.d.ts +17 -0
- package/dist/cache-key.js +37 -0
- package/dist/cache-key.js.map +1 -0
- package/dist/guards.d.ts +100 -0
- package/dist/guards.js +276 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +281 -0
- package/dist/index.js.map +1 -0
- package/dist/js-shell-detect.d.ts +21 -0
- package/dist/js-shell-detect.js +113 -0
- package/dist/js-shell-detect.js.map +1 -0
- package/dist/links.d.ts +34 -0
- package/dist/links.js +154 -0
- package/dist/links.js.map +1 -0
- package/dist/model-picker.d.ts +36 -0
- package/dist/model-picker.js +88 -0
- package/dist/model-picker.js.map +1 -0
- package/dist/pdf.d.ts +42 -0
- package/dist/pdf.js +110 -0
- package/dist/pdf.js.map +1 -0
- package/dist/readability.d.ts +61 -0
- package/dist/readability.js +458 -0
- package/dist/readability.js.map +1 -0
- package/dist/sitemap.d.ts +74 -0
- package/dist/sitemap.js +199 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/sonnet.d.ts +64 -0
- package/dist/sonnet.js +157 -0
- package/dist/sonnet.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Mozilla Readability + Turndown pipeline (Review #6).
|
|
4
|
+
*
|
|
5
|
+
* Pulls the "main content" from an HTML page (sidebar/nav/ads stripped),
|
|
6
|
+
* then converts to GitHub-Flavored Markdown for downstream consumption.
|
|
7
|
+
*
|
|
8
|
+
* Readability is the same library Firefox Reader View uses — battle-tested
|
|
9
|
+
* across millions of articles. It's not perfect (especially on
|
|
10
|
+
* non-article pages like pricing tables or app dashboards), but it's the
|
|
11
|
+
* established baseline. Site-specific overrides land in host_extractors
|
|
12
|
+
* (chunk M14-17).
|
|
13
|
+
*
|
|
14
|
+
* Robustness contract (perf-extract): `extractMarkdown` NEVER throws and
|
|
15
|
+
* NEVER builds a DOM for input that could OOM or stack-overflow the
|
|
16
|
+
* process. Pathological inputs (oversized bodies, binary blobs, 2500+-deep
|
|
17
|
+
* nesting) degrade to a bounded tag-strip and report WHY via the
|
|
18
|
+
* `degraded` field — see guards.ts for the measured ceilings.
|
|
19
|
+
*/
|
|
20
|
+
import { Readability } from '@mozilla/readability';
|
|
21
|
+
import { JSDOM } from 'jsdom';
|
|
22
|
+
import TurndownService from 'turndown';
|
|
23
|
+
import { MAX_CAPTURED_TABLES, MAX_CAPTURE_TABLE_ROWS, MAX_DEGRADED_TEXT_CHARS, MAX_EXTRACT_HTML_CHARS, MAX_RECOVERY_CONTENT_CHARS, MAX_SAFE_NESTING_DEPTH, MAX_SINGLE_TABLE_CHARS, MAX_TABLE_CELLS, MAX_TABLE_COLS, MAX_TOTAL_CAPTURED_CHARS, estimateMaxNestingDepth, looksLikeBinary, stripScriptStyleBlocks, stripTagsLinear, } from './guards.js';
|
|
24
|
+
// v3 — adds input ceilings + typed degrade signaling + table-recovery caps
|
|
25
|
+
// (perf-extract). Output changes ONLY for pathological pages (oversized,
|
|
26
|
+
// binary, table-recovery beyond caps); bumping invalidates the extractions
|
|
27
|
+
// cache so any such page cached under v2 gets re-extracted under the new
|
|
28
|
+
// guards instead of serving a mixed-format row.
|
|
29
|
+
export const READABILITY_VERSION = 'readability-v3';
|
|
30
|
+
/**
|
|
31
|
+
* Extract main-content markdown from an HTML string.
|
|
32
|
+
*
|
|
33
|
+
* `url` is the source URL — used by Readability to resolve relative
|
|
34
|
+
* links + by Turndown for absolute-URL preservation. Pass the
|
|
35
|
+
* canonicalized URL (so the resolved hrefs in the markdown are stable
|
|
36
|
+
* across re-fetches).
|
|
37
|
+
*
|
|
38
|
+
* Never throws: binary input, oversized bodies (> MAX_EXTRACT_HTML_CHARS),
|
|
39
|
+
* pathologically deep markup, and any parse failure degrade to a bounded
|
|
40
|
+
* iterative plain-text strip instead of propagating — with the cause
|
|
41
|
+
* reported in `degraded`. The contract is "garbage in, best-effort text
|
|
42
|
+
* out" — callers can treat the result as always-present.
|
|
43
|
+
*/
|
|
44
|
+
export function extractMarkdown(html, url, opts = {}) {
|
|
45
|
+
// Table recovery is a win on data/reference pages but a liability on news
|
|
46
|
+
// articles (it re-adds non-content tables as false positives). Page-type
|
|
47
|
+
// routing toggles this; default on.
|
|
48
|
+
const recoverTables = opts.recoverTables ?? true;
|
|
49
|
+
// Binary sniff first — cheapest guard, and tag-stripping a PNG would
|
|
50
|
+
// emit multi-MB garbage "text" downstream. Typed empty result instead.
|
|
51
|
+
if (looksLikeBinary(html)) {
|
|
52
|
+
return emptyResult({
|
|
53
|
+
reasons: ['binary_input'],
|
|
54
|
+
detail: 'input looks like a binary format, not HTML/text',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// Size ceiling BEFORE any DOM work. Measured: jsdom peaks at ~270 MB RSS
|
|
58
|
+
// per input MB (2x for tables) — see guards.ts. Oversized bodies get a
|
|
59
|
+
// bounded tag-strip of the prefix we would otherwise have parsed.
|
|
60
|
+
if (html.length > MAX_EXTRACT_HTML_CHARS) {
|
|
61
|
+
return degradedTextResult(html.slice(0, MAX_EXTRACT_HTML_CHARS), {
|
|
62
|
+
reasons: ['input_too_large'],
|
|
63
|
+
detail: `input ${html.length} chars > ${MAX_EXTRACT_HTML_CHARS}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// Pre-flight depth guard (F-2). jsdom's serializer and Readability's DOM
|
|
67
|
+
// walks recurse with the markup depth, so ~5000-deep nesting overflows the
|
|
68
|
+
// stack and a ~4000-deep page already costs ~20s just to parse. Detect that
|
|
69
|
+
// on the raw string and skip the DOM entirely — degrade to a tag strip.
|
|
70
|
+
if (estimateMaxNestingDepth(html) > MAX_SAFE_NESTING_DEPTH) {
|
|
71
|
+
return degradedTextResult(html, {
|
|
72
|
+
reasons: ['nesting_too_deep'],
|
|
73
|
+
detail: `estimated nesting depth > ${MAX_SAFE_NESTING_DEPTH}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return extractViaReadability(html, url, recoverTables);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Belt-and-suspenders: any unforeseen deep-recursion / parser failure
|
|
81
|
+
// degrades to text rather than throwing.
|
|
82
|
+
return degradedTextResult(html, { reasons: ['parse_failed'] });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function extractViaReadability(html, url, recoverTables) {
|
|
86
|
+
const dom = new JSDOM(html, { url });
|
|
87
|
+
const doc = dom.window.document;
|
|
88
|
+
// Strip UI noise (e.g. MediaWiki "[edit]" section links) before Readability:
|
|
89
|
+
// they are link-dense enough that Readability drops the whole heading wrapper,
|
|
90
|
+
// collapsing distinct sections together and losing their headings.
|
|
91
|
+
stripPreReadabilityNoise(doc);
|
|
92
|
+
// Capture significant data tables BEFORE Readability runs: it mutates the
|
|
93
|
+
// document and frequently strips large/link-dense tables it scores as
|
|
94
|
+
// clutter, so on reference/data pages the main table would otherwise be lost
|
|
95
|
+
// entirely (Turndown's table rule never receives it).
|
|
96
|
+
const capture = recoverTables
|
|
97
|
+
? collectSignificantTables(doc)
|
|
98
|
+
: { tables: [], capped: false, cappedDetail: undefined };
|
|
99
|
+
// The page's own <h1> is usually the clean title; Readability's article.title
|
|
100
|
+
// is often derived from <title> and carries a " — Site" suffix. Capture it now
|
|
101
|
+
// (before Readability mutates the DOM) so we can strip that suffix.
|
|
102
|
+
const pageH1 = doc.querySelector('h1')?.textContent ?? null;
|
|
103
|
+
// keepClasses preserves `language-*` on <code> so Turndown's preCode rule can
|
|
104
|
+
// emit a fenced-code language hint (F-1); Readability strips all classes
|
|
105
|
+
// otherwise. Classes never reach the markdown output, so this is invisible
|
|
106
|
+
// everywhere except that one rule.
|
|
107
|
+
const article = new Readability(doc, { keepClasses: true }).parse();
|
|
108
|
+
if (!article) {
|
|
109
|
+
// Readability gave up — fall back to body text.
|
|
110
|
+
const bodyText = dom.window.document.body?.textContent?.trim() ?? '';
|
|
111
|
+
return {
|
|
112
|
+
title: null,
|
|
113
|
+
markdown: bodyText,
|
|
114
|
+
text: bodyText,
|
|
115
|
+
excerpt: null,
|
|
116
|
+
byline: null,
|
|
117
|
+
siteName: null,
|
|
118
|
+
length: bodyText.length,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const turndown = makeTurndown();
|
|
122
|
+
// Re-attach significant tables Readability dropped so the table rule can
|
|
123
|
+
// convert them — otherwise the data (often the page's whole point) is gone.
|
|
124
|
+
const recovery = recoverTables
|
|
125
|
+
? recoverDroppedTables(article.content, capture.tables, url)
|
|
126
|
+
: { html: '', capped: false };
|
|
127
|
+
const contentHtml = recovery.html ? `${article.content}\n${recovery.html}` : article.content;
|
|
128
|
+
const body = turndown.turndown(contentHtml);
|
|
129
|
+
// Prefer the page <h1> over a suffixed article.title (see cleanTitle).
|
|
130
|
+
const title = cleanTitle(article.title ?? '', pageH1);
|
|
131
|
+
// Readability extracts the title separately and strips the source <h1> from
|
|
132
|
+
// article.content, so the converted body has no title heading. Restore it
|
|
133
|
+
// (this otherwise drops the lead heading and depresses content recall).
|
|
134
|
+
const markdown = prependTitleHeading(body, title);
|
|
135
|
+
// A tripped capture/recovery cap means table recovery was not fully
|
|
136
|
+
// attempted — signal it, the rest of the extraction is still first-class.
|
|
137
|
+
const capped = capture.capped || recovery.capped;
|
|
138
|
+
return {
|
|
139
|
+
title: title || null,
|
|
140
|
+
markdown,
|
|
141
|
+
text: article.textContent ?? null,
|
|
142
|
+
excerpt: article.excerpt ?? null,
|
|
143
|
+
byline: article.byline ?? null,
|
|
144
|
+
siteName: article.siteName ?? null,
|
|
145
|
+
length: markdown.length,
|
|
146
|
+
...(capped
|
|
147
|
+
? {
|
|
148
|
+
degraded: {
|
|
149
|
+
reasons: ['table_recovery_capped'],
|
|
150
|
+
detail: capture.cappedDetail ?? 'article content too large for recovery re-parse',
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Empty typed result for input we refuse to process at all (binary). */
|
|
157
|
+
function emptyResult(degraded) {
|
|
158
|
+
return {
|
|
159
|
+
title: null,
|
|
160
|
+
markdown: '',
|
|
161
|
+
text: '',
|
|
162
|
+
excerpt: null,
|
|
163
|
+
byline: null,
|
|
164
|
+
siteName: null,
|
|
165
|
+
length: 0,
|
|
166
|
+
degraded,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Iterative HTML→text fallback for input we won't risk in the DOM. Drops
|
|
171
|
+
* script/style bodies (indexOf scan — the old lazy regex backtracked
|
|
172
|
+
* quadratically on unclosed-`<script` bombs), strips tags, and decodes the
|
|
173
|
+
* few entities common in visible text. Never recurses, so it can't overflow
|
|
174
|
+
* on pathological depth. Output is capped at MAX_DEGRADED_TEXT_CHARS.
|
|
175
|
+
*/
|
|
176
|
+
function degradedTextResult(html, degraded) {
|
|
177
|
+
let text = stripTagsLinear(stripScriptStyleBlocks(html))
|
|
178
|
+
.replace(/ /gi, ' ')
|
|
179
|
+
.replace(/&/gi, '&')
|
|
180
|
+
.replace(/</gi, '<')
|
|
181
|
+
.replace(/>/gi, '>')
|
|
182
|
+
.replace(/'|'/gi, "'")
|
|
183
|
+
.replace(/"/gi, '"')
|
|
184
|
+
.replace(/\s+/g, ' ')
|
|
185
|
+
.trim();
|
|
186
|
+
const reasons = [...degraded.reasons];
|
|
187
|
+
if (text.length > MAX_DEGRADED_TEXT_CHARS) {
|
|
188
|
+
text = text.slice(0, MAX_DEGRADED_TEXT_CHARS);
|
|
189
|
+
reasons.push('output_truncated');
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
title: null,
|
|
193
|
+
markdown: text,
|
|
194
|
+
text,
|
|
195
|
+
excerpt: null,
|
|
196
|
+
byline: null,
|
|
197
|
+
siteName: null,
|
|
198
|
+
length: text.length,
|
|
199
|
+
degraded: { reasons, detail: degraded.detail },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Prefer the page's own `<h1>` text over Readability's `article.title` when the
|
|
204
|
+
* h1 is a (shorter) case-insensitive prefix of it — Readability often derives
|
|
205
|
+
* the title from the `<title>` tag, which carries a " — SiteName" suffix the
|
|
206
|
+
* page `<h1>` does not. The prefix check guards against a wrong/banner `<h1>`.
|
|
207
|
+
*/
|
|
208
|
+
function cleanTitle(rawTitle, pageH1) {
|
|
209
|
+
const title = rawTitle.trim();
|
|
210
|
+
const h1 = (pageH1 ?? '').replace(/\s+/g, ' ').trim();
|
|
211
|
+
if (h1 && h1.length < title.length && title.toLowerCase().startsWith(h1.toLowerCase())) {
|
|
212
|
+
return h1;
|
|
213
|
+
}
|
|
214
|
+
return title;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Prepend the page title as a top-level (`#`) heading. Readability returns the
|
|
218
|
+
* title separately and drops the source `<h1>` from the content body, which
|
|
219
|
+
* leaves the markdown with no title and lowers content recall. Skips
|
|
220
|
+
* prepending when the body already opens with that title (some pages keep
|
|
221
|
+
* their `<h1>` in content), to avoid a duplicate heading.
|
|
222
|
+
*/
|
|
223
|
+
function prependTitleHeading(body, rawTitle) {
|
|
224
|
+
const title = rawTitle.trim();
|
|
225
|
+
if (!title)
|
|
226
|
+
return body;
|
|
227
|
+
const firstLine = body.replace(/^\s+/, '').split('\n', 1)[0] ?? '';
|
|
228
|
+
const normalize = (s) => s
|
|
229
|
+
.replace(/^#+\s*/, '')
|
|
230
|
+
.trim()
|
|
231
|
+
.toLowerCase();
|
|
232
|
+
if (normalize(firstLine) === title.toLowerCase())
|
|
233
|
+
return body;
|
|
234
|
+
return `# ${title}\n\n${body}`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Remove UI noise that confuses Readability's content cleaning, run before it.
|
|
238
|
+
* MediaWiki `[edit]` section links (`.mw-editsection`) are link-dense enough
|
|
239
|
+
* that Readability drops the entire heading wrapper, collapsing sections and
|
|
240
|
+
* losing their headings; removing them recovers the section headings AND strips
|
|
241
|
+
* the `[edit]` clutter from the output. No-op on non-MediaWiki pages.
|
|
242
|
+
*/
|
|
243
|
+
function stripPreReadabilityNoise(doc) {
|
|
244
|
+
for (const el of Array.from(doc.querySelectorAll('.mw-editsection, .mw-editsection-bracket'))) {
|
|
245
|
+
el.remove();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Recover a DROPPED table only when it is a SUBSTANTIAL data table. Small
|
|
249
|
+
// dropped tables on article pages are almost always chrome ("related",
|
|
250
|
+
// "most read") — recovering them adds noise and hurts article extraction;
|
|
251
|
+
// large dropped tables on data/reference pages ARE the content. In effect this
|
|
252
|
+
// is page-type routing: article pages drop only small tables (nothing recovered
|
|
253
|
+
// → lean Readability path), data/reference pages drop big ones (recovered).
|
|
254
|
+
const RECOVER_MIN_ROWS = 12;
|
|
255
|
+
const RECOVER_MIN_TEXT = 1200;
|
|
256
|
+
/** Heading-row text — a stable identity for a table, used to dedupe recoveries. */
|
|
257
|
+
function tableSignature(table) {
|
|
258
|
+
const firstRow = table.querySelector('tr');
|
|
259
|
+
return (firstRow?.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* A table worth recovering = a real data table, not a layout/nav/infobox box.
|
|
263
|
+
* Requires ≥ 3 rows and ≥ 2 columns, and excludes known chrome containers.
|
|
264
|
+
*/
|
|
265
|
+
function isSignificantDataTable(table) {
|
|
266
|
+
const cls = (table.getAttribute('class') ?? '').toLowerCase();
|
|
267
|
+
if (/\b(navbox|vertical-navbox|sidebar|infobox|metadata|nomobile|toccolours|noprint)\b/.test(cls)) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
if (table.closest('.navbox, .vertical-navbox, .sidebar, .infobox, nav, header, footer')) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
const rows = Array.from(table.querySelectorAll('tr'));
|
|
274
|
+
if (rows.length < 3)
|
|
275
|
+
return false;
|
|
276
|
+
const maxCols = rows.reduce((m, r) => Math.max(m, r.querySelectorAll('th, td').length), 0);
|
|
277
|
+
return maxCols >= 2;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Snapshot significant data tables (outerHTML + signature) from a document,
|
|
281
|
+
* under the guards.ts ceilings: giant tables are rejected by row count
|
|
282
|
+
* BEFORE their outerHTML is serialized (serialization itself is the
|
|
283
|
+
* allocation we're guarding), and the total snapshot budget is bounded so a
|
|
284
|
+
* many-tables page can't duplicate the whole document into strings.
|
|
285
|
+
*/
|
|
286
|
+
function collectSignificantTables(doc) {
|
|
287
|
+
const tables = [];
|
|
288
|
+
let totalChars = 0;
|
|
289
|
+
let capped = false;
|
|
290
|
+
let cappedDetail;
|
|
291
|
+
for (const t of Array.from(doc.querySelectorAll('table'))) {
|
|
292
|
+
if (!isSignificantDataTable(t))
|
|
293
|
+
continue;
|
|
294
|
+
if (tables.length >= MAX_CAPTURED_TABLES) {
|
|
295
|
+
capped = true;
|
|
296
|
+
cappedDetail = `more than ${MAX_CAPTURED_TABLES} significant tables`;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
const rows = t.querySelectorAll('tr').length;
|
|
300
|
+
if (rows > MAX_CAPTURE_TABLE_ROWS) {
|
|
301
|
+
capped = true;
|
|
302
|
+
cappedDetail = `table with ${rows} rows > ${MAX_CAPTURE_TABLE_ROWS}`;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const html = t.outerHTML;
|
|
306
|
+
if (html.length > MAX_SINGLE_TABLE_CHARS ||
|
|
307
|
+
totalChars + html.length > MAX_TOTAL_CAPTURED_CHARS) {
|
|
308
|
+
capped = true;
|
|
309
|
+
cappedDetail = 'table snapshot budget exceeded';
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
totalChars += html.length;
|
|
313
|
+
tables.push({
|
|
314
|
+
html,
|
|
315
|
+
signature: tableSignature(t),
|
|
316
|
+
rows,
|
|
317
|
+
textLen: (t.textContent ?? '').replace(/\s+/g, ' ').trim().length,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return { tables, capped, cappedDetail };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* HTML of captured tables NOT already present in the Readability content
|
|
324
|
+
* (matched by heading-row signature, so a table Readability kept is never
|
|
325
|
+
* duplicated). Empty string when there is nothing to recover. Skips the
|
|
326
|
+
* whole re-parse (a SECOND jsdom of the article content — the expensive
|
|
327
|
+
* half of recovery) when the content is too large to risk.
|
|
328
|
+
*/
|
|
329
|
+
function recoverDroppedTables(contentHtml, captured, url) {
|
|
330
|
+
if (captured.length === 0)
|
|
331
|
+
return { html: '', capped: false };
|
|
332
|
+
if (contentHtml.length > MAX_RECOVERY_CONTENT_CHARS) {
|
|
333
|
+
return { html: '', capped: true };
|
|
334
|
+
}
|
|
335
|
+
const presentDoc = new JSDOM(contentHtml, { url }).window.document;
|
|
336
|
+
const present = new Set(collectSignificantTables(presentDoc).tables.map((t) => t.signature));
|
|
337
|
+
const html = captured
|
|
338
|
+
.filter((t) => !present.has(t.signature))
|
|
339
|
+
.filter((t) => t.rows >= RECOVER_MIN_ROWS || t.textLen >= RECOVER_MIN_TEXT)
|
|
340
|
+
.map((t) => t.html)
|
|
341
|
+
.join('\n');
|
|
342
|
+
return { html, capped: false };
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Canonicalize a raw highlighter token into a Markdown fence language. Drops
|
|
346
|
+
* non-languages ("default"/"text"/"plaintext"/"none") and normalizes a couple
|
|
347
|
+
* of aliases so a scraping customer gets a stable, conventional tag.
|
|
348
|
+
*/
|
|
349
|
+
function normalizeFenceLang(raw) {
|
|
350
|
+
const l = (raw ?? '').trim().toLowerCase();
|
|
351
|
+
if (!l || l === 'default' || l === 'text' || l === 'plaintext' || l === 'none')
|
|
352
|
+
return '';
|
|
353
|
+
if (l === 'python3' || l === 'python2')
|
|
354
|
+
return 'python';
|
|
355
|
+
return l;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Derive a code-fence language from a <pre> node by checking, in order:
|
|
359
|
+
* 1. `language-xxx` / `lang-xxx` on the <code> (or <pre>) — highlight.js etc.
|
|
360
|
+
* 2. `highlight-source-xxx` on an ancestor — GitHub's blob wrapper.
|
|
361
|
+
* 3. `highlight-xxx` on an ancestor — Sphinx / Python-docs wrapper (the <pre>
|
|
362
|
+
* has no language class of its own; the lang lives on a grandparent div).
|
|
363
|
+
* Returns '' for a genuinely bare fence (no convention matched).
|
|
364
|
+
*/
|
|
365
|
+
function detectFenceLang(pre) {
|
|
366
|
+
const code = pre.querySelector('code');
|
|
367
|
+
for (const el of [code, pre]) {
|
|
368
|
+
const m = el?.className?.match(/(?:^|\s)(?:language|lang)-([a-zA-Z0-9+#.-]+)/);
|
|
369
|
+
if (m)
|
|
370
|
+
return normalizeFenceLang(m[1]);
|
|
371
|
+
}
|
|
372
|
+
// Walk a few ancestors for the wrapper conventions. GitHub before Sphinx,
|
|
373
|
+
// since `highlight-source-go` also matches the looser `highlight-xxx` pattern.
|
|
374
|
+
let node = pre;
|
|
375
|
+
for (let depth = 0; node && depth < 4; depth++) {
|
|
376
|
+
const cls = node.className ?? '';
|
|
377
|
+
const gh = cls.match(/(?:^|\s)highlight-source-([a-zA-Z0-9+#.-]+)/);
|
|
378
|
+
if (gh)
|
|
379
|
+
return normalizeFenceLang(gh[1]);
|
|
380
|
+
const sphinx = cls.match(/(?:^|\s)highlight-([a-zA-Z0-9+#.-]+)/);
|
|
381
|
+
if (sphinx)
|
|
382
|
+
return normalizeFenceLang(sphinx[1]);
|
|
383
|
+
node = node.parentElement;
|
|
384
|
+
}
|
|
385
|
+
return '';
|
|
386
|
+
}
|
|
387
|
+
function makeTurndown() {
|
|
388
|
+
const td = new TurndownService({
|
|
389
|
+
headingStyle: 'atx',
|
|
390
|
+
codeBlockStyle: 'fenced',
|
|
391
|
+
bulletListMarker: '-',
|
|
392
|
+
emDelimiter: '_',
|
|
393
|
+
strongDelimiter: '**',
|
|
394
|
+
linkStyle: 'inlined',
|
|
395
|
+
});
|
|
396
|
+
// Preserve <pre> code blocks with their language hint. The hint lives under
|
|
397
|
+
// several conventions across the web; detectFenceLang covers the common ones
|
|
398
|
+
// so a bare fence doesn't silently drop information a scraping customer asked
|
|
399
|
+
// for (syntax highlighting, language routing).
|
|
400
|
+
td.addRule('preCode', {
|
|
401
|
+
filter: ['pre'],
|
|
402
|
+
replacement: (_content, node) => {
|
|
403
|
+
const pre = node;
|
|
404
|
+
const codeEl = pre.querySelector('code');
|
|
405
|
+
const lang = detectFenceLang(pre);
|
|
406
|
+
const code = codeEl?.textContent ?? pre.textContent ?? '';
|
|
407
|
+
return `\n\n\`\`\`${lang}\n${code}\n\`\`\`\n\n`;
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
// Convert <table> to GitHub-flavored Markdown. Turndown has no table
|
|
411
|
+
// support by default, so a data table would otherwise collapse into a run
|
|
412
|
+
// of concatenated cell text and lose all structure.
|
|
413
|
+
td.addRule('gfmTable', {
|
|
414
|
+
filter: 'table',
|
|
415
|
+
replacement: (_content, node) => {
|
|
416
|
+
const rows = Array.from(node.querySelectorAll('tr'));
|
|
417
|
+
if (rows.length === 0)
|
|
418
|
+
return '';
|
|
419
|
+
const cellsOf = (tr) => Array.from(tr.querySelectorAll('th, td')).map((c) => (c.textContent ?? '')
|
|
420
|
+
.trim()
|
|
421
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
422
|
+
.replace(/\|/g, '\\|'));
|
|
423
|
+
let grid = rows.map(cellsOf).filter((r) => r.length > 0);
|
|
424
|
+
if (grid.length === 0)
|
|
425
|
+
return '';
|
|
426
|
+
// reduce, not Math.max(...spread): the spread throws RangeError past
|
|
427
|
+
// ~65k rows, which would degrade the whole page instead of the table.
|
|
428
|
+
const rawCols = grid.reduce((m, r) => Math.max(m, r.length), 0);
|
|
429
|
+
// Geometry ceilings (perf-extract): padding multiplies the widest row
|
|
430
|
+
// across ALL rows, so a single ragged 100k-cell row would otherwise
|
|
431
|
+
// blow the output up to hundreds of MB. Far above any legit corpus
|
|
432
|
+
// table — output is byte-identical for sane pages.
|
|
433
|
+
const cols = Math.min(rawCols, MAX_TABLE_COLS);
|
|
434
|
+
let clipped = rawCols > MAX_TABLE_COLS;
|
|
435
|
+
const maxRows = Math.max(2, Math.floor(MAX_TABLE_CELLS / Math.max(1, cols)));
|
|
436
|
+
if (grid.length > maxRows) {
|
|
437
|
+
grid = grid.slice(0, maxRows);
|
|
438
|
+
clipped = true;
|
|
439
|
+
}
|
|
440
|
+
const pad = (r) => {
|
|
441
|
+
const out = r.slice(0, cols);
|
|
442
|
+
while (out.length < cols)
|
|
443
|
+
out.push('');
|
|
444
|
+
return out;
|
|
445
|
+
};
|
|
446
|
+
const line = (r) => `| ${pad(r).join(' | ')} |`;
|
|
447
|
+
const header = line(grid[0]);
|
|
448
|
+
const sep = `| ${Array.from({ length: cols }, () => '---').join(' | ')} |`;
|
|
449
|
+
const body = grid.slice(1).map(line);
|
|
450
|
+
const note = clipped ? '\n\n_(table truncated: exceeds size limits)_' : '';
|
|
451
|
+
return `\n\n${[header, sep, ...body].join('\n')}${note}\n\n`;
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
// Drop noisy elements that survive Readability sometimes.
|
|
455
|
+
td.remove(['script', 'style', 'noscript', 'iframe']);
|
|
456
|
+
return td;
|
|
457
|
+
}
|
|
458
|
+
//# sourceMappingURL=readability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readability.js","sourceRoot":"","sources":["../src/readability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap discovery + parsing (Phase B).
|
|
3
|
+
*
|
|
4
|
+
* Sources for /map URL discovery:
|
|
5
|
+
* 1. `robots.txt` Sitemap: directive(s)
|
|
6
|
+
* 2. `/sitemap.xml` at the host root
|
|
7
|
+
* 3. Recursive: sitemapindex → sub-sitemaps → urls
|
|
8
|
+
*
|
|
9
|
+
* Sitemap XML is constrained enough that a regex parse beats a full
|
|
10
|
+
* XML parser dep. Both `<urlset>` and `<sitemapindex>` use the same
|
|
11
|
+
* `<loc>` element; we discriminate by the wrapping element to decide
|
|
12
|
+
* whether each `<loc>` is a URL or a nested sitemap.
|
|
13
|
+
*/
|
|
14
|
+
export interface ParsedSitemap {
|
|
15
|
+
/** Direct page URLs from <urlset>/<url>/<loc>. */
|
|
16
|
+
urls: string[];
|
|
17
|
+
/** Sub-sitemap URLs from <sitemapindex>/<sitemap>/<loc>. */
|
|
18
|
+
subsitemaps: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse a sitemap XML blob. Returns separately the page URLs and the
|
|
22
|
+
* nested sub-sitemap URLs. A document can technically contain both
|
|
23
|
+
* (rare but allowed); we split based on whether the document has
|
|
24
|
+
* <sitemapindex> as a top-level element.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseSitemapXml(xml: string): ParsedSitemap;
|
|
27
|
+
/**
|
|
28
|
+
* Pull `Sitemap:` directives out of a robots.txt body.
|
|
29
|
+
*
|
|
30
|
+
* Per https://datatracker.ietf.org/doc/html/rfc9309, Sitemap is a
|
|
31
|
+
* separate field with absolute-URL value. Case-insensitive.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseRobotsTxtForSitemaps(robotsTxt: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Discover URLs for a host via the standard cascade.
|
|
36
|
+
*
|
|
37
|
+
* - First fetches `/robots.txt` and extracts any Sitemap: directives.
|
|
38
|
+
* - If none, falls back to `/sitemap.xml` at the host root.
|
|
39
|
+
* - Recursively follows sitemapindex → sub-sitemaps (bounded by
|
|
40
|
+
* `maxSitemapsToFollow` to avoid infinite loops / sitemap bombs).
|
|
41
|
+
*
|
|
42
|
+
* Pure — the caller (`/map` route) passes in a fetchImpl so SSRF + the
|
|
43
|
+
* shared dispatcher live at the boundary.
|
|
44
|
+
*/
|
|
45
|
+
export interface DiscoverUrlsOptions {
|
|
46
|
+
fetchImpl: typeof fetch;
|
|
47
|
+
/** Max distinct sitemap URLs to fetch (prevents bombs). Default 20. */
|
|
48
|
+
maxSitemapsToFollow?: number;
|
|
49
|
+
/** Max URLs to collect (stops early once hit). Default 10_000. */
|
|
50
|
+
limit?: number;
|
|
51
|
+
/** Include subdomains? Default false (same-host only). */
|
|
52
|
+
includeSubdomains?: boolean;
|
|
53
|
+
/** Substring filter on URLs. */
|
|
54
|
+
search?: string;
|
|
55
|
+
/** Per-fetch timeout ms. Default 10_000. */
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
/** Optional override for the user agent. */
|
|
58
|
+
userAgent?: string;
|
|
59
|
+
/**
|
|
60
|
+
* SSRF pre-flight for each discovered sitemap URL. The /map route passes
|
|
61
|
+
* validateUrlForExternalRequest; child sitemaps come from a target's
|
|
62
|
+
* robots.txt `Sitemap:` directives / sitemapindex `<loc>` and are otherwise
|
|
63
|
+
* attacker-controlled. A rejected URL is skipped. (PENTEST_REPORT.md P5.)
|
|
64
|
+
*/
|
|
65
|
+
validateUrl?: (url: string) => Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export interface DiscoverUrlsResult {
|
|
68
|
+
urls: string[];
|
|
69
|
+
/** Distinct sitemap URLs we actually fetched. */
|
|
70
|
+
sitemapsConsidered: string[];
|
|
71
|
+
/** True if we stopped because we hit the URL limit. */
|
|
72
|
+
truncated: boolean;
|
|
73
|
+
}
|
|
74
|
+
export declare function discoverUrls(startUrl: string, options: DiscoverUrlsOptions): Promise<DiscoverUrlsResult>;
|