@v1nvn/readability-mcp 0.17.0 → 0.19.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/README.md +52 -3
- package/dist/assets/{cli-C4RkP1lV.js → cli-CQl4_iKO.js} +4 -2
- package/dist/assets/{cli-C4RkP1lV.js.map → cli-CQl4_iKO.js.map} +1 -1
- package/dist/assets/{extract-BWRBFiUP.js → extract-BF4w5w8k.js} +1935 -1822
- package/dist/assets/extract-BF4w5w8k.js.map +1 -0
- package/dist/index.js +869 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/assets/extract-BWRBFiUP.js.map +0 -1
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { JSDOM } from "jsdom";
|
|
6
|
+
import { stringify } from "yaml";
|
|
4
7
|
import remarkGfm from "remark-gfm";
|
|
5
8
|
import remarkParse from "remark-parse";
|
|
6
9
|
import { unified } from "unified";
|
|
7
10
|
import { Readability, isProbablyReaderable } from "@mozilla/readability";
|
|
8
|
-
import { JSDOM } from "jsdom";
|
|
9
|
-
import { readFileSync } from "node:fs";
|
|
10
|
-
import { stringify } from "yaml";
|
|
11
11
|
import DOMPurify from "dompurify";
|
|
12
12
|
import TurndownService from "turndown";
|
|
13
13
|
import { gfm } from "turndown-plugin-gfm";
|
|
14
14
|
var package_default = {
|
|
15
15
|
name: "@v1nvn/readability-mcp",
|
|
16
|
-
version: "0.
|
|
16
|
+
version: "0.19.0",
|
|
17
17
|
description: "MCP server that turns rendered (post-JS) HTML into clean Markdown + metadata via Readability, Turndown, and DOMPurify.",
|
|
18
18
|
type: "module",
|
|
19
19
|
main: "dist/index.js",
|
|
@@ -93,7 +93,7 @@ function resolveLogLevel(env) {
|
|
|
93
93
|
}
|
|
94
94
|
var SERVER_TITLE = "Readability MCP";
|
|
95
95
|
var SERVER_DESCRIPTION = "Turn already-rendered (post-JavaScript) HTML into clean, LLM-friendly Markdown plus metadata, via Mozilla Readability, Turndown, and DOMPurify. Makes no outbound requests — input is the rendered HTML, read from a file path (localPath) so the page bytes never enter the model context.";
|
|
96
|
-
var SERVER_INSTRUCTIONS = `Eleven always-on tools, all fed a file path (localPath) holding already-rendered HTML (e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture) — except \`chunk_text\`, which operates on already-extracted text.
|
|
96
|
+
var SERVER_INSTRUCTIONS = `Eleven always-on tools, all fed a file path (localPath) holding already-rendered HTML (e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture) — except \`chunk_text\`, which operates on already-extracted text. Sampling-gated \`summarize\` and \`suggest_preset\` add a twelfth and thirteenth when (and only when) the host advertises the MCP \`sampling\` capability. The server never fetches URLs.
|
|
97
97
|
|
|
98
98
|
- extract: main tool. Runs Readability to pull the article and returns Markdown + metadata + diagnostics. Use by default for article-like pages. Pass the \`chunk\` option to also emit token-bounded chunks for RAG/embedding.
|
|
99
99
|
- extract_links: return a structured list of anchor links ({text, href, rel, isExternal}) from the raw DOM — hrefs absolutized against baseUrl; pairs with chrome-devtools for crawl/navigation decisions.
|
|
@@ -109,6 +109,7 @@ var SERVER_INSTRUCTIONS = `Eleven always-on tools, all fed a file path (localPat
|
|
|
109
109
|
|
|
110
110
|
Sampling-gated (listed only when the client advertises \`sampling\` on initialize):
|
|
111
111
|
- summarize: delegate to the HOST’s model via \`sampling/createMessage\` — input {text, maxTokens?}, typically the output of \`extract\`/\`html_to_markdown\`. The server embeds no model and calls no provider directly; the host picks the model and may prompt the user first.
|
|
112
|
+
- suggest_preset: two-round suggest loop for a lost extraction — after \`extract\` reports gated content, a fallback, a near-empty result, or visible debris (video-player controls), the HOST model proposes {detectors, include, exclude} selectors from a copy-safe DOM outline; proposals are validated deterministically (no positional pseudos, no :contains, no generated hash classes, every selector must match) and applied through the real pipeline; a converged preset is stored in memory and persisted as <site>.json in the local preset cache so later extractions of that site apply it automatically.
|
|
112
113
|
|
|
113
114
|
Rounding out the surface:
|
|
114
115
|
- resources: \`extract({cache:true})\` caches results as addressable \`readability://page/{hash}\` Resources; \`diagnostics.cache = {hit, normalizedHash, originalHash}\`. Re-renders that differ only in nonce/CSP/generated-id collapse to the same key (normalized-hash keying).
|
|
@@ -128,6 +129,115 @@ function levelEnabled(config, level) {
|
|
|
128
129
|
return LEVEL_RANK[level] >= LEVEL_RANK[config.logLevel];
|
|
129
130
|
}
|
|
130
131
|
//#endregion
|
|
132
|
+
//#region src/logger.ts
|
|
133
|
+
var LEVEL_LABEL = {
|
|
134
|
+
debug: "DEBUG",
|
|
135
|
+
info: "INFO",
|
|
136
|
+
warn: "WARN",
|
|
137
|
+
error: "ERROR"
|
|
138
|
+
};
|
|
139
|
+
function format(level, message) {
|
|
140
|
+
return `${LEVEL_LABEL[level]} ${message}`;
|
|
141
|
+
}
|
|
142
|
+
function emit(writer, level, message) {
|
|
143
|
+
writer(format(level, message));
|
|
144
|
+
}
|
|
145
|
+
var Logger = class {
|
|
146
|
+
stderr;
|
|
147
|
+
constructor(stderr = (line) => process.stderr.write(`${line}\n`)) {
|
|
148
|
+
this.stderr = stderr;
|
|
149
|
+
}
|
|
150
|
+
debug(message) {
|
|
151
|
+
if (levelEnabled(activeConfig, "debug")) emit(this.stderr, "debug", message);
|
|
152
|
+
}
|
|
153
|
+
error(message) {
|
|
154
|
+
if (levelEnabled(activeConfig, "error")) emit(this.stderr, "error", message);
|
|
155
|
+
}
|
|
156
|
+
info(message) {
|
|
157
|
+
if (levelEnabled(activeConfig, "info")) emit(this.stderr, "info", message);
|
|
158
|
+
}
|
|
159
|
+
warn(message) {
|
|
160
|
+
if (levelEnabled(activeConfig, "warn")) emit(this.stderr, "warn", message);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
var activeConfig = loadConfig();
|
|
164
|
+
var logger = new Logger();
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/policy/presets.ts
|
|
167
|
+
var presets = /* @__PURE__ */ new Map();
|
|
168
|
+
var generation = 0;
|
|
169
|
+
function presetGeneration() {
|
|
170
|
+
return generation;
|
|
171
|
+
}
|
|
172
|
+
function addPreset(preset) {
|
|
173
|
+
const key = normalizeSiteKey(preset.site);
|
|
174
|
+
if (!key) throw new Error(`invalid preset site: ${preset.site}`);
|
|
175
|
+
presets.set(key, preset);
|
|
176
|
+
generation += 1;
|
|
177
|
+
}
|
|
178
|
+
function removePreset(site) {
|
|
179
|
+
const key = normalizeSiteKey(site);
|
|
180
|
+
const removed = key !== void 0 && presets.delete(key);
|
|
181
|
+
if (removed) generation += 1;
|
|
182
|
+
return removed;
|
|
183
|
+
}
|
|
184
|
+
function presetForSite(site) {
|
|
185
|
+
const key = normalizeSiteKey(site);
|
|
186
|
+
return key ? presets.get(key) : void 0;
|
|
187
|
+
}
|
|
188
|
+
function normalizeSiteKey(value) {
|
|
189
|
+
const raw = value?.trim();
|
|
190
|
+
if (!raw) return;
|
|
191
|
+
let host;
|
|
192
|
+
try {
|
|
193
|
+
host = new URL(raw).hostname.toLowerCase();
|
|
194
|
+
} catch {
|
|
195
|
+
host = raw.toLowerCase();
|
|
196
|
+
}
|
|
197
|
+
const key = host.startsWith("www.") ? host.slice(4) : host;
|
|
198
|
+
return /^[a-z0-9.-]+$/.test(key) ? key : void 0;
|
|
199
|
+
}
|
|
200
|
+
function selectorMisses(scope, selector) {
|
|
201
|
+
try {
|
|
202
|
+
return scope.querySelector(selector) === null;
|
|
203
|
+
} catch {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function presetMatches(document, preset) {
|
|
208
|
+
for (const detector of preset.detectors) if (selectorMisses(document, detector)) return false;
|
|
209
|
+
if (preset.scope.include && selectorMisses(document.body, preset.scope.include)) return false;
|
|
210
|
+
for (const selector of preset.scope.exclude ?? []) try {
|
|
211
|
+
document.querySelectorAll(selector);
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
function resolvePreset(document, baseUrl, overridden) {
|
|
218
|
+
const site = normalizeSiteKey(baseUrl);
|
|
219
|
+
if (!site) return;
|
|
220
|
+
const preset = presets.get(site);
|
|
221
|
+
if (!preset) return;
|
|
222
|
+
if (overridden) return { signal: {
|
|
223
|
+
applied: false,
|
|
224
|
+
reason: "overridden",
|
|
225
|
+
site
|
|
226
|
+
} };
|
|
227
|
+
if (!presetMatches(document, preset)) return { signal: {
|
|
228
|
+
applied: false,
|
|
229
|
+
reason: "detectors-missed",
|
|
230
|
+
site
|
|
231
|
+
} };
|
|
232
|
+
return {
|
|
233
|
+
scope: preset.scope,
|
|
234
|
+
signal: {
|
|
235
|
+
applied: true,
|
|
236
|
+
site
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
131
241
|
//#region src/resources.ts
|
|
132
242
|
var MAX_ENTRIES = 256;
|
|
133
243
|
var TTL_MS = 18e5;
|
|
@@ -193,6 +303,7 @@ function buildArgsFingerprint(args) {
|
|
|
193
303
|
maxNodes: args.maxNodes ?? null,
|
|
194
304
|
metadataMode: args.metadataMode,
|
|
195
305
|
minArticleLength: args.minArticleLength ?? null,
|
|
306
|
+
presetGeneration: presetGeneration(),
|
|
196
307
|
readabilityOverrides: args.readabilityOverrides ?? null,
|
|
197
308
|
sanitize: args.sanitize,
|
|
198
309
|
selectors: sel,
|
|
@@ -308,1459 +419,868 @@ function toErrorResult(err) {
|
|
|
308
419
|
};
|
|
309
420
|
}
|
|
310
421
|
//#endregion
|
|
311
|
-
//#region src/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
};
|
|
318
|
-
function format(level, message) {
|
|
319
|
-
return `${LEVEL_LABEL[level]} ${message}`;
|
|
422
|
+
//#region src/pipeline/dom.ts
|
|
423
|
+
function buildDocument(html, baseUrl) {
|
|
424
|
+
const dom = new JSDOM(html, { url: baseUrl });
|
|
425
|
+
return {
|
|
426
|
+
document: dom.window.document,
|
|
427
|
+
window: dom.window
|
|
428
|
+
};
|
|
320
429
|
}
|
|
321
|
-
function
|
|
322
|
-
|
|
430
|
+
function isElement(node) {
|
|
431
|
+
return node.nodeType === 1;
|
|
323
432
|
}
|
|
324
|
-
var Logger = class {
|
|
325
|
-
stderr;
|
|
326
|
-
constructor(stderr = (line) => process.stderr.write(`${line}\n`)) {
|
|
327
|
-
this.stderr = stderr;
|
|
328
|
-
}
|
|
329
|
-
debug(message) {
|
|
330
|
-
if (levelEnabled(activeConfig, "debug")) emit(this.stderr, "debug", message);
|
|
331
|
-
}
|
|
332
|
-
error(message) {
|
|
333
|
-
if (levelEnabled(activeConfig, "error")) emit(this.stderr, "error", message);
|
|
334
|
-
}
|
|
335
|
-
info(message) {
|
|
336
|
-
if (levelEnabled(activeConfig, "info")) emit(this.stderr, "info", message);
|
|
337
|
-
}
|
|
338
|
-
warn(message) {
|
|
339
|
-
if (levelEnabled(activeConfig, "warn")) emit(this.stderr, "warn", message);
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
var activeConfig = loadConfig();
|
|
343
|
-
var logger = new Logger();
|
|
344
433
|
//#endregion
|
|
345
|
-
//#region src/policy/
|
|
346
|
-
var
|
|
347
|
-
var
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
start
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
return blocks;
|
|
365
|
-
}
|
|
366
|
-
function headingText(raw) {
|
|
367
|
-
return raw.replace(HEADING_MARKERS, "").trim();
|
|
434
|
+
//#region src/policy/math.ts
|
|
435
|
+
var MARKER_CLASS = "rdrm-math";
|
|
436
|
+
var DISPLAY_ATTR = "data-display";
|
|
437
|
+
var KATEX_CLASS = "katex";
|
|
438
|
+
var KATEX_DISPLAY_CLASS = "katex-display";
|
|
439
|
+
var TEX_ANNOTATION_SELECTOR = "annotation[encoding=\"application/x-tex\"]";
|
|
440
|
+
var MATHJAX_INLINE_TYPE = "math/tex";
|
|
441
|
+
var MATHJAX_DISPLAY_TYPE = "math/tex; mode=display";
|
|
442
|
+
var MATHML_DISPLAY_SELECTOR = ".ltx_equation, .ltx_displaymath, .equation-display, .math-display";
|
|
443
|
+
var BROKEN_PLACEHOLDER = "[?]";
|
|
444
|
+
function createMarker(document, tex, display) {
|
|
445
|
+
const marker = document.createElement("span");
|
|
446
|
+
marker.className = MARKER_CLASS;
|
|
447
|
+
marker.setAttribute(DISPLAY_ATTR, display ? "true" : "false");
|
|
448
|
+
marker.textContent = tex;
|
|
449
|
+
return marker;
|
|
368
450
|
}
|
|
369
|
-
function
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
451
|
+
function convertAnnotations(document) {
|
|
452
|
+
const annotations = document.querySelectorAll(TEX_ANNOTATION_SELECTOR);
|
|
453
|
+
for (const annotation of Array.from(annotations)) {
|
|
454
|
+
if (!annotation.isConnected) continue;
|
|
455
|
+
try {
|
|
456
|
+
const katex = annotation.closest(`.${KATEX_CLASS}`);
|
|
457
|
+
const math = annotation.closest("math");
|
|
458
|
+
let container = annotation;
|
|
459
|
+
let display = false;
|
|
460
|
+
if (katex) {
|
|
461
|
+
container = katex;
|
|
462
|
+
display = katex.closest(`.${KATEX_DISPLAY_CLASS}`) !== null;
|
|
463
|
+
} else if (math) {
|
|
464
|
+
container = math;
|
|
465
|
+
display = math.getAttribute("display") === "block" || math.closest(MATHML_DISPLAY_SELECTOR) !== null;
|
|
466
|
+
}
|
|
467
|
+
const tex = annotation.textContent.trim() || (math?.getAttribute("alttext") ?? "").trim();
|
|
468
|
+
container.replaceWith(createMarker(document, tex || BROKEN_PLACEHOLDER, display));
|
|
469
|
+
} catch {
|
|
470
|
+
annotation.replaceWith(createMarker(document, BROKEN_PLACEHOLDER, false));
|
|
383
471
|
}
|
|
384
|
-
const candidate = buffer ? `${buffer}\n${line}` : line;
|
|
385
|
-
if (candidate.length > maxChars) {
|
|
386
|
-
flush();
|
|
387
|
-
buffer = line;
|
|
388
|
-
} else buffer = candidate;
|
|
389
472
|
}
|
|
390
|
-
flush();
|
|
391
|
-
return pieces;
|
|
392
473
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
for (const part of parts) {
|
|
401
|
-
const text = part.trim();
|
|
402
|
-
if (!text) continue;
|
|
403
|
-
if (HEADING_FIRST_LINE.test(text)) heading = headingText(text);
|
|
404
|
-
blocks.push({
|
|
405
|
-
headingContext: heading,
|
|
406
|
-
text
|
|
407
|
-
});
|
|
474
|
+
function convertOrphanedKatex(document) {
|
|
475
|
+
for (const katex of Array.from(document.getElementsByClassName(KATEX_CLASS))) {
|
|
476
|
+
if (!katex.isConnected) continue;
|
|
477
|
+
try {
|
|
478
|
+
const display = katex.closest(`.${KATEX_DISPLAY_CLASS}`) !== null;
|
|
479
|
+
katex.replaceWith(createMarker(document, BROKEN_PLACEHOLDER, display));
|
|
480
|
+
} catch {}
|
|
408
481
|
}
|
|
409
|
-
return blocks;
|
|
410
482
|
}
|
|
411
|
-
function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
483
|
+
function convertMathJax(document) {
|
|
484
|
+
const scripts = document.querySelectorAll(`script[type="${MATHJAX_INLINE_TYPE}"], script[type="${MATHJAX_DISPLAY_TYPE}"]`);
|
|
485
|
+
for (const script of Array.from(scripts)) {
|
|
486
|
+
if (!script.isConnected) continue;
|
|
487
|
+
try {
|
|
488
|
+
const display = (script.getAttribute("type") ?? "").includes("mode=display");
|
|
489
|
+
const tex = script.textContent.trim();
|
|
490
|
+
script.replaceWith(createMarker(document, tex || BROKEN_PLACEHOLDER, display));
|
|
491
|
+
} catch {
|
|
492
|
+
script.replaceWith(createMarker(document, BROKEN_PLACEHOLDER, false));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
416
495
|
}
|
|
417
|
-
function
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return units;
|
|
496
|
+
function extractMath(document) {
|
|
497
|
+
convertAnnotations(document);
|
|
498
|
+
convertOrphanedKatex(document);
|
|
499
|
+
convertMathJax(document);
|
|
422
500
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region src/policy/resolver.ts
|
|
503
|
+
var DEFAULT_CHAR_THRESHOLD = 500;
|
|
504
|
+
var DEFAULT_N_TOP_CANDIDATES = 5;
|
|
505
|
+
var CLASSES_TO_PRESERVE = [
|
|
506
|
+
"hljs",
|
|
507
|
+
"language-asm",
|
|
508
|
+
"language-assembly",
|
|
509
|
+
"language-bash",
|
|
510
|
+
"language-c",
|
|
511
|
+
"language-clojure",
|
|
512
|
+
"language-cpp",
|
|
513
|
+
"language-cs",
|
|
514
|
+
"language-csharp",
|
|
515
|
+
"language-css",
|
|
516
|
+
"language-dart",
|
|
517
|
+
"language-diff",
|
|
518
|
+
"language-dockerfile",
|
|
519
|
+
"language-elixir",
|
|
520
|
+
"language-erlang",
|
|
521
|
+
"language-go",
|
|
522
|
+
"language-graphql",
|
|
523
|
+
"language-haskell",
|
|
524
|
+
"language-html",
|
|
525
|
+
"language-ini",
|
|
526
|
+
"language-java",
|
|
527
|
+
"language-javascript",
|
|
528
|
+
"language-js",
|
|
529
|
+
"language-jsx",
|
|
530
|
+
"language-json",
|
|
531
|
+
"language-kotlin",
|
|
532
|
+
"language-lisp",
|
|
533
|
+
"language-lua",
|
|
534
|
+
"language-md",
|
|
535
|
+
"language-markdown",
|
|
536
|
+
"language-objc",
|
|
537
|
+
"language-objectivec",
|
|
538
|
+
"language-perl",
|
|
539
|
+
"language-php",
|
|
540
|
+
"language-plaintext",
|
|
541
|
+
"language-powershell",
|
|
542
|
+
"language-py",
|
|
543
|
+
"language-python",
|
|
544
|
+
"language-r",
|
|
545
|
+
"language-rb",
|
|
546
|
+
"language-rs",
|
|
547
|
+
"language-ruby",
|
|
548
|
+
"language-rust",
|
|
549
|
+
"language-scala",
|
|
550
|
+
"language-sh",
|
|
551
|
+
"language-shell",
|
|
552
|
+
"language-sql",
|
|
553
|
+
"language-swift",
|
|
554
|
+
"language-text",
|
|
555
|
+
"language-toml",
|
|
556
|
+
"language-ts",
|
|
557
|
+
"language-tsx",
|
|
558
|
+
"language-typescript",
|
|
559
|
+
"language-vim",
|
|
560
|
+
"language-wasm",
|
|
561
|
+
"language-xml",
|
|
562
|
+
"language-yaml",
|
|
563
|
+
"language-yml",
|
|
564
|
+
"rdrm-math"
|
|
565
|
+
];
|
|
566
|
+
var KNOWN_LANGUAGE_TOKENS = new Set(CLASSES_TO_PRESERVE.filter((c) => c.startsWith("language-")).map((c) => c.slice(9)));
|
|
567
|
+
function knobsForMode(mode) {
|
|
568
|
+
switch (mode) {
|
|
569
|
+
case "aggressive": return {
|
|
570
|
+
charThreshold: Math.round(DEFAULT_CHAR_THRESHOLD / 2),
|
|
571
|
+
nbTopCandidates: 10
|
|
572
|
+
};
|
|
573
|
+
case "balanced": return null;
|
|
574
|
+
case "conservative": return {
|
|
575
|
+
charThreshold: DEFAULT_CHAR_THRESHOLD * 2,
|
|
576
|
+
nbTopCandidates: Math.max(1, Math.round(DEFAULT_N_TOP_CANDIDATES / 2))
|
|
577
|
+
};
|
|
492
578
|
}
|
|
493
|
-
return chunks;
|
|
494
579
|
}
|
|
495
|
-
function
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
580
|
+
function resolveReadabilityOptions(input) {
|
|
581
|
+
const modeKnobs = knobsForMode(input.extraction ?? "balanced");
|
|
582
|
+
const keepClasses = input.keepClasses ?? false;
|
|
583
|
+
const charThreshold = input.minArticleLength !== void 0 ? input.minArticleLength : modeKnobs?.charThreshold;
|
|
584
|
+
const nbTopCandidates = modeKnobs?.nbTopCandidates;
|
|
585
|
+
return {
|
|
586
|
+
classesToPreserve: keepClasses ? [] : [...CLASSES_TO_PRESERVE],
|
|
587
|
+
keepClasses,
|
|
588
|
+
...charThreshold !== void 0 ? { charThreshold } : {},
|
|
589
|
+
...nbTopCandidates !== void 0 ? { nbTopCandidates } : {},
|
|
590
|
+
...input.maxNodes !== void 0 ? { maxElemsToParse: input.maxNodes } : {},
|
|
591
|
+
...input.readabilityOverrides ?? {}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/pipeline/normalize.ts
|
|
596
|
+
var NONCE_ATTR = "nonce";
|
|
597
|
+
function normalizeDocument(document, options) {
|
|
598
|
+
extractMath(document);
|
|
599
|
+
const baseEls = document.querySelectorAll("base");
|
|
600
|
+
const scriptEls = document.querySelectorAll("script:not([type=\"application/ld+json\"])");
|
|
601
|
+
baseEls.forEach((el) => {
|
|
602
|
+
el.remove();
|
|
603
|
+
});
|
|
604
|
+
scriptEls.forEach((el) => {
|
|
605
|
+
el.remove();
|
|
606
|
+
});
|
|
607
|
+
document.querySelectorAll(`[${NONCE_ATTR}]`).forEach((el) => {
|
|
608
|
+
el.removeAttribute(NONCE_ATTR);
|
|
609
|
+
});
|
|
610
|
+
const chromeRemoved = options?.cleanChrome === false ? 0 : stripChrome(document);
|
|
611
|
+
return {
|
|
612
|
+
boilerplateRemoved: stripBoilerplate(document),
|
|
613
|
+
chromeRemoved,
|
|
614
|
+
iframes: 0,
|
|
615
|
+
scripts: scriptEls.length
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
var CONSENT_SELECTORS = [
|
|
619
|
+
"[role=\"dialog\"]",
|
|
620
|
+
"[aria-modal=\"true\"]",
|
|
621
|
+
"#onetrust-banner-sdk",
|
|
622
|
+
"#onetrust-consent-sdk",
|
|
623
|
+
"#onetrust-pc-sdk",
|
|
624
|
+
".cc-window",
|
|
625
|
+
".cc-banner",
|
|
626
|
+
".cc-revoke",
|
|
627
|
+
".osano-cm-window",
|
|
628
|
+
".osano-cm-dialog",
|
|
629
|
+
".qc-cmp2-container",
|
|
630
|
+
".qc-cmp-ui-container",
|
|
631
|
+
"#sp_message_container",
|
|
632
|
+
"[id^=\"sp_message_container_\"]",
|
|
633
|
+
"#didomi-host",
|
|
634
|
+
".didomi-popup-container",
|
|
635
|
+
"#truste-consent-track",
|
|
636
|
+
"#consent_blackbar",
|
|
637
|
+
"#cookie-banner",
|
|
638
|
+
".cookie-banner",
|
|
639
|
+
"#consent-banner",
|
|
640
|
+
".consent-banner",
|
|
641
|
+
".cookie-bar",
|
|
642
|
+
".gdpr-banner",
|
|
643
|
+
".privacy-banner"
|
|
644
|
+
];
|
|
645
|
+
function isFullViewportOverlay(style) {
|
|
646
|
+
if (!/position\s*:\s*(?:fixed|sticky)/i.test(style)) return false;
|
|
647
|
+
const zIndexMatch = /z-index\s*:\s*(\d+)/i.exec(style);
|
|
648
|
+
if (!zIndexMatch || Number.parseInt(zIndexMatch[1], 10) < 1e3) return false;
|
|
649
|
+
if (/inset\s*:\s*0/i.test(style)) return true;
|
|
650
|
+
const widthFull = /width\s*:\s*100(?:%|vw)/i.test(style) || /left\s*:\s*0/i.test(style) && /right\s*:\s*0/i.test(style);
|
|
651
|
+
const heightFull = /height\s*:\s*100(?:%|vh)/i.test(style) || /top\s*:\s*0/i.test(style) && /bottom\s*:\s*0/i.test(style);
|
|
652
|
+
return widthFull && heightFull;
|
|
653
|
+
}
|
|
654
|
+
function stripChrome(document) {
|
|
655
|
+
let removed = 0;
|
|
656
|
+
const consentEls = document.querySelectorAll(CONSENT_SELECTORS.join(","));
|
|
657
|
+
for (const el of consentEls) {
|
|
658
|
+
if (!el.isConnected) continue;
|
|
659
|
+
el.remove();
|
|
660
|
+
removed++;
|
|
500
661
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
text: headingText(text)
|
|
508
|
-
});
|
|
509
|
-
units.push({
|
|
510
|
-
headingContext: context(),
|
|
511
|
-
kind: "heading",
|
|
512
|
-
text
|
|
513
|
-
});
|
|
514
|
-
continue;
|
|
662
|
+
const styledEls = document.querySelectorAll("[style]");
|
|
663
|
+
for (const el of styledEls) {
|
|
664
|
+
if (!el.isConnected) continue;
|
|
665
|
+
if (isFullViewportOverlay(el.getAttribute("style") ?? "")) {
|
|
666
|
+
el.remove();
|
|
667
|
+
removed++;
|
|
515
668
|
}
|
|
516
|
-
units.push({
|
|
517
|
-
headingContext: context(),
|
|
518
|
-
kind: block.kind === "code" ? "code" : "text",
|
|
519
|
-
text
|
|
520
|
-
});
|
|
521
669
|
}
|
|
522
|
-
return
|
|
670
|
+
return removed;
|
|
523
671
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (current.length === 0) currentContext = unit.headingContext;
|
|
543
|
-
current.push(unit);
|
|
544
|
-
}
|
|
545
|
-
flush();
|
|
546
|
-
return sections;
|
|
547
|
-
}
|
|
548
|
-
function joinedLength(units) {
|
|
549
|
-
if (units.length === 0) return 0;
|
|
550
|
-
let total = units[0].text.length;
|
|
551
|
-
for (let i = 1; i < units.length; i++) total += 2 + units[i].text.length;
|
|
552
|
-
return total;
|
|
672
|
+
var BOILERPLATE_TOKENS = [
|
|
673
|
+
"newsletter",
|
|
674
|
+
"newsletter-signup",
|
|
675
|
+
"mailing-list",
|
|
676
|
+
"email-signup",
|
|
677
|
+
"subscribe-form",
|
|
678
|
+
"signup-form",
|
|
679
|
+
"related-posts",
|
|
680
|
+
"related-post",
|
|
681
|
+
"read-next",
|
|
682
|
+
"more-from",
|
|
683
|
+
"you-might-also",
|
|
684
|
+
"recommended-posts",
|
|
685
|
+
"recommended"
|
|
686
|
+
];
|
|
687
|
+
function signatureMatches(el) {
|
|
688
|
+
const signature = `${el.getAttribute("class") ?? ""} ${el.getAttribute("id") ?? ""}`.toLowerCase();
|
|
689
|
+
return BOILERPLATE_TOKENS.some((token) => signature.includes(token));
|
|
553
690
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
691
|
+
var BOILERPLATE_CONTAINER_TAGS = /* @__PURE__ */ new Set([
|
|
692
|
+
"ASIDE",
|
|
693
|
+
"DIV",
|
|
694
|
+
"FORM",
|
|
695
|
+
"NAV",
|
|
696
|
+
"OL",
|
|
697
|
+
"SECTION",
|
|
698
|
+
"UL"
|
|
699
|
+
]);
|
|
700
|
+
function stripBoilerplate(document) {
|
|
701
|
+
const limit = .25 * document.body.textContent.length;
|
|
702
|
+
let removed = 0;
|
|
703
|
+
for (const el of document.querySelectorAll("[class],[id]")) {
|
|
704
|
+
if (!el.isConnected) continue;
|
|
705
|
+
if (!BOILERPLATE_CONTAINER_TAGS.has(el.tagName)) continue;
|
|
706
|
+
if (!signatureMatches(el)) continue;
|
|
707
|
+
let ancestor = el.parentElement;
|
|
708
|
+
while (ancestor && !signatureMatches(ancestor)) ancestor = ancestor.parentElement;
|
|
709
|
+
if (ancestor) continue;
|
|
710
|
+
if (el.textContent.length >= limit) continue;
|
|
711
|
+
el.remove();
|
|
712
|
+
removed++;
|
|
713
|
+
}
|
|
714
|
+
return removed;
|
|
560
715
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
716
|
+
function applySelectors(document, selectors) {
|
|
717
|
+
if (!selectors) return;
|
|
718
|
+
if (selectors.exclude) for (const selector of selectors.exclude) document.querySelectorAll(selector).forEach((el) => {
|
|
719
|
+
el.remove();
|
|
720
|
+
});
|
|
721
|
+
if (selectors.include) {
|
|
722
|
+
const body = document.body;
|
|
723
|
+
const root = body.querySelector(selectors.include);
|
|
724
|
+
if (root && root !== body) {
|
|
725
|
+
body.innerHTML = "";
|
|
726
|
+
body.appendChild(root);
|
|
570
727
|
}
|
|
571
728
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
729
|
+
}
|
|
730
|
+
var PLACEHOLDER_TOKENS = [
|
|
731
|
+
"placeholder",
|
|
732
|
+
"blank",
|
|
733
|
+
"spacer",
|
|
734
|
+
"lazy",
|
|
735
|
+
"loading",
|
|
736
|
+
"1x1",
|
|
737
|
+
"transparent",
|
|
738
|
+
"pixel",
|
|
739
|
+
"dummy"
|
|
740
|
+
];
|
|
741
|
+
function isPlaceholderSrc(src) {
|
|
742
|
+
if (!src) return true;
|
|
743
|
+
if (src.startsWith("data:")) return true;
|
|
744
|
+
const lowered = src.toLowerCase();
|
|
745
|
+
return PLACEHOLDER_TOKENS.some((token) => lowered.includes(token));
|
|
746
|
+
}
|
|
747
|
+
function usableAttr(el, names) {
|
|
748
|
+
for (const name of names) {
|
|
749
|
+
const value = el.getAttribute(name);
|
|
750
|
+
if (value) return value;
|
|
576
751
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
append(unit);
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
const pieces = unit.text.length > maxChars ? splitOversizedTextUnit(unit, maxChars) : [unit];
|
|
604
|
-
for (const piece of pieces) {
|
|
605
|
-
const pieceSep = current.length > 0 ? 2 : 0;
|
|
606
|
-
if (current.length > 0 && currentLen + pieceSep + piece.text.length > maxChars) emit();
|
|
607
|
-
append(piece);
|
|
608
|
-
}
|
|
752
|
+
}
|
|
753
|
+
function pickLargestSrcset(srcset) {
|
|
754
|
+
let bestUrl;
|
|
755
|
+
let bestValue = -1;
|
|
756
|
+
let firstUrl;
|
|
757
|
+
let sawDescriptor = false;
|
|
758
|
+
for (const raw of srcset.split(",")) {
|
|
759
|
+
const entry = raw.trim();
|
|
760
|
+
if (!entry) continue;
|
|
761
|
+
const splitAt = entry.search(/\s/);
|
|
762
|
+
const rawUrl = splitAt === -1 ? entry : entry.slice(0, splitAt);
|
|
763
|
+
const descriptor = splitAt === -1 ? "" : entry.slice(splitAt).trim();
|
|
764
|
+
const url = rawUrl.length >= 2 && (rawUrl.startsWith("\"") && rawUrl.endsWith("\"") || rawUrl.startsWith("'") && rawUrl.endsWith("'")) ? rawUrl.slice(1, -1) : rawUrl;
|
|
765
|
+
if (!url) continue;
|
|
766
|
+
if (firstUrl === void 0) firstUrl = url;
|
|
767
|
+
if (!descriptor) continue;
|
|
768
|
+
const value = /^(\d+(?:\.\d+)?)[wx]$/.exec(descriptor);
|
|
769
|
+
if (!value) continue;
|
|
770
|
+
sawDescriptor = true;
|
|
771
|
+
const numeric = Number.parseFloat(value[1]);
|
|
772
|
+
if (numeric > bestValue) {
|
|
773
|
+
bestValue = numeric;
|
|
774
|
+
bestUrl = url;
|
|
609
775
|
}
|
|
610
776
|
}
|
|
611
|
-
|
|
612
|
-
return groups;
|
|
777
|
+
return sawDescriptor ? bestUrl : firstUrl;
|
|
613
778
|
}
|
|
614
|
-
function
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
let carrierLen = 0;
|
|
624
|
-
for (let j = end - 1; j >= 0; j--) {
|
|
625
|
-
const unit = prev[j];
|
|
626
|
-
if (unit.kind !== "text") break;
|
|
627
|
-
const sep = carrier.length > 0 ? 2 : 0;
|
|
628
|
-
if (carrierLen + sep + unit.text.length > overlapChars) break;
|
|
629
|
-
carrier.unshift(unit);
|
|
630
|
-
carrierLen += sep + unit.text.length;
|
|
631
|
-
}
|
|
779
|
+
function resolveFromPicture(img) {
|
|
780
|
+
const picture = img.closest("picture");
|
|
781
|
+
if (!picture) return;
|
|
782
|
+
for (const source of picture.querySelectorAll("source")) {
|
|
783
|
+
if (source.hasAttribute("media")) continue;
|
|
784
|
+
const srcset = source.getAttribute("srcset");
|
|
785
|
+
if (srcset) {
|
|
786
|
+
const url = pickLargestSrcset(srcset);
|
|
787
|
+
if (url) return url;
|
|
632
788
|
}
|
|
633
|
-
const units = carrier.length === 0 ? group : [...carrier, ...group];
|
|
634
|
-
result.push({
|
|
635
|
-
headingContext: group[0]?.headingContext ?? "",
|
|
636
|
-
units
|
|
637
|
-
});
|
|
638
789
|
}
|
|
639
|
-
return result;
|
|
640
790
|
}
|
|
641
|
-
function
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
const
|
|
647
|
-
if (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
for (const group of groups) {
|
|
651
|
-
if (group.units.length === 0) continue;
|
|
652
|
-
const text = group.units.map((unit) => unit.text).join("\n\n").trim();
|
|
653
|
-
if (!text) continue;
|
|
654
|
-
chunks.push({
|
|
655
|
-
headingContext: group.headingContext,
|
|
656
|
-
index: chunks.length,
|
|
657
|
-
text,
|
|
658
|
-
tokenCount: Math.round(text.length / 4)
|
|
659
|
-
});
|
|
791
|
+
function resolveRealSource(img) {
|
|
792
|
+
const dataSrc = usableAttr(img, ["data-src"]);
|
|
793
|
+
if (dataSrc) return dataSrc;
|
|
794
|
+
const fromPicture = resolveFromPicture(img);
|
|
795
|
+
if (fromPicture) return fromPicture;
|
|
796
|
+
const ownSrcset = img.getAttribute("srcset");
|
|
797
|
+
if (ownSrcset) {
|
|
798
|
+
const url = pickLargestSrcset(ownSrcset);
|
|
799
|
+
if (url) return url;
|
|
660
800
|
}
|
|
661
|
-
return
|
|
801
|
+
return usableAttr(img, ["data-original", "data-lazy-src"]);
|
|
662
802
|
}
|
|
663
|
-
function
|
|
664
|
-
|
|
665
|
-
|
|
803
|
+
function resolveLazyImages(document) {
|
|
804
|
+
let resolved = 0;
|
|
805
|
+
for (const img of document.querySelectorAll("img")) {
|
|
806
|
+
const currentSrc = img.getAttribute("src") ?? "";
|
|
807
|
+
if (!isPlaceholderSrc(currentSrc)) continue;
|
|
808
|
+
const real = resolveRealSource(img);
|
|
809
|
+
if (real && real !== currentSrc) {
|
|
810
|
+
img.setAttribute("src", real);
|
|
811
|
+
resolved++;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return resolved;
|
|
666
815
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
var
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
"
|
|
816
|
+
var HIGHLIGHT_SOURCE_PREFIX = "highlight-source-";
|
|
817
|
+
var LANGUAGE_PREFIX = "language-";
|
|
818
|
+
var LANG_PREFIX = "lang-";
|
|
819
|
+
var SP_PREFIX = "sp-";
|
|
820
|
+
var BRUSH_RE = /brush:\s*([A-Za-z][\w-]*)/;
|
|
821
|
+
var BOGUS_TOKENS = /* @__PURE__ */ new Set([
|
|
822
|
+
"",
|
|
823
|
+
"highlight",
|
|
824
|
+
"source",
|
|
825
|
+
"sp"
|
|
674
826
|
]);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
]
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
exclude: z.array(z.string()).optional().describe("CSS selectors for boilerplate to remove before extraction (e.g. [\"nav\", \"footer\", \"[role=banner]\"]).")
|
|
702
|
-
}).optional().describe("Scope the extracted/converted content by CSS selector before processing.");
|
|
703
|
-
var readabilityOverridesSchema = z.record(z.string(), z.unknown()).optional().describe("Escape hatch: a record spread verbatim into the Readability options. Unstable and unvalidated; overrides the extraction/keepClasses/maxNodes/minArticleLength knobs.");
|
|
704
|
-
var chunkStrategySchema = z.enum(["char", "semantic"]);
|
|
705
|
-
var chunkOptionsSchema = z.object({
|
|
706
|
-
maxTokens: z.number().int().min(1).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)."),
|
|
707
|
-
overlap: z.number().int().min(0).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.").default(0),
|
|
708
|
-
strategy: chunkStrategySchema.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).").default("semantic")
|
|
709
|
-
}).describe("Token-bounded chunking options for splitting the extracted markdown into RAG/embedding-ready slices.");
|
|
710
|
-
var turndownOptionsShape = {
|
|
711
|
-
debug: z.boolean().describe("Emit per-stage timings (normalize, readability, sanitize, turndown, metadata) under diagnostics.trace. Debug-only — leaves trace absent by default.").default(false),
|
|
712
|
-
cleanChrome: z.boolean().describe("Strip browser chrome (scrollbars, consent/cookie banners, fixed nav and overlays) before conversion. These elements poison Readability density scoring and clutter fragment output.").default(true),
|
|
713
|
-
codeBlockStyle: codeBlockStyleSchema.describe("Markdown code-block style: 'fenced' (triple backticks) or 'indented' (four-space).").default("fenced"),
|
|
714
|
-
format: formatSchema.describe("Returned payload format: 'markdown' (default), 'html', 'text', or 'json' (emits {metadata, content, diagnostics}).").default("markdown"),
|
|
715
|
-
gfm: z.boolean().describe("Enable GitHub-Flavored Markdown: tables, strikethrough, and task lists.").default(true),
|
|
716
|
-
headingStyle: headingStyleSchema.describe("Markdown heading style: 'atx' (#) or 'setext' (underlining with = / -).").default("atx"),
|
|
717
|
-
images: imageModeSchema.describe("Image handling: 'keep' (inline ), 'drop', 'src-only' (bare URL text), or 'reference' (link-reference style).").default("keep"),
|
|
718
|
-
maxChars: z.number().int().min(0).describe("Truncate markdown/text output at a block boundary; never splits a fenced code block. Ignored for html/json formats.").optional(),
|
|
719
|
-
metadataMode: metadataModeSchema.describe("Prepend a metadata block to the markdown/text payload: 'none' (default), 'yaml', or 'json'.").default("none"),
|
|
720
|
-
sanitize: z.boolean().describe("Run DOMPurify over the extracted/fragment HTML before conversion (strips scripts, event handlers, and iframes).").default(true),
|
|
721
|
-
tables: tableFormatSchema.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.").optional(),
|
|
722
|
-
baseUrl: z.url().describe("Base URL for absolutizing relative links and images. NEVER fetched — origin context only.").optional(),
|
|
723
|
-
wordsPerMinute: z.number().int().min(1).describe("Reading speed (words per minute) used to compute metadata.readingTimeMin.").default(200)
|
|
724
|
-
};
|
|
725
|
-
var extractInputShape = {
|
|
726
|
-
localPath: localPathField,
|
|
727
|
-
...turndownOptionsShape,
|
|
728
|
-
cache: z.boolean().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.").default(false),
|
|
729
|
-
extraction: extractionSchema.describe("Readability scoring aggressiveness: 'balanced' (default), 'aggressive', or 'conservative'. Maps to Readability's scorer knobs.").default("balanced"),
|
|
730
|
-
keepClasses: z.boolean().describe("Retain all CSS classes on extracted nodes. Defaults false, which strips non-language classes.").default(false),
|
|
731
|
-
maxNodes: z.number().int().min(0).describe("Hard cap on elements parsed (Readability maxElemsToParse). Safety/perf guard for very large documents.").optional(),
|
|
732
|
-
minArticleLength: z.number().int().min(0).describe("Minimum article character length below which extraction falls back to the selector cascade (Readability charThreshold).").optional(),
|
|
733
|
-
readabilityOverrides: readabilityOverridesSchema,
|
|
734
|
-
selectors: selectorsSchema,
|
|
735
|
-
chunk: chunkOptionsSchema.optional().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."),
|
|
736
|
-
imageInventory: z.boolean().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.").default(false)
|
|
737
|
-
};
|
|
738
|
-
var extractInputSchema = z.object(extractInputShape);
|
|
739
|
-
var htmlToMarkdownInputShape = {
|
|
740
|
-
localPath: localPathField,
|
|
741
|
-
...turndownOptionsShape,
|
|
742
|
-
selectors: selectorsSchema
|
|
743
|
-
};
|
|
744
|
-
var htmlToMarkdownInputSchema = z.object(htmlToMarkdownInputShape);
|
|
745
|
-
var outlineInputShape = {
|
|
746
|
-
localPath: localPathField,
|
|
747
|
-
baseUrl: z.url().describe("Base URL, carried through to metadata.baseUrl and used to absolutize links. NEVER fetched — origin context only.").optional(),
|
|
748
|
-
selectors: selectorsSchema
|
|
749
|
-
};
|
|
750
|
-
var outlineInputSchema = z.object(outlineInputShape);
|
|
751
|
-
var extractMetadataInputShape = {
|
|
752
|
-
localPath: localPathField,
|
|
753
|
-
baseUrl: z.url().describe("Base URL, carried through to metadata.baseUrl and used to absolutize links. NEVER fetched — origin context only.").optional()
|
|
754
|
-
};
|
|
755
|
-
var extractMetadataInputSchema = z.object(extractMetadataInputShape);
|
|
756
|
-
var extractLinksInputShape = {
|
|
757
|
-
localPath: localPathField,
|
|
758
|
-
baseUrl: z.url().describe("Base URL for absolutizing relative hrefs and computing isExternal. NEVER fetched — origin context only.").optional(),
|
|
759
|
-
sameOriginOnly: z.boolean().describe("Drop cross-origin links; keep same-origin, relative, and fragment links.").default(false),
|
|
760
|
-
selectors: selectorsSchema
|
|
761
|
-
};
|
|
762
|
-
var extractLinksInputSchema = z.object(extractLinksInputShape);
|
|
763
|
-
var chunkTextInputShape = {
|
|
764
|
-
text: z.string().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."),
|
|
765
|
-
maxTokens: z.number().int().min(1).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).").default(500),
|
|
766
|
-
overlap: z.number().int().min(0).describe("Tokens to overlap between consecutive chunks (>=0). The trailing overlapChars of chunk N becomes the leading context of chunk N+1.").default(0),
|
|
767
|
-
strategy: chunkStrategySchema.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.").default("semantic")
|
|
768
|
-
};
|
|
769
|
-
var chunkTextInputSchema = z.object(chunkTextInputShape);
|
|
770
|
-
var extractSectionInputShape = {
|
|
771
|
-
localPath: localPathField,
|
|
772
|
-
baseUrl: z.url().describe("Base URL for absolutizing relative links and images. NEVER fetched — origin context only.").optional(),
|
|
773
|
-
selector: z.string().describe("CSS selector scoping extraction to one subtree; passed straight through as selectors.include. Provide exactly one of selector/heading.").optional(),
|
|
774
|
-
heading: z.string().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.").optional()
|
|
775
|
-
};
|
|
776
|
-
var extractSectionInputSchema = z.object(extractSectionInputShape).superRefine((value, ctx) => {
|
|
777
|
-
if (value.selector !== void 0 === (value.heading !== void 0)) ctx.addIssue({
|
|
778
|
-
code: "custom",
|
|
779
|
-
message: "Provide exactly one of `selector` or `heading` (both set or both unset is invalid).",
|
|
780
|
-
path: ["selector"]
|
|
781
|
-
});
|
|
782
|
-
});
|
|
783
|
-
var extractTablesInputShape = {
|
|
784
|
-
localPath: localPathField,
|
|
785
|
-
baseUrl: z.url().describe("Base URL, carried through to metadata.baseUrl. NEVER fetched — origin context only.").optional(),
|
|
786
|
-
format: tableFormatSchema.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).").default("gfm"),
|
|
787
|
-
selectors: selectorsSchema
|
|
788
|
-
};
|
|
789
|
-
var extractTablesInputSchema = z.object(extractTablesInputShape);
|
|
790
|
-
var extractListInputShape = {
|
|
791
|
-
localPath: localPathField,
|
|
792
|
-
baseUrl: z.url().describe("Base URL for absolutizing item hrefs against. NEVER fetched — origin context only.").optional(),
|
|
793
|
-
selectors: selectorsSchema
|
|
794
|
-
};
|
|
795
|
-
var extractListInputSchema = z.object(extractListInputShape);
|
|
796
|
-
var extractGridInputShape = {
|
|
797
|
-
localPath: localPathField,
|
|
798
|
-
baseUrl: z.url().describe("Base URL, carried through to metadata.baseUrl. NEVER fetched — origin context only.").optional(),
|
|
799
|
-
format: tableFormatSchema.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).").default("gfm"),
|
|
800
|
-
selectors: selectorsSchema,
|
|
801
|
-
rowSelector: z.string().describe("CSS selector for repeating row containers. When set WITH cellSelector, selector mode is used (no auto-detection). Example: '[class*=\"estimate-row\"]'.").optional(),
|
|
802
|
-
cellSelector: z.string().describe("CSS selector for cells within each row (scoped to the row subtree). Required together with rowSelector for selector mode. Example: '[class*=\"cell\"]'.").optional()
|
|
803
|
-
};
|
|
804
|
-
var extractGridInputSchema = z.object(extractGridInputShape).superRefine((value, ctx) => {
|
|
805
|
-
if (value.rowSelector !== void 0 !== (value.cellSelector !== void 0)) ctx.addIssue({
|
|
806
|
-
code: "custom",
|
|
807
|
-
message: "Provide both `rowSelector` and `cellSelector` for selector mode, or neither for auto-detection (setting only one is invalid).",
|
|
808
|
-
path: ["rowSelector"]
|
|
827
|
+
function isValidToken(token) {
|
|
828
|
+
return !BOGUS_TOKENS.has(token);
|
|
829
|
+
}
|
|
830
|
+
function hasLanguageClass(el) {
|
|
831
|
+
for (const cls of el.classList) if (cls !== LANGUAGE_PREFIX && cls.startsWith(LANGUAGE_PREFIX)) return true;
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
function collectClassSources(pre, code) {
|
|
835
|
+
const sources = [{
|
|
836
|
+
classes: [...pre.classList],
|
|
837
|
+
raw: pre.getAttribute("class") ?? ""
|
|
838
|
+
}];
|
|
839
|
+
let depth = 0;
|
|
840
|
+
let el = pre.parentElement;
|
|
841
|
+
while (el && depth < 2) {
|
|
842
|
+
sources.push({
|
|
843
|
+
classes: [...el.classList],
|
|
844
|
+
raw: el.getAttribute("class") ?? ""
|
|
845
|
+
});
|
|
846
|
+
depth++;
|
|
847
|
+
if (el.tagName === "DIV" && [...el.classList].some((c) => c.startsWith("highlight"))) break;
|
|
848
|
+
el = el.parentElement;
|
|
849
|
+
}
|
|
850
|
+
if (code) sources.push({
|
|
851
|
+
classes: [...code.classList],
|
|
852
|
+
raw: code.getAttribute("class") ?? ""
|
|
809
853
|
});
|
|
810
|
-
|
|
854
|
+
return sources;
|
|
855
|
+
}
|
|
856
|
+
function resolveCodeToken(pre, code) {
|
|
857
|
+
const sources = collectClassSources(pre, code);
|
|
858
|
+
function findByPrefix(prefix) {
|
|
859
|
+
for (const src of sources) for (const cls of src.classes) if (cls.startsWith(prefix)) {
|
|
860
|
+
const token = cls.slice(prefix.length).toLowerCase();
|
|
861
|
+
if (isValidToken(token)) return token;
|
|
862
|
+
}
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
const highlightToken = findByPrefix(HIGHLIGHT_SOURCE_PREFIX);
|
|
866
|
+
if (highlightToken) return {
|
|
867
|
+
token: highlightToken,
|
|
868
|
+
fromHighlightSource: true
|
|
869
|
+
};
|
|
870
|
+
const languageToken = findByPrefix(LANGUAGE_PREFIX);
|
|
871
|
+
if (languageToken) return {
|
|
872
|
+
token: languageToken,
|
|
873
|
+
fromHighlightSource: false
|
|
874
|
+
};
|
|
875
|
+
const langToken = findByPrefix(LANG_PREFIX);
|
|
876
|
+
if (langToken) return {
|
|
877
|
+
token: langToken,
|
|
878
|
+
fromHighlightSource: false
|
|
879
|
+
};
|
|
880
|
+
for (const src of sources) for (const cls of src.classes) {
|
|
881
|
+
if (!cls.startsWith(SP_PREFIX)) continue;
|
|
882
|
+
const token = cls.slice(3).toLowerCase();
|
|
883
|
+
if (KNOWN_LANGUAGE_TOKENS.has(token)) return {
|
|
884
|
+
token,
|
|
885
|
+
fromHighlightSource: false
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
for (const src of sources) {
|
|
889
|
+
const match = BRUSH_RE.exec(src.raw);
|
|
890
|
+
if (match) {
|
|
891
|
+
const token = match[1].toLowerCase();
|
|
892
|
+
if (isValidToken(token)) return {
|
|
893
|
+
token,
|
|
894
|
+
fromHighlightSource: false
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
function canonicalizePre(pre) {
|
|
901
|
+
const code = pre.querySelector("code");
|
|
902
|
+
if (code && hasLanguageClass(code)) return false;
|
|
903
|
+
const match = resolveCodeToken(pre, code);
|
|
904
|
+
if (!match) return false;
|
|
905
|
+
let target = code;
|
|
906
|
+
if (!target) {
|
|
907
|
+
target = pre.ownerDocument.createElement("code");
|
|
908
|
+
while (pre.firstChild) target.appendChild(pre.firstChild);
|
|
909
|
+
pre.appendChild(target);
|
|
910
|
+
}
|
|
911
|
+
const classes = [`language-${match.token}`];
|
|
912
|
+
if (target.classList.contains("hljs")) classes.push("hljs");
|
|
913
|
+
target.setAttribute("class", classes.join(" "));
|
|
914
|
+
if (match.fromHighlightSource) {
|
|
915
|
+
const parent = pre.parentElement;
|
|
916
|
+
if (parent?.tagName === "DIV" && [...parent.classList].some((c) => c.startsWith("highlight"))) parent.replaceWith(pre);
|
|
917
|
+
}
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
function canonicalizeCodeBlocks(document) {
|
|
921
|
+
let count = 0;
|
|
922
|
+
for (const pre of document.querySelectorAll("pre")) {
|
|
923
|
+
if (!pre.isConnected) continue;
|
|
924
|
+
try {
|
|
925
|
+
if (canonicalizePre(pre)) count++;
|
|
926
|
+
} catch {}
|
|
927
|
+
}
|
|
928
|
+
return count;
|
|
929
|
+
}
|
|
811
930
|
//#endregion
|
|
812
|
-
//#region src/
|
|
813
|
-
var
|
|
814
|
-
|
|
815
|
-
canonical: z.string().optional().describe("Declared canonical URL from <link rel=\"canonical\"> (or og:url as fallback). Distinct from baseUrl, which is the origin context passed in."),
|
|
816
|
-
estimator: z.string().optional().describe("Name of the heuristic backing tokenEstimate (e.g. \"chars/4\")."),
|
|
817
|
-
excerpt: z.string().optional().describe("Short article summary produced by Readability."),
|
|
818
|
-
lang: z.string().optional().describe("Detected document language."),
|
|
819
|
-
publishedTime: z.string().optional().describe("Publication timestamp resolved from JSON-LD, <meta>, or <time> elements."),
|
|
820
|
-
readingTimeMin: z.number().int().optional().describe("Estimated reading time in minutes, derived from wordCount and wordsPerMinute."),
|
|
821
|
-
siteName: z.string().optional().describe("Publishing site name, resolved from OpenGraph or <meta>."),
|
|
822
|
-
structured: z.record(z.string(), z.unknown()).optional().describe("Parsed schema.org JSON-LD primary object (Recipe/Product/Event/HowTo/Article…) when present — the raw graph node with @context stripped and @type normalized to a \"+\"-joined string. Absent when the page has no recognizable structured data."),
|
|
823
|
-
title: z.string().optional().describe("Article title, resolved by priority cascade (JSON-LD → OpenGraph → Twitter → <meta> → Readability → <title>)."),
|
|
824
|
-
tokenEstimate: z.number().int().optional().describe("Rough output token count (chars/4 by default) for context budgeting."),
|
|
825
|
-
baseUrl: z.string().optional().describe("The baseUrl passed in (origin context)."),
|
|
826
|
-
wordCount: z.number().int().optional().describe("Number of whitespace-separated words in the extracted text.")
|
|
827
|
-
}).describe("Resolved article metadata. Each field is the first non-empty value across a priority cascade.");
|
|
828
|
-
var chunkObjectSchema = z.object({
|
|
829
|
-
index: z.number().int().min(0).describe("Zero-based chunk position within the emitted sequence."),
|
|
830
|
-
text: z.string().describe("The chunk body (markdown or text), trimmed, sized to stay within the requested token budget."),
|
|
831
|
-
tokenCount: z.number().int().min(0).describe("Estimated token count of text (chars/4), same heuristic as metadata.tokenEstimate."),
|
|
832
|
-
headingContext: z.string().describe("Nearest preceding markdown heading text in effect at the chunk’s first block. Empty string when the chunk precedes any heading; carried from the overlap source when a chunk begins with overlap text.")
|
|
833
|
-
}).describe("One token-bounded slice of the extracted markdown, with its section heading for context.");
|
|
834
|
-
var imageEntrySchema = z.object({
|
|
835
|
-
src: z.string().describe("Absolute (resolved) image URL, absolutized against baseUrl."),
|
|
836
|
-
alt: z.string().describe("The img alt attribute, or empty string when absent."),
|
|
837
|
-
width: z.number().int().optional().describe("Pixel dimension from the attribute, when present."),
|
|
838
|
-
height: z.number().int().optional().describe("Pixel dimension from the attribute, when present."),
|
|
839
|
-
caption: z.string().describe("figcaption text from the enclosing <figure>, else alt.")
|
|
840
|
-
}).describe("One extracted image with resolved source and caption.");
|
|
841
|
-
var outputSchemaShape = {
|
|
842
|
-
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
843
|
-
content: z.string().describe("The human/LLM-readable payload — Markdown/html/text, or the serialized JSON when format=json."),
|
|
844
|
-
chunks: z.array(chunkObjectSchema).optional().describe("Token-bounded chunks of the extracted markdown, populated by `extract` only when the `chunk` option is set and the format yields a markdown/text body. Absent for html_to_markdown and for html/json extract formats."),
|
|
845
|
-
images: z.array(imageEntrySchema).optional().describe("Inventory of article images (absolute src, alt, dimensions, caption); populated only when imageInventory:true is passed to extract."),
|
|
846
|
-
metadata: metadataObjectSchema,
|
|
847
|
-
diagnostics: z.object({
|
|
848
|
-
boilerplateRemoved: z.number().int().optional().describe("Count of boilerplate blocks (related-posts, newsletter signup, read-next) stripped before conversion."),
|
|
849
|
-
cache: z.object({
|
|
850
|
-
hit: z.boolean().describe("True when this call was served from the in-memory cache without re-running the pipeline."),
|
|
851
|
-
normalizedHash: z.string().describe("sha256 of the volatility-normalized HTML (whitespace collapsed, scripts/nonce/CSP/generated ids stripped) — the actual cache key, shared across re-renders that differ only in volatile markup."),
|
|
852
|
-
originalHash: z.string().describe("sha256 of the raw HTML as passed (trimmed). Differs from normalizedHash when volatile markup (nonce/CSP/generated ids) was collapsed; equal when the input was already stable. Used to diagnose should-have-hit-but-didn’t misses.")
|
|
853
|
-
}).optional().describe("Cache signal — populated only by `extract` when called with cache:true. Absent otherwise (goldens, default path, and other tools never emit this field)."),
|
|
854
|
-
chromeRemoved: z.number().int().optional().describe("Count of browser-chrome nodes stripped before conversion (scrollbars, consent banners, overlays)."),
|
|
855
|
-
extractedNode: z.string().optional().describe("DOM root extraction came from: \"readability\" (main path), a fallback selector (e.g. \"article\", \"main\"), or \"fragment\" for html_to_markdown."),
|
|
856
|
-
fallbackUsed: z.boolean().describe("True if Readability parse failed and a selector cascade salvaged content. Always true for html_to_markdown."),
|
|
857
|
-
gated: z.object({
|
|
858
|
-
likely: z.boolean().describe("True when heuristics strongly suggest the content is paywalled or truncated."),
|
|
859
|
-
reason: z.string().describe("Short label naming the detected signal (e.g. \"paywall overlay\", \"metered paywall message\").")
|
|
860
|
-
}).optional().describe("Likely paywall / gating signal. The extraction may be partial; the host can re-capture after authenticating. Detection only — this server never fetches or authenticates."),
|
|
861
|
-
imagesResolved: z.number().int().optional().describe("Count of lazy/placeholder images resolved to their real src before conversion."),
|
|
862
|
-
pagination: z.object({
|
|
863
|
-
type: z.enum(["infinite", "paginated"]).describe("Kind of pagination signal detected in the document."),
|
|
864
|
-
nextUrl: z.string().optional().describe("Absolute URL of the detected next page (paginated only). Mirrors the href found in the DOM; never fetched by this server."),
|
|
865
|
-
selector: z.string().optional().describe("CSS selector of the detected load-more / infinite-scroll sentinel (infinite only).")
|
|
866
|
-
}).optional().describe("Detected pagination or infinite-scroll signal. Detection only — the host drives loading; this server never fetches."),
|
|
867
|
-
readerable: z.boolean().optional().describe("Readability isProbablyReaderable verdict on the document (extract main path only)."),
|
|
868
|
-
removedNodes: z.number().int().optional().describe("Net element count removed across the whole pipeline (delta vs. the parsed document)."),
|
|
869
|
-
sanitization: z.object({
|
|
870
|
-
iframes: z.number().int().describe("<iframe> elements removed by sanitization."),
|
|
871
|
-
scripts: z.number().int().describe("<script> and event-handler nodes removed by sanitization.")
|
|
872
|
-
}).optional().describe("Counts of nodes removed by DOMPurify sanitization."),
|
|
873
|
-
truncated: z.boolean().describe("True if the payload was truncated by maxChars."),
|
|
874
|
-
trace: z.array(z.object({
|
|
875
|
-
stage: z.string().describe("Pipeline stage name (e.g. \"normalize\", \"readability\", \"sanitize\", \"turndown\", \"metadata\")."),
|
|
876
|
-
ms: z.number().describe("Wall-clock duration of the stage in milliseconds, measured via performance.now().")
|
|
877
|
-
}).describe("One timed pipeline stage.")).optional().describe("Per-stage timings emitted only when debug:true is passed to extract/html_to_markdown. Stages are non-overlapping and ordered; absent otherwise.")
|
|
878
|
-
}).describe("Pipeline telemetry describing what was extracted, sanitized, and removed.")
|
|
879
|
-
};
|
|
880
|
-
z.object(outputSchemaShape);
|
|
881
|
-
var outlineOutputShape = {
|
|
882
|
-
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
883
|
-
content: z.string().describe("Indented-bullet table of contents, one line per heading, nested by depth."),
|
|
884
|
-
outline: z.array(z.object({
|
|
885
|
-
level: z.number().int().min(1).max(6).describe("Heading level (1–6)."),
|
|
886
|
-
text: z.string().describe("Heading text content."),
|
|
887
|
-
anchor: z.string().describe("Stable anchor id: the heading own id, a descendant permalink fragment, or a slug of the text (deduped -1, -2, … for generated slugs).")
|
|
888
|
-
}).describe("A single document heading with its stable anchor.")).describe("Document headings (h1–h6) in document order, each with a stable anchor id."),
|
|
889
|
-
metadata: z.object({
|
|
890
|
-
title: z.string().optional().describe("Document title from <title>, falling back to the first <h1>."),
|
|
891
|
-
baseUrl: z.string().optional().describe("The baseUrl passed in (origin context, never fetched).")
|
|
892
|
-
}).describe("Outline document metadata.")
|
|
893
|
-
};
|
|
894
|
-
z.object(outlineOutputShape);
|
|
895
|
-
var extractMetadataOutputShape = {
|
|
896
|
-
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
897
|
-
content: z.string().describe("Human-readable key:value rendering of the metadata block, so content[0].text is never empty."),
|
|
898
|
-
metadata: metadataObjectSchema
|
|
899
|
-
};
|
|
900
|
-
z.object(extractMetadataOutputShape);
|
|
901
|
-
var chunkTextOutputShape = {
|
|
902
|
-
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
903
|
-
content: z.string().describe("Readable index of the chunks (one numbered section per chunk, each prefixed with its heading context), so content[0].text is always scannable."),
|
|
904
|
-
chunks: z.array(chunkObjectSchema).describe("The emitted chunks in order. Empty array when the input contains no non-whitespace content.")
|
|
905
|
-
};
|
|
906
|
-
z.object(chunkTextOutputShape);
|
|
907
|
-
var linkObjectSchema = z.object({
|
|
908
|
-
text: z.string().describe("Anchor text content, whitespace-collapsed and trimmed (capped at 300 chars)."),
|
|
909
|
-
href: z.string().describe("Absolute href (resolved against baseUrl when provided); unchanged when baseUrl is absent or the pair fails to parse."),
|
|
910
|
-
rel: z.string().describe("The raw rel attribute value (e.g. \"noopener noreferrer\", \"nofollow\"), or the empty string when absent."),
|
|
911
|
-
isExternal: z.boolean().describe("True when baseUrl is provided and the href parses to a different origin than baseUrl. False for relative, fragment, same-origin, non-http(s) (mailto/tel/javascript), and malformed hrefs.")
|
|
912
|
-
}).describe("A single anchor link with its text, absolute href, rel, and origin.");
|
|
913
|
-
var extractLinksOutputShape = {
|
|
914
|
-
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
915
|
-
content: z.string().describe("Readable rendering of the link list (one `- [text](href)` line per link), so content[0].text is never empty."),
|
|
916
|
-
links: z.array(linkObjectSchema).describe("Anchors in document order, hrefs absolutized against baseUrl. No deduplication."),
|
|
917
|
-
metadata: z.object({ baseUrl: z.string().optional().describe("The baseUrl passed in (origin context, never fetched).") }).describe("Extract-links document metadata.")
|
|
918
|
-
};
|
|
919
|
-
z.object(extractLinksOutputShape);
|
|
920
|
-
var tableEntrySchema = z.object({
|
|
921
|
-
index: z.number().int().min(0).describe("0-based position among emitted tables."),
|
|
922
|
-
rows: z.number().int().min(0).describe("Row count of the matrix (after rowspan/colspan resolution)."),
|
|
923
|
-
cols: z.number().int().min(0).describe("Column count of the matrix (after colspan resolution)."),
|
|
924
|
-
markdown: z.string().describe("The table rendered in the requested format (gfm/csv/json).")
|
|
925
|
-
}).describe("One extracted table with its dimensions and rendered form.");
|
|
926
|
-
var extractTablesOutputShape = {
|
|
927
|
-
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
928
|
-
content: z.string().describe("All tables rendered in the requested format, joined by blank lines; \"(no tables found)\" when none."),
|
|
929
|
-
tables: z.array(tableEntrySchema).describe("Every <table> on the page (rowspan/colspan-resolved), in document order."),
|
|
930
|
-
metadata: z.object({
|
|
931
|
-
baseUrl: z.string().optional().describe("The baseUrl passed in (origin context)."),
|
|
932
|
-
format: tableFormatSchema.describe("The requested render format."),
|
|
933
|
-
tableCount: z.number().int().describe("Number of tables emitted.")
|
|
934
|
-
}).describe("Tables-tool metadata.")
|
|
935
|
-
};
|
|
936
|
-
z.object(extractTablesOutputShape);
|
|
937
|
-
var gridEntrySchema = z.object({
|
|
938
|
-
rows: z.number().int().min(0).describe("Row count of the detected grid (after ragged-row padding)."),
|
|
939
|
-
cols: z.number().int().min(0).describe("Column count of the detected grid (max cell width across rows)."),
|
|
940
|
-
markdown: z.string().describe("The grid rendered in the requested format (gfm/csv/json).")
|
|
941
|
-
}).describe("One detected grid with its dimensions and rendered form.");
|
|
942
|
-
var extractGridOutputShape = {
|
|
943
|
-
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
944
|
-
content: z.string().describe("The grid rendered in the requested format, or \"(no repeating grid found)\" when no grid is detected."),
|
|
945
|
-
grid: gridEntrySchema.describe("The detected grid (dimensions + rendered markdown). Rows/cols are 0 and markdown is empty when nothing is detected."),
|
|
946
|
-
diagnostics: z.object({
|
|
947
|
-
confidence: z.enum([
|
|
948
|
-
"high",
|
|
949
|
-
"low",
|
|
950
|
-
"medium"
|
|
951
|
-
]).describe("`high` when ≥6 detected data rows, `medium` when ≥3 (minRows), `low` otherwise. Counts the detected cluster only — a recovered header row is inference and does not raise it. `low` for non-grid pages."),
|
|
952
|
-
containerSelector: z.string().describe("CSS-ish hint (tag#id.class) of the winning container, or the rowSelector in selector mode. Empty when not detected."),
|
|
953
|
-
detected: z.boolean().describe("True when a repeating grid was found (≥3 same-shape sibling rows each with ≥2 direct element-children, outside nav/header/footer/aside)."),
|
|
954
|
-
rowCount: z.number().int().describe("Number of rows emitted, including any recovered header row. 0 when not detected."),
|
|
955
|
-
colCount: z.number().int().describe("Number of columns (max cell width across rows). 0 when not detected."),
|
|
956
|
-
rowTag: z.string().describe("Uppercase DOM tag name of the repeating row container (e.g. \"DIV\", \"TR\", \"LI\"). Empty when not detected."),
|
|
957
|
-
note: z.string().describe("Short human-readable status: the detection reason when detected, or the rejection reason when not.")
|
|
958
|
-
}).describe("Grid-detection telemetry describing the winning candidate."),
|
|
959
|
-
metadata: z.object({
|
|
960
|
-
baseUrl: z.string().optional().describe("The baseUrl passed in (origin context, never fetched)."),
|
|
961
|
-
format: tableFormatSchema.describe("The requested render format."),
|
|
962
|
-
detected: z.boolean().describe("Mirrors diagnostics.detected.")
|
|
963
|
-
}).describe("Extract-grid document metadata.")
|
|
964
|
-
};
|
|
965
|
-
z.object(extractGridOutputShape);
|
|
966
|
-
var listItemSchema = z.object({
|
|
967
|
-
score: z.number().int().describe("Item substance score: primary-anchor text length + non-link body text length. Long titles (real feed items) score higher than short nav labels; items with both a long title and surrounding body text (snippets, excerpts) score highest."),
|
|
968
|
-
snippet: z.string().describe("Item body text with the title peeled off, whitespace-collapsed and clipped at 200 chars. Empty when the item is title-only."),
|
|
969
|
-
title: z.string().describe("Primary anchor text (longest-text <a> in the item, whitespace-collapsed). Always non-empty for emitted items."),
|
|
970
|
-
url: z.string().describe("Absolute href of the primary anchor, resolved against baseUrl. Always non-empty for emitted items.")
|
|
971
|
-
}).describe("One detected list item with its title, URL, snippet, and score.");
|
|
972
|
-
var extractListOutputShape = {
|
|
973
|
-
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
974
|
-
content: z.string().describe("Readable rendering of the items (one numbered `title — url` line per item, each followed by an indented snippet), or a single `not a list: …` line when no list structure is detected."),
|
|
975
|
-
items: z.array(listItemSchema).describe("Detected list items in document order. Empty when the page has no repeated same-shape sibling structure with anchors (e.g. article pages)."),
|
|
976
|
-
diagnostics: z.object({
|
|
977
|
-
confidence: z.enum([
|
|
978
|
-
"high",
|
|
979
|
-
"low",
|
|
980
|
-
"medium"
|
|
981
|
-
]).describe("`high` when ≥6 items and avg score ≥30, `medium` when ≥3 items, `low` otherwise. `low` for non-list pages."),
|
|
982
|
-
containerSelector: z.string().describe("CSS-ish hint (tag#id.class) of the winning container. Empty when not detected."),
|
|
983
|
-
detected: z.boolean().describe("True when a list/feed/index structure was found (≥3 same-shape siblings each with a navigation anchor, outside nav/header/footer/aside)."),
|
|
984
|
-
itemCount: z.number().int().describe("Number of items emitted. 0 when not detected."),
|
|
985
|
-
itemTag: z.string().describe("Uppercase DOM tag name of the winning sibling group (e.g. \"TR\", \"LI\", \"ARTICLE\", \"DIV\"). Empty when not detected."),
|
|
986
|
-
note: z.string().describe("Short human-readable status: the detection reason when detected, or the rejection reason when not.")
|
|
987
|
-
}).describe("List-detection telemetry describing the winning candidate."),
|
|
988
|
-
metadata: z.object({ baseUrl: z.string().optional().describe("The baseUrl passed in (origin context, never fetched).") }).describe("Extract-list document metadata.")
|
|
989
|
-
};
|
|
990
|
-
z.object(extractListOutputShape);
|
|
991
|
-
//#endregion
|
|
992
|
-
//#region src/pipeline/dom.ts
|
|
993
|
-
function buildDocument(html, baseUrl) {
|
|
994
|
-
const dom = new JSDOM(html, { url: baseUrl });
|
|
931
|
+
//#region src/policy/text.ts
|
|
932
|
+
var TOKEN_ESTIMATOR = "chars/4";
|
|
933
|
+
function estimateTokens(textContent) {
|
|
995
934
|
return {
|
|
996
|
-
|
|
997
|
-
|
|
935
|
+
tokenEstimate: Math.round(textContent.length / 4),
|
|
936
|
+
estimator: TOKEN_ESTIMATOR
|
|
998
937
|
};
|
|
999
938
|
}
|
|
1000
|
-
function
|
|
1001
|
-
return
|
|
939
|
+
function countWords(text) {
|
|
940
|
+
return (text.match(/\S+/g) ?? []).length;
|
|
1002
941
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
var MARKER_CLASS = "rdrm-math";
|
|
1006
|
-
var DISPLAY_ATTR = "data-display";
|
|
1007
|
-
var KATEX_CLASS = "katex";
|
|
1008
|
-
var KATEX_DISPLAY_CLASS = "katex-display";
|
|
1009
|
-
var TEX_ANNOTATION_SELECTOR = "annotation[encoding=\"application/x-tex\"]";
|
|
1010
|
-
var MATHJAX_INLINE_TYPE = "math/tex";
|
|
1011
|
-
var MATHJAX_DISPLAY_TYPE = "math/tex; mode=display";
|
|
1012
|
-
var MATHML_DISPLAY_SELECTOR = ".ltx_equation, .ltx_displaymath, .equation-display, .math-display";
|
|
1013
|
-
var BROKEN_PLACEHOLDER = "[?]";
|
|
1014
|
-
function createMarker(document, tex, display) {
|
|
1015
|
-
const marker = document.createElement("span");
|
|
1016
|
-
marker.className = MARKER_CLASS;
|
|
1017
|
-
marker.setAttribute(DISPLAY_ATTR, display ? "true" : "false");
|
|
1018
|
-
marker.textContent = tex;
|
|
1019
|
-
return marker;
|
|
942
|
+
function nonEmpty(value) {
|
|
943
|
+
return value?.trim() ? value : void 0;
|
|
1020
944
|
}
|
|
1021
|
-
function
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
let container = annotation;
|
|
1029
|
-
let display = false;
|
|
1030
|
-
if (katex) {
|
|
1031
|
-
container = katex;
|
|
1032
|
-
display = katex.closest(`.${KATEX_DISPLAY_CLASS}`) !== null;
|
|
1033
|
-
} else if (math) {
|
|
1034
|
-
container = math;
|
|
1035
|
-
display = math.getAttribute("display") === "block" || math.closest(MATHML_DISPLAY_SELECTOR) !== null;
|
|
1036
|
-
}
|
|
1037
|
-
const tex = annotation.textContent.trim() || (math?.getAttribute("alttext") ?? "").trim();
|
|
1038
|
-
container.replaceWith(createMarker(document, tex || BROKEN_PLACEHOLDER, display));
|
|
1039
|
-
} catch {
|
|
1040
|
-
annotation.replaceWith(createMarker(document, BROKEN_PLACEHOLDER, false));
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
945
|
+
function computeTextMetrics(text, wordsPerMinute) {
|
|
946
|
+
const wordCount = countWords(text);
|
|
947
|
+
return {
|
|
948
|
+
wordCount,
|
|
949
|
+
readingTimeMin: wordCount === 0 ? 0 : Math.max(1, Math.round(wordCount / wordsPerMinute)),
|
|
950
|
+
...estimateTokens(text)
|
|
951
|
+
};
|
|
1043
952
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
953
|
+
//#endregion
|
|
954
|
+
//#region src/policy/markdown.ts
|
|
955
|
+
var processor = unified().use(remarkParse).use(remarkGfm);
|
|
956
|
+
var HEADING_MARKERS = /^#{1,6}\s+/;
|
|
957
|
+
function parseBlocks(source) {
|
|
958
|
+
const tree = processor.parse(source);
|
|
959
|
+
const blocks = [];
|
|
960
|
+
for (const node of tree.children) {
|
|
961
|
+
const start = node.position?.start.offset;
|
|
962
|
+
const end = node.position?.end.offset;
|
|
963
|
+
if (start === void 0 || end === void 0) continue;
|
|
964
|
+
const kind = node.type === "code" ? "code" : node.type === "heading" ? "heading" : "other";
|
|
965
|
+
const depth = node.type === "heading" ? node.depth : 0;
|
|
966
|
+
blocks.push({
|
|
967
|
+
depth,
|
|
968
|
+
end,
|
|
969
|
+
kind,
|
|
970
|
+
start
|
|
971
|
+
});
|
|
1051
972
|
}
|
|
973
|
+
return blocks;
|
|
1052
974
|
}
|
|
1053
|
-
function
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
975
|
+
function headingText(raw) {
|
|
976
|
+
return raw.replace(HEADING_MARKERS, "").trim();
|
|
977
|
+
}
|
|
978
|
+
function hardSplitLines(text, maxChars) {
|
|
979
|
+
const pieces = [];
|
|
980
|
+
let buffer = "";
|
|
981
|
+
function flush() {
|
|
982
|
+
if (buffer) {
|
|
983
|
+
pieces.push(buffer);
|
|
984
|
+
buffer = "";
|
|
1063
985
|
}
|
|
1064
986
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
987
|
+
for (const line of text.split("\n")) {
|
|
988
|
+
if (line.length > maxChars) {
|
|
989
|
+
flush();
|
|
990
|
+
for (let i = 0; i < line.length; i += maxChars) pieces.push(line.slice(i, i + maxChars));
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const candidate = buffer ? `${buffer}\n${line}` : line;
|
|
994
|
+
if (candidate.length > maxChars) {
|
|
995
|
+
flush();
|
|
996
|
+
buffer = line;
|
|
997
|
+
} else buffer = candidate;
|
|
998
|
+
}
|
|
999
|
+
flush();
|
|
1000
|
+
return pieces;
|
|
1070
1001
|
}
|
|
1071
1002
|
//#endregion
|
|
1072
|
-
//#region src/
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
"
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
"
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
"
|
|
1091
|
-
"
|
|
1092
|
-
"
|
|
1093
|
-
"
|
|
1094
|
-
"
|
|
1095
|
-
"
|
|
1096
|
-
"
|
|
1097
|
-
"
|
|
1098
|
-
"
|
|
1099
|
-
"
|
|
1100
|
-
"
|
|
1101
|
-
"
|
|
1102
|
-
"language-lisp",
|
|
1103
|
-
"language-lua",
|
|
1104
|
-
"language-md",
|
|
1105
|
-
"language-markdown",
|
|
1106
|
-
"language-objc",
|
|
1107
|
-
"language-objectivec",
|
|
1108
|
-
"language-perl",
|
|
1109
|
-
"language-php",
|
|
1110
|
-
"language-plaintext",
|
|
1111
|
-
"language-powershell",
|
|
1112
|
-
"language-py",
|
|
1113
|
-
"language-python",
|
|
1114
|
-
"language-r",
|
|
1115
|
-
"language-rb",
|
|
1116
|
-
"language-rs",
|
|
1117
|
-
"language-ruby",
|
|
1118
|
-
"language-rust",
|
|
1119
|
-
"language-scala",
|
|
1120
|
-
"language-sh",
|
|
1121
|
-
"language-shell",
|
|
1122
|
-
"language-sql",
|
|
1123
|
-
"language-swift",
|
|
1124
|
-
"language-text",
|
|
1125
|
-
"language-toml",
|
|
1126
|
-
"language-ts",
|
|
1127
|
-
"language-tsx",
|
|
1128
|
-
"language-typescript",
|
|
1129
|
-
"language-vim",
|
|
1130
|
-
"language-wasm",
|
|
1131
|
-
"language-xml",
|
|
1132
|
-
"language-yaml",
|
|
1133
|
-
"language-yml",
|
|
1134
|
-
"rdrm-math"
|
|
1003
|
+
//#region src/output/format.ts
|
|
1004
|
+
function dropEchoedTitle(body, title) {
|
|
1005
|
+
const blocks = parseBlocks(body);
|
|
1006
|
+
if (blocks.length === 0) return body;
|
|
1007
|
+
const first = blocks[0];
|
|
1008
|
+
if (first.kind === "heading" && headingText(body.slice(first.start, first.end)) === title.trim()) return body.slice(first.end).replace(/^\n+/, "");
|
|
1009
|
+
return body;
|
|
1010
|
+
}
|
|
1011
|
+
function renderMarkdown(input) {
|
|
1012
|
+
const title = input.metadata.title?.trim();
|
|
1013
|
+
let body = input.markdown;
|
|
1014
|
+
if (title) {
|
|
1015
|
+
body = dropEchoedTitle(body, title);
|
|
1016
|
+
return `# ${title}\n\n${body}`.replace(/\n+$/, "\n");
|
|
1017
|
+
}
|
|
1018
|
+
return body.replace(/\n+$/, "\n");
|
|
1019
|
+
}
|
|
1020
|
+
var METADATA_KEYS = [
|
|
1021
|
+
"title",
|
|
1022
|
+
"byline",
|
|
1023
|
+
"siteName",
|
|
1024
|
+
"lang",
|
|
1025
|
+
"publishedTime",
|
|
1026
|
+
"excerpt",
|
|
1027
|
+
"canonical",
|
|
1028
|
+
"baseUrl",
|
|
1029
|
+
"wordCount",
|
|
1030
|
+
"readingTimeMin",
|
|
1031
|
+
"tokenEstimate",
|
|
1032
|
+
"estimator"
|
|
1135
1033
|
];
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
nbTopCandidates: 10
|
|
1142
|
-
};
|
|
1143
|
-
case "balanced": return null;
|
|
1144
|
-
case "conservative": return {
|
|
1145
|
-
charThreshold: DEFAULT_CHAR_THRESHOLD * 2,
|
|
1146
|
-
nbTopCandidates: Math.max(1, Math.round(DEFAULT_N_TOP_CANDIDATES / 2))
|
|
1147
|
-
};
|
|
1034
|
+
function pickMetadata(metadata) {
|
|
1035
|
+
const picked = {};
|
|
1036
|
+
for (const key of METADATA_KEYS) {
|
|
1037
|
+
const value = metadata[key];
|
|
1038
|
+
if (value !== void 0) picked[key] = value;
|
|
1148
1039
|
}
|
|
1040
|
+
return picked;
|
|
1149
1041
|
}
|
|
1150
|
-
function
|
|
1151
|
-
|
|
1152
|
-
const keepClasses = input.keepClasses ?? false;
|
|
1153
|
-
const charThreshold = input.minArticleLength !== void 0 ? input.minArticleLength : modeKnobs?.charThreshold;
|
|
1154
|
-
const nbTopCandidates = modeKnobs?.nbTopCandidates;
|
|
1155
|
-
return {
|
|
1156
|
-
classesToPreserve: keepClasses ? [] : [...CLASSES_TO_PRESERVE],
|
|
1157
|
-
keepClasses,
|
|
1158
|
-
...charThreshold !== void 0 ? { charThreshold } : {},
|
|
1159
|
-
...nbTopCandidates !== void 0 ? { nbTopCandidates } : {},
|
|
1160
|
-
...input.maxNodes !== void 0 ? { maxElemsToParse: input.maxNodes } : {},
|
|
1161
|
-
...input.readabilityOverrides ?? {}
|
|
1162
|
-
};
|
|
1042
|
+
function yamlFrontmatter(metadata) {
|
|
1043
|
+
return `---\n${stringify(pickMetadata(metadata), { lineWidth: 0 })}---\n`;
|
|
1163
1044
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
var NONCE_ATTR = "nonce";
|
|
1167
|
-
function normalizeDocument(document, options) {
|
|
1168
|
-
extractMath(document);
|
|
1169
|
-
const baseEls = document.querySelectorAll("base");
|
|
1170
|
-
const scriptEls = document.querySelectorAll("script:not([type=\"application/ld+json\"])");
|
|
1171
|
-
baseEls.forEach((el) => {
|
|
1172
|
-
el.remove();
|
|
1173
|
-
});
|
|
1174
|
-
scriptEls.forEach((el) => {
|
|
1175
|
-
el.remove();
|
|
1176
|
-
});
|
|
1177
|
-
document.querySelectorAll(`[${NONCE_ATTR}]`).forEach((el) => {
|
|
1178
|
-
el.removeAttribute(NONCE_ATTR);
|
|
1179
|
-
});
|
|
1180
|
-
const chromeRemoved = options?.cleanChrome === false ? 0 : stripChrome(document);
|
|
1181
|
-
return {
|
|
1182
|
-
boilerplateRemoved: stripBoilerplate(document),
|
|
1183
|
-
chromeRemoved,
|
|
1184
|
-
iframes: 0,
|
|
1185
|
-
scripts: scriptEls.length
|
|
1186
|
-
};
|
|
1045
|
+
function jsonFrontmatter(metadata) {
|
|
1046
|
+
return "```json\n" + JSON.stringify(pickMetadata(metadata), null, 2) + "\n```\n";
|
|
1187
1047
|
}
|
|
1188
|
-
|
|
1189
|
-
"
|
|
1190
|
-
"
|
|
1191
|
-
|
|
1192
|
-
"#onetrust-consent-sdk",
|
|
1193
|
-
"#onetrust-pc-sdk",
|
|
1194
|
-
".cc-window",
|
|
1195
|
-
".cc-banner",
|
|
1196
|
-
".cc-revoke",
|
|
1197
|
-
".osano-cm-window",
|
|
1198
|
-
".osano-cm-dialog",
|
|
1199
|
-
".qc-cmp2-container",
|
|
1200
|
-
".qc-cmp-ui-container",
|
|
1201
|
-
"#sp_message_container",
|
|
1202
|
-
"[id^=\"sp_message_container_\"]",
|
|
1203
|
-
"#didomi-host",
|
|
1204
|
-
".didomi-popup-container",
|
|
1205
|
-
"#truste-consent-track",
|
|
1206
|
-
"#consent_blackbar",
|
|
1207
|
-
"#cookie-banner",
|
|
1208
|
-
".cookie-banner",
|
|
1209
|
-
"#consent-banner",
|
|
1210
|
-
".consent-banner",
|
|
1211
|
-
".cookie-bar",
|
|
1212
|
-
".gdpr-banner",
|
|
1213
|
-
".privacy-banner"
|
|
1214
|
-
];
|
|
1215
|
-
function isFullViewportOverlay(style) {
|
|
1216
|
-
if (!/position\s*:\s*(?:fixed|sticky)/i.test(style)) return false;
|
|
1217
|
-
const zIndexMatch = /z-index\s*:\s*(\d+)/i.exec(style);
|
|
1218
|
-
if (!zIndexMatch || Number.parseInt(zIndexMatch[1], 10) < 1e3) return false;
|
|
1219
|
-
if (/inset\s*:\s*0/i.test(style)) return true;
|
|
1220
|
-
const widthFull = /width\s*:\s*100(?:%|vw)/i.test(style) || /left\s*:\s*0/i.test(style) && /right\s*:\s*0/i.test(style);
|
|
1221
|
-
const heightFull = /height\s*:\s*100(?:%|vh)/i.test(style) || /top\s*:\s*0/i.test(style) && /bottom\s*:\s*0/i.test(style);
|
|
1222
|
-
return widthFull && heightFull;
|
|
1048
|
+
function withFrontmatter(payload, mode, metadata) {
|
|
1049
|
+
if (mode === "yaml") return `${yamlFrontmatter(metadata)}${payload}`;
|
|
1050
|
+
if (mode === "json") return `${jsonFrontmatter(metadata)}\n${payload}`;
|
|
1051
|
+
return payload;
|
|
1223
1052
|
}
|
|
1224
|
-
function
|
|
1225
|
-
let
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
if (isFullViewportOverlay(el.getAttribute("style") ?? "")) {
|
|
1236
|
-
el.remove();
|
|
1237
|
-
removed++;
|
|
1053
|
+
function formatPayload(input) {
|
|
1054
|
+
let payload;
|
|
1055
|
+
switch (input.format) {
|
|
1056
|
+
case "html": return input.sanitizedHtml;
|
|
1057
|
+
case "json": {
|
|
1058
|
+
const body = {
|
|
1059
|
+
metadata: input.metadata,
|
|
1060
|
+
content: input.markdown,
|
|
1061
|
+
diagnostics: input.diagnostics
|
|
1062
|
+
};
|
|
1063
|
+
return JSON.stringify(body, null, 2);
|
|
1238
1064
|
}
|
|
1065
|
+
case "text":
|
|
1066
|
+
payload = input.textContent;
|
|
1067
|
+
break;
|
|
1068
|
+
default: payload = renderMarkdown(input);
|
|
1239
1069
|
}
|
|
1240
|
-
return
|
|
1070
|
+
return withFrontmatter(payload, input.metadataMode, input.metadata);
|
|
1241
1071
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
"email-signup",
|
|
1247
|
-
"subscribe-form",
|
|
1248
|
-
"signup-form",
|
|
1249
|
-
"related-posts",
|
|
1250
|
-
"related-post",
|
|
1251
|
-
"read-next",
|
|
1252
|
-
"more-from",
|
|
1253
|
-
"you-might-also",
|
|
1254
|
-
"recommended-posts",
|
|
1255
|
-
"recommended"
|
|
1256
|
-
];
|
|
1257
|
-
function signatureMatches(el) {
|
|
1258
|
-
const signature = `${el.getAttribute("class") ?? ""} ${el.getAttribute("id") ?? ""}`.toLowerCase();
|
|
1259
|
-
return BOILERPLATE_TOKENS.some((token) => signature.includes(token));
|
|
1072
|
+
//#endregion
|
|
1073
|
+
//#region src/pipeline/readability.ts
|
|
1074
|
+
function isReaderable(document) {
|
|
1075
|
+
return isProbablyReaderable(document);
|
|
1260
1076
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
"FORM",
|
|
1265
|
-
"NAV",
|
|
1266
|
-
"OL",
|
|
1267
|
-
"SECTION",
|
|
1268
|
-
"UL"
|
|
1269
|
-
]);
|
|
1270
|
-
function stripBoilerplate(document) {
|
|
1271
|
-
const limit = .25 * document.body.textContent.length;
|
|
1272
|
-
let removed = 0;
|
|
1273
|
-
for (const el of document.querySelectorAll("[class],[id]")) {
|
|
1274
|
-
if (!el.isConnected) continue;
|
|
1275
|
-
if (!BOILERPLATE_CONTAINER_TAGS.has(el.tagName)) continue;
|
|
1276
|
-
if (!signatureMatches(el)) continue;
|
|
1277
|
-
let ancestor = el.parentElement;
|
|
1278
|
-
while (ancestor && !signatureMatches(ancestor)) ancestor = ancestor.parentElement;
|
|
1279
|
-
if (ancestor) continue;
|
|
1280
|
-
if (el.textContent.length >= limit) continue;
|
|
1281
|
-
el.remove();
|
|
1282
|
-
removed++;
|
|
1283
|
-
}
|
|
1284
|
-
return removed;
|
|
1077
|
+
function parseArticle(document, options) {
|
|
1078
|
+
const clone = document.cloneNode(true);
|
|
1079
|
+
return new Readability(clone, options).parse();
|
|
1285
1080
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const body = document.body;
|
|
1293
|
-
const root = body.querySelector(selectors.include);
|
|
1294
|
-
if (root && root !== body) {
|
|
1295
|
-
body.innerHTML = "";
|
|
1296
|
-
body.appendChild(root);
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1081
|
+
//#endregion
|
|
1082
|
+
//#region src/pipeline/sanitize.ts
|
|
1083
|
+
function countRemoved(removed, tagName) {
|
|
1084
|
+
let count = 0;
|
|
1085
|
+
for (const entry of removed) if ("element" in entry && entry.element.nodeName.toUpperCase() === tagName) count += 1;
|
|
1086
|
+
return count;
|
|
1299
1087
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
"transparent",
|
|
1308
|
-
"pixel",
|
|
1309
|
-
"dummy"
|
|
1310
|
-
];
|
|
1311
|
-
function isPlaceholderSrc(src) {
|
|
1312
|
-
if (!src) return true;
|
|
1313
|
-
if (src.startsWith("data:")) return true;
|
|
1314
|
-
const lowered = src.toLowerCase();
|
|
1315
|
-
return PLACEHOLDER_TOKENS.some((token) => lowered.includes(token));
|
|
1088
|
+
function sanitizeHtml(dirty, window) {
|
|
1089
|
+
const purify = DOMPurify(window);
|
|
1090
|
+
return {
|
|
1091
|
+
html: purify.sanitize(dirty),
|
|
1092
|
+
iframesRemoved: countRemoved(purify.removed, "IFRAME"),
|
|
1093
|
+
scriptsRemoved: countRemoved(purify.removed, "SCRIPT")
|
|
1094
|
+
};
|
|
1316
1095
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1096
|
+
//#endregion
|
|
1097
|
+
//#region src/policy/footnotes.ts
|
|
1098
|
+
var FOOTNOTE_SIGNAL_RE = /cite_note|cite_ref|class="footnotes"|class="references"|data-footnote|role="doc-endnote|<sup\b[^>]*>\s*<a\s[^>]*href="#/i;
|
|
1099
|
+
var DEFINITION_CONTAINER_SELECTORS = [
|
|
1100
|
+
"ol.footnotes",
|
|
1101
|
+
"ol[class*=\"footnotes\"]",
|
|
1102
|
+
"ol.references",
|
|
1103
|
+
"ol[class*=\"references\"]",
|
|
1104
|
+
"section[class*=\"footnote\"]",
|
|
1105
|
+
"div[class*=\"footnote\"]",
|
|
1106
|
+
"[role=\"doc-endnotes\"]",
|
|
1107
|
+
"[role=\"doc-bibliography\"]"
|
|
1108
|
+
];
|
|
1109
|
+
var STANDALONE_DEF_ID_RE = /^(?:fn|cite_note|footnote|note)[:_-]/i;
|
|
1110
|
+
var BACKREF_LEADING_RE = /^(?:↑\s?|↩\s?|\^\s|Jump to\s*)/;
|
|
1111
|
+
function cleanDefText(text) {
|
|
1112
|
+
return text.replace(/\s+/g, " ").trim().replace(BACKREF_LEADING_RE, "");
|
|
1322
1113
|
}
|
|
1323
|
-
function
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const descriptor = splitAt === -1 ? "" : entry.slice(splitAt).trim();
|
|
1334
|
-
const url = rawUrl.length >= 2 && (rawUrl.startsWith("\"") && rawUrl.endsWith("\"") || rawUrl.startsWith("'") && rawUrl.endsWith("'")) ? rawUrl.slice(1, -1) : rawUrl;
|
|
1335
|
-
if (!url) continue;
|
|
1336
|
-
if (firstUrl === void 0) firstUrl = url;
|
|
1337
|
-
if (!descriptor) continue;
|
|
1338
|
-
const value = /^(\d+(?:\.\d+)?)[wx]$/.exec(descriptor);
|
|
1339
|
-
if (!value) continue;
|
|
1340
|
-
sawDescriptor = true;
|
|
1341
|
-
const numeric = Number.parseFloat(value[1]);
|
|
1342
|
-
if (numeric > bestValue) {
|
|
1343
|
-
bestValue = numeric;
|
|
1344
|
-
bestUrl = url;
|
|
1114
|
+
function collectDefinitions(document) {
|
|
1115
|
+
const containers = /* @__PURE__ */ new Set();
|
|
1116
|
+
const defs = /* @__PURE__ */ new Map();
|
|
1117
|
+
const standaloneIds = /* @__PURE__ */ new Set();
|
|
1118
|
+
for (const selector of DEFINITION_CONTAINER_SELECTORS) {
|
|
1119
|
+
let matched;
|
|
1120
|
+
try {
|
|
1121
|
+
matched = document.querySelectorAll(selector);
|
|
1122
|
+
} catch {
|
|
1123
|
+
continue;
|
|
1345
1124
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const srcset = source.getAttribute("srcset");
|
|
1355
|
-
if (srcset) {
|
|
1356
|
-
const url = pickLargestSrcset(srcset);
|
|
1357
|
-
if (url) return url;
|
|
1125
|
+
for (const container of Array.from(matched)) {
|
|
1126
|
+
if (!containers.has(container)) containers.add(container);
|
|
1127
|
+
for (const item of Array.from(container.querySelectorAll("li, [role=\"doc-endnote\"]"))) {
|
|
1128
|
+
const id = item.id;
|
|
1129
|
+
if (!id || defs.has(id)) continue;
|
|
1130
|
+
const text = cleanDefText(item.textContent);
|
|
1131
|
+
if (text) defs.set(id, text);
|
|
1132
|
+
}
|
|
1358
1133
|
}
|
|
1359
1134
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
if (ownSrcset) {
|
|
1368
|
-
const url = pickLargestSrcset(ownSrcset);
|
|
1369
|
-
if (url) return url;
|
|
1370
|
-
}
|
|
1371
|
-
return usableAttr(img, ["data-original", "data-lazy-src"]);
|
|
1372
|
-
}
|
|
1373
|
-
function resolveLazyImages(document) {
|
|
1374
|
-
let resolved = 0;
|
|
1375
|
-
for (const img of document.querySelectorAll("img")) {
|
|
1376
|
-
const currentSrc = img.getAttribute("src") ?? "";
|
|
1377
|
-
if (!isPlaceholderSrc(currentSrc)) continue;
|
|
1378
|
-
const real = resolveRealSource(img);
|
|
1379
|
-
if (real && real !== currentSrc) {
|
|
1380
|
-
img.setAttribute("src", real);
|
|
1381
|
-
resolved++;
|
|
1135
|
+
for (const li of Array.from(document.querySelectorAll("li[id]"))) {
|
|
1136
|
+
const id = li.id;
|
|
1137
|
+
if (!id || defs.has(id) || !STANDALONE_DEF_ID_RE.test(id)) continue;
|
|
1138
|
+
const text = cleanDefText(li.textContent);
|
|
1139
|
+
if (text) {
|
|
1140
|
+
defs.set(id, text);
|
|
1141
|
+
standaloneIds.add(id);
|
|
1382
1142
|
}
|
|
1383
1143
|
}
|
|
1384
|
-
return
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
var SP_PREFIX = "sp-";
|
|
1390
|
-
var BRUSH_RE = /brush:\s*([A-Za-z][\w-]*)/;
|
|
1391
|
-
var BOGUS_TOKENS = /* @__PURE__ */ new Set([
|
|
1392
|
-
"",
|
|
1393
|
-
"highlight",
|
|
1394
|
-
"source",
|
|
1395
|
-
"sp"
|
|
1396
|
-
]);
|
|
1397
|
-
function isValidToken(token) {
|
|
1398
|
-
return !BOGUS_TOKENS.has(token);
|
|
1399
|
-
}
|
|
1400
|
-
function hasLanguageClass(el) {
|
|
1401
|
-
for (const cls of el.classList) if (cls !== LANGUAGE_PREFIX && cls.startsWith(LANGUAGE_PREFIX)) return true;
|
|
1402
|
-
return false;
|
|
1403
|
-
}
|
|
1404
|
-
function collectClassSources(pre, code) {
|
|
1405
|
-
const sources = [{
|
|
1406
|
-
classes: [...pre.classList],
|
|
1407
|
-
raw: pre.getAttribute("class") ?? ""
|
|
1408
|
-
}];
|
|
1409
|
-
let depth = 0;
|
|
1410
|
-
let el = pre.parentElement;
|
|
1411
|
-
while (el && depth < 2) {
|
|
1412
|
-
sources.push({
|
|
1413
|
-
classes: [...el.classList],
|
|
1414
|
-
raw: el.getAttribute("class") ?? ""
|
|
1415
|
-
});
|
|
1416
|
-
depth++;
|
|
1417
|
-
if (el.tagName === "DIV" && [...el.classList].some((c) => c.startsWith("highlight"))) break;
|
|
1418
|
-
el = el.parentElement;
|
|
1419
|
-
}
|
|
1420
|
-
if (code) sources.push({
|
|
1421
|
-
classes: [...code.classList],
|
|
1422
|
-
raw: code.getAttribute("class") ?? ""
|
|
1423
|
-
});
|
|
1424
|
-
return sources;
|
|
1144
|
+
return {
|
|
1145
|
+
containers,
|
|
1146
|
+
defs,
|
|
1147
|
+
standaloneIds
|
|
1148
|
+
};
|
|
1425
1149
|
}
|
|
1426
|
-
function
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
}
|
|
1150
|
+
function processFootnotes(html) {
|
|
1151
|
+
if (!html || !FOOTNOTE_SIGNAL_RE.test(html)) return null;
|
|
1152
|
+
let document;
|
|
1153
|
+
try {
|
|
1154
|
+
document = buildDocument(html).document;
|
|
1155
|
+
} catch {
|
|
1433
1156
|
return null;
|
|
1434
1157
|
}
|
|
1435
|
-
const
|
|
1436
|
-
if (
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
}
|
|
1158
|
+
const { containers, defs, standaloneIds } = collectDefinitions(document);
|
|
1159
|
+
if (defs.size === 0) return null;
|
|
1160
|
+
const refHits = [];
|
|
1161
|
+
const defIdToNumber = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const sup of Array.from(document.querySelectorAll("sup"))) {
|
|
1163
|
+
if (!sup.isConnected) continue;
|
|
1164
|
+
try {
|
|
1165
|
+
const anchor = sup.querySelector("a[href^=\"#\"]");
|
|
1166
|
+
if (!anchor) continue;
|
|
1167
|
+
const frag = (anchor.getAttribute("href") ?? "").slice(1);
|
|
1168
|
+
if (!frag || !defs.has(frag)) continue;
|
|
1169
|
+
let n = defIdToNumber.get(frag);
|
|
1170
|
+
if (n === void 0) {
|
|
1171
|
+
n = defIdToNumber.size + 1;
|
|
1172
|
+
defIdToNumber.set(frag, n);
|
|
1173
|
+
}
|
|
1174
|
+
refHits.push({
|
|
1175
|
+
defId: frag,
|
|
1176
|
+
n,
|
|
1177
|
+
sup
|
|
1178
|
+
});
|
|
1179
|
+
} catch {}
|
|
1457
1180
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1181
|
+
if (refHits.length === 0) return null;
|
|
1182
|
+
for (const { n, sup } of refHits) try {
|
|
1183
|
+
sup.replaceWith(document.createTextNode(`[^${n}]`));
|
|
1184
|
+
} catch {}
|
|
1185
|
+
for (const container of containers) if (container.isConnected) container.remove();
|
|
1186
|
+
for (const id of standaloneIds) {
|
|
1187
|
+
const el = document.getElementById(id);
|
|
1188
|
+
if (el?.isConnected) el.remove();
|
|
1467
1189
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
if (!target) {
|
|
1477
|
-
target = pre.ownerDocument.createElement("code");
|
|
1478
|
-
while (pre.firstChild) target.appendChild(pre.firstChild);
|
|
1479
|
-
pre.appendChild(target);
|
|
1190
|
+
const numberToDefId = /* @__PURE__ */ new Map();
|
|
1191
|
+
for (const [id, n] of defIdToNumber) numberToDefId.set(n, id);
|
|
1192
|
+
const footnoteDefs = [];
|
|
1193
|
+
for (let n = 1; n <= numberToDefId.size; n++) {
|
|
1194
|
+
const id = numberToDefId.get(n);
|
|
1195
|
+
if (id === void 0) break;
|
|
1196
|
+
const text = defs.get(id);
|
|
1197
|
+
if (text !== void 0) footnoteDefs.push(text);
|
|
1480
1198
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1199
|
+
return {
|
|
1200
|
+
footnoteDefs,
|
|
1201
|
+
html: document.body.innerHTML
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
//#endregion
|
|
1205
|
+
//#region src/policy/cell-text.ts
|
|
1206
|
+
var CELL_CHROME_SELECTOR = "[aria-label], [data-tooltip], [data-toggle=\"tooltip\"], .tooltip, .badge";
|
|
1207
|
+
function normalize(text) {
|
|
1208
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1209
|
+
}
|
|
1210
|
+
function nonEmptyHrefs(cell) {
|
|
1211
|
+
const anchors = cell.tagName === "A" ? [cell, ...Array.from(cell.querySelectorAll("a[href]"))] : Array.from(cell.querySelectorAll("a[href]"));
|
|
1212
|
+
const hrefs = [];
|
|
1213
|
+
for (const a of anchors) {
|
|
1214
|
+
const href = (a.getAttribute("href") ?? "").trim();
|
|
1215
|
+
if (href !== "") hrefs.push(href);
|
|
1487
1216
|
}
|
|
1488
|
-
return
|
|
1217
|
+
return hrefs;
|
|
1489
1218
|
}
|
|
1490
|
-
function
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1219
|
+
function resolveCellText(cell) {
|
|
1220
|
+
const full = normalize(cell.textContent);
|
|
1221
|
+
let resolved = full;
|
|
1222
|
+
if (full !== "") {
|
|
1223
|
+
const clone = cell.cloneNode(true);
|
|
1224
|
+
clone.querySelectorAll(CELL_CHROME_SELECTOR).forEach((el) => {
|
|
1225
|
+
el.remove();
|
|
1226
|
+
});
|
|
1227
|
+
const stripped = normalize(clone.textContent);
|
|
1228
|
+
resolved = stripped !== "" ? stripped : full;
|
|
1497
1229
|
}
|
|
1498
|
-
|
|
1230
|
+
if (resolved === "") {
|
|
1231
|
+
const hrefs = nonEmptyHrefs(cell);
|
|
1232
|
+
if (hrefs.length > 0) return hrefs.join(" ");
|
|
1233
|
+
}
|
|
1234
|
+
return resolved;
|
|
1499
1235
|
}
|
|
1500
1236
|
//#endregion
|
|
1501
|
-
//#region src/
|
|
1502
|
-
|
|
1503
|
-
|
|
1237
|
+
//#region src/policy/tables.ts
|
|
1238
|
+
var SECTION_TAGS = /* @__PURE__ */ new Set([
|
|
1239
|
+
"TBODY",
|
|
1240
|
+
"TFOOT",
|
|
1241
|
+
"THEAD"
|
|
1242
|
+
]);
|
|
1243
|
+
var CELL_TAGS = /* @__PURE__ */ new Set(["TD", "TH"]);
|
|
1244
|
+
function spanOf(cell, attr) {
|
|
1245
|
+
const raw = cell.getAttribute(attr);
|
|
1246
|
+
if (raw === null) return 1;
|
|
1247
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1248
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 1;
|
|
1249
|
+
return parsed;
|
|
1504
1250
|
}
|
|
1505
|
-
function
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1251
|
+
function collectRows(table) {
|
|
1252
|
+
const rows = [];
|
|
1253
|
+
for (const child of Array.from(table.children)) if (child.tagName === "TR") rows.push(child);
|
|
1254
|
+
else if (SECTION_TAGS.has(child.tagName)) {
|
|
1255
|
+
for (const tr of Array.from(child.children)) if (tr.tagName === "TR") rows.push(tr);
|
|
1256
|
+
}
|
|
1257
|
+
return rows;
|
|
1508
1258
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
function countElements(html, window) {
|
|
1512
|
-
if (!window || !html) return 0;
|
|
1513
|
-
const template = window.document.createElement("div");
|
|
1514
|
-
template.innerHTML = html;
|
|
1515
|
-
return template.querySelectorAll("*").length;
|
|
1259
|
+
function cellsOf(tr) {
|
|
1260
|
+
return Array.from(tr.children).filter((child) => CELL_TAGS.has(child.tagName));
|
|
1516
1261
|
}
|
|
1517
|
-
function
|
|
1518
|
-
|
|
1519
|
-
const removedNodes = Math.max(0, (input.documentElementCount ?? 0) - articleElementCount);
|
|
1520
|
-
return {
|
|
1521
|
-
readerable: input.readerable,
|
|
1522
|
-
extractedNode: input.extractedNode,
|
|
1523
|
-
fallbackUsed: input.fallbackUsed ?? false,
|
|
1524
|
-
gated: input.gated,
|
|
1525
|
-
imagesResolved: input.imagesResolved,
|
|
1526
|
-
pagination: input.pagination,
|
|
1527
|
-
removedNodes,
|
|
1528
|
-
boilerplateRemoved: input.boilerplateRemoved,
|
|
1529
|
-
chromeRemoved: input.chromeRemoved,
|
|
1530
|
-
sanitization: input.sanitization,
|
|
1531
|
-
trace: input.trace,
|
|
1532
|
-
truncated: input.truncated ?? false,
|
|
1533
|
-
...input.cache ? { cache: input.cache } : {}
|
|
1534
|
-
};
|
|
1262
|
+
function parseTableMatrix(table) {
|
|
1263
|
+
return buildCellGrid(table).map((row) => row.map((cell) => cell === null ? "" : resolveCellText(cell)));
|
|
1535
1264
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
entries = [];
|
|
1539
|
-
constructor(enabled) {
|
|
1540
|
-
this.enabled = enabled;
|
|
1541
|
-
}
|
|
1542
|
-
collect() {
|
|
1543
|
-
return this.enabled ? this.entries : void 0;
|
|
1544
|
-
}
|
|
1545
|
-
run(stage, fn) {
|
|
1546
|
-
if (!this.enabled) return fn();
|
|
1547
|
-
const start = performance.now();
|
|
1548
|
-
try {
|
|
1549
|
-
return fn();
|
|
1550
|
-
} finally {
|
|
1551
|
-
this.entries.push({
|
|
1552
|
-
ms: performance.now() - start,
|
|
1553
|
-
stage
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
};
|
|
1558
|
-
//#endregion
|
|
1559
|
-
//#region src/policy/gating.ts
|
|
1560
|
-
var PAYWALL_SELECTORS = [
|
|
1561
|
-
"[class*=\"paywall\"]",
|
|
1562
|
-
"[id*=\"paywall\"]",
|
|
1563
|
-
".piano",
|
|
1564
|
-
"#piano",
|
|
1565
|
-
".tp-modal",
|
|
1566
|
-
".tp-active",
|
|
1567
|
-
"[id*=\"piano\"]",
|
|
1568
|
-
"[class*=\"piano\"]",
|
|
1569
|
-
"[class*=\"subscribe-wall\"]",
|
|
1570
|
-
"[id*=\"subscribe-wall\"]",
|
|
1571
|
-
"[class*=\"metered-wall\"]",
|
|
1572
|
-
"[id*=\"metered-wall\"]",
|
|
1573
|
-
".leaky-paywall"
|
|
1574
|
-
];
|
|
1575
|
-
var METERED_TEXT_RE = /(\d+)\s*(?:free\s*)?(?:articles?|stories?)\s*(?:left|remaining)|you\s+have\s+reached\s+(?:your\s+)?(?:free\s+)?(?:article\s+|story\s+)?limit|subscribe\s+to\s+(?:continue\s+)?reading|read\s+the\s+full\s+(?:article|story)|unlock\s+(?:this|full|all)\s+(?:article|story|content)|keep\s+reading\s+with/i;
|
|
1576
|
-
function findPaywallOverlay(document) {
|
|
1577
|
-
for (const selector of PAYWALL_SELECTORS) if (document.querySelector(selector)?.isConnected) return {
|
|
1578
|
-
likely: true,
|
|
1579
|
-
reason: "paywall overlay"
|
|
1580
|
-
};
|
|
1265
|
+
function escapeGfmCell(text) {
|
|
1266
|
+
return text.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
1581
1267
|
}
|
|
1582
|
-
function
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
};
|
|
1268
|
+
function renderTableGfm(matrix) {
|
|
1269
|
+
if (matrix.length === 0) return "";
|
|
1270
|
+
const cols = matrix[0].length;
|
|
1271
|
+
const lines = [];
|
|
1272
|
+
lines.push(`| ${matrix[0].map(escapeGfmCell).join(" | ")} |`);
|
|
1273
|
+
lines.push(`| ${Array.from({ length: cols }, () => "---").join(" | ")} |`);
|
|
1274
|
+
for (let r = 1; r < matrix.length; r++) lines.push(`| ${matrix[r].map(escapeGfmCell).join(" | ")} |`);
|
|
1275
|
+
return lines.join("\n");
|
|
1588
1276
|
}
|
|
1589
|
-
function
|
|
1590
|
-
|
|
1277
|
+
function escapeCsvField(text) {
|
|
1278
|
+
if (/[",\r\n]/.test(text)) return `"${text.replace(/"/g, "\"\"")}"`;
|
|
1279
|
+
return text;
|
|
1591
1280
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
if (!src || !baseUrl) return src;
|
|
1596
|
-
try {
|
|
1597
|
-
return new URL(src, baseUrl).href;
|
|
1598
|
-
} catch {
|
|
1599
|
-
return src;
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
//#endregion
|
|
1603
|
-
//#region src/policy/pagination.ts
|
|
1604
|
-
var NEXT_LINK_TEXT_RE = /^(next(\s+page)?|older(\s+posts?)?|[›»→]|next\s*[›»→]|older\s*[›»→])$/i;
|
|
1605
|
-
function usableHref(href) {
|
|
1606
|
-
if (!href || href === "#") return;
|
|
1607
|
-
return href;
|
|
1608
|
-
}
|
|
1609
|
-
function findPaginated(document, baseUrl) {
|
|
1610
|
-
const linkNext = document.querySelector("link[rel=\"next\"][href]");
|
|
1611
|
-
if (linkNext) {
|
|
1612
|
-
const href = usableHref(linkNext.getAttribute("href"));
|
|
1613
|
-
if (href) return {
|
|
1614
|
-
type: "paginated",
|
|
1615
|
-
nextUrl: absolutize(href, baseUrl)
|
|
1616
|
-
};
|
|
1617
|
-
}
|
|
1618
|
-
const aRelNext = document.querySelector("a[rel=\"next\"][href]");
|
|
1619
|
-
if (aRelNext) {
|
|
1620
|
-
const href = usableHref(aRelNext.getAttribute("href"));
|
|
1621
|
-
if (href) return {
|
|
1622
|
-
type: "paginated",
|
|
1623
|
-
nextUrl: absolutize(href, baseUrl)
|
|
1624
|
-
};
|
|
1625
|
-
}
|
|
1626
|
-
for (const anchor of document.querySelectorAll("a[href]")) {
|
|
1627
|
-
const text = anchor.textContent.trim();
|
|
1628
|
-
if (!text || !NEXT_LINK_TEXT_RE.test(text)) continue;
|
|
1629
|
-
const href = usableHref(anchor.getAttribute("href"));
|
|
1630
|
-
if (!href) continue;
|
|
1631
|
-
return {
|
|
1632
|
-
type: "paginated",
|
|
1633
|
-
nextUrl: absolutize(href, baseUrl)
|
|
1634
|
-
};
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
var INFINITE_ATTR_SELECTORS = [
|
|
1638
|
-
"[data-load-more]",
|
|
1639
|
-
"[data-infinite-scroll]",
|
|
1640
|
-
"[data-pagination]",
|
|
1641
|
-
"[infinite-scroll]"
|
|
1642
|
-
];
|
|
1643
|
-
var INFINITE_SUBSTRING_SELECTORS = [
|
|
1644
|
-
"[class*=\"load-more\"]",
|
|
1645
|
-
"[class*=\"loadmore\"]",
|
|
1646
|
-
"[class*=\"infinite\"]",
|
|
1647
|
-
"[id*=\"load-more\"]",
|
|
1648
|
-
"[class*=\"sentinel\"]"
|
|
1649
|
-
];
|
|
1650
|
-
var LOAD_MORE_BUTTON_RE = /^(load more|show more|view more|more results|load more comments)$/i;
|
|
1651
|
-
function findInfinite(document) {
|
|
1652
|
-
for (const selector of INFINITE_ATTR_SELECTORS) if (document.querySelector(selector)?.isConnected) return {
|
|
1653
|
-
selector,
|
|
1654
|
-
type: "infinite"
|
|
1655
|
-
};
|
|
1656
|
-
for (const selector of INFINITE_SUBSTRING_SELECTORS) if (document.querySelector(selector)?.isConnected) return {
|
|
1657
|
-
selector,
|
|
1658
|
-
type: "infinite"
|
|
1659
|
-
};
|
|
1660
|
-
for (const button of document.querySelectorAll("button")) {
|
|
1661
|
-
const text = button.textContent.trim();
|
|
1662
|
-
if (text && LOAD_MORE_BUTTON_RE.test(text)) return {
|
|
1663
|
-
selector: "button",
|
|
1664
|
-
type: "infinite"
|
|
1665
|
-
};
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
function detectPagination(document, baseUrl) {
|
|
1669
|
-
return findPaginated(document, baseUrl) ?? findInfinite(document);
|
|
1670
|
-
}
|
|
1671
|
-
//#endregion
|
|
1672
|
-
//#region src/tools/html-source.ts
|
|
1673
|
-
function readHtmlFile(localPath) {
|
|
1674
|
-
const raw = readFileSync(localPath, "utf8");
|
|
1675
|
-
const trimmed = raw.trim();
|
|
1676
|
-
if (trimmed.length < 2 || trimmed.at(0) !== "\"" || trimmed.at(-1) !== "\"") return raw;
|
|
1677
|
-
try {
|
|
1678
|
-
const parsed = JSON.parse(trimmed);
|
|
1679
|
-
return typeof parsed === "string" ? parsed : raw;
|
|
1680
|
-
} catch {
|
|
1681
|
-
return raw;
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
//#endregion
|
|
1685
|
-
//#region src/policy/cell-text.ts
|
|
1686
|
-
var CELL_CHROME_SELECTOR = "[aria-label], [data-tooltip], [data-toggle=\"tooltip\"], .tooltip, .badge";
|
|
1687
|
-
function normalize(text) {
|
|
1688
|
-
return text.replace(/\s+/g, " ").trim();
|
|
1689
|
-
}
|
|
1690
|
-
function nonEmptyHrefs(cell) {
|
|
1691
|
-
const anchors = cell.tagName === "A" ? [cell, ...Array.from(cell.querySelectorAll("a[href]"))] : Array.from(cell.querySelectorAll("a[href]"));
|
|
1692
|
-
const hrefs = [];
|
|
1693
|
-
for (const a of anchors) {
|
|
1694
|
-
const href = (a.getAttribute("href") ?? "").trim();
|
|
1695
|
-
if (href !== "") hrefs.push(href);
|
|
1696
|
-
}
|
|
1697
|
-
return hrefs;
|
|
1698
|
-
}
|
|
1699
|
-
function resolveCellText(cell) {
|
|
1700
|
-
const full = normalize(cell.textContent);
|
|
1701
|
-
let resolved = full;
|
|
1702
|
-
if (full !== "") {
|
|
1703
|
-
const clone = cell.cloneNode(true);
|
|
1704
|
-
clone.querySelectorAll(CELL_CHROME_SELECTOR).forEach((el) => {
|
|
1705
|
-
el.remove();
|
|
1706
|
-
});
|
|
1707
|
-
const stripped = normalize(clone.textContent);
|
|
1708
|
-
resolved = stripped !== "" ? stripped : full;
|
|
1709
|
-
}
|
|
1710
|
-
if (resolved === "") {
|
|
1711
|
-
const hrefs = nonEmptyHrefs(cell);
|
|
1712
|
-
if (hrefs.length > 0) return hrefs.join(" ");
|
|
1713
|
-
}
|
|
1714
|
-
return resolved;
|
|
1715
|
-
}
|
|
1716
|
-
//#endregion
|
|
1717
|
-
//#region src/policy/tables.ts
|
|
1718
|
-
var SECTION_TAGS = /* @__PURE__ */ new Set([
|
|
1719
|
-
"TBODY",
|
|
1720
|
-
"TFOOT",
|
|
1721
|
-
"THEAD"
|
|
1722
|
-
]);
|
|
1723
|
-
var CELL_TAGS = /* @__PURE__ */ new Set(["TD", "TH"]);
|
|
1724
|
-
function spanOf(cell, attr) {
|
|
1725
|
-
const raw = cell.getAttribute(attr);
|
|
1726
|
-
if (raw === null) return 1;
|
|
1727
|
-
const parsed = Number.parseInt(raw, 10);
|
|
1728
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return 1;
|
|
1729
|
-
return parsed;
|
|
1730
|
-
}
|
|
1731
|
-
function collectRows(table) {
|
|
1732
|
-
const rows = [];
|
|
1733
|
-
for (const child of Array.from(table.children)) if (child.tagName === "TR") rows.push(child);
|
|
1734
|
-
else if (SECTION_TAGS.has(child.tagName)) {
|
|
1735
|
-
for (const tr of Array.from(child.children)) if (tr.tagName === "TR") rows.push(tr);
|
|
1736
|
-
}
|
|
1737
|
-
return rows;
|
|
1738
|
-
}
|
|
1739
|
-
function cellsOf(tr) {
|
|
1740
|
-
return Array.from(tr.children).filter((child) => CELL_TAGS.has(child.tagName));
|
|
1741
|
-
}
|
|
1742
|
-
function parseTableMatrix(table) {
|
|
1743
|
-
return buildCellGrid(table).map((row) => row.map((cell) => cell === null ? "" : resolveCellText(cell)));
|
|
1744
|
-
}
|
|
1745
|
-
function escapeGfmCell(text) {
|
|
1746
|
-
return text.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
1747
|
-
}
|
|
1748
|
-
function renderTableGfm(matrix) {
|
|
1749
|
-
if (matrix.length === 0) return "";
|
|
1750
|
-
const cols = matrix[0].length;
|
|
1751
|
-
const lines = [];
|
|
1752
|
-
lines.push(`| ${matrix[0].map(escapeGfmCell).join(" | ")} |`);
|
|
1753
|
-
lines.push(`| ${Array.from({ length: cols }, () => "---").join(" | ")} |`);
|
|
1754
|
-
for (let r = 1; r < matrix.length; r++) lines.push(`| ${matrix[r].map(escapeGfmCell).join(" | ")} |`);
|
|
1755
|
-
return lines.join("\n");
|
|
1756
|
-
}
|
|
1757
|
-
function escapeCsvField(text) {
|
|
1758
|
-
if (/[",\r\n]/.test(text)) return `"${text.replace(/"/g, "\"\"")}"`;
|
|
1759
|
-
return text;
|
|
1760
|
-
}
|
|
1761
|
-
function renderTableCsv(matrix) {
|
|
1762
|
-
if (matrix.length === 0) return "";
|
|
1763
|
-
return matrix.map((row) => row.map(escapeCsvField).join(",")).join("\n");
|
|
1281
|
+
function renderTableCsv(matrix) {
|
|
1282
|
+
if (matrix.length === 0) return "";
|
|
1283
|
+
return matrix.map((row) => row.map(escapeCsvField).join(",")).join("\n");
|
|
1764
1284
|
}
|
|
1765
1285
|
function headerKeys(header) {
|
|
1766
1286
|
return header.map((cell, i) => cell === "" ? `column_${i}` : cell);
|
|
@@ -1895,464 +1415,434 @@ function renderTable(matrix, format, keys) {
|
|
|
1895
1415
|
}
|
|
1896
1416
|
}
|
|
1897
1417
|
//#endregion
|
|
1898
|
-
//#region src/
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
}
|
|
1906
|
-
function countWords(text) {
|
|
1907
|
-
return (text.match(/\S+/g) ?? []).length;
|
|
1908
|
-
}
|
|
1909
|
-
function nonEmpty(value) {
|
|
1910
|
-
return value?.trim() ? value : void 0;
|
|
1911
|
-
}
|
|
1912
|
-
function computeTextMetrics(text, wordsPerMinute) {
|
|
1913
|
-
const wordCount = countWords(text);
|
|
1914
|
-
return {
|
|
1915
|
-
wordCount,
|
|
1916
|
-
readingTimeMin: wordCount === 0 ? 0 : Math.max(1, Math.round(wordCount / wordsPerMinute)),
|
|
1917
|
-
...estimateTokens(text)
|
|
1918
|
-
};
|
|
1919
|
-
}
|
|
1920
|
-
//#endregion
|
|
1921
|
-
//#region src/policy/metadata.ts
|
|
1922
|
-
var ARTICLE_TYPES = /* @__PURE__ */ new Set([
|
|
1923
|
-
"Article",
|
|
1924
|
-
"BlogPosting",
|
|
1925
|
-
"NewsArticle",
|
|
1926
|
-
"Report",
|
|
1927
|
-
"ScholarlyArticle",
|
|
1928
|
-
"SocialMediaPosting",
|
|
1929
|
-
"TechArticle",
|
|
1930
|
-
"WebPage"
|
|
1931
|
-
]);
|
|
1932
|
-
function first(...values) {
|
|
1933
|
-
for (const value of values) {
|
|
1934
|
-
const picked = nonEmpty(value);
|
|
1935
|
-
if (picked) return picked;
|
|
1418
|
+
//#region src/pipeline/urls.ts
|
|
1419
|
+
function absolutize(src, baseUrl) {
|
|
1420
|
+
if (!src || !baseUrl) return src;
|
|
1421
|
+
try {
|
|
1422
|
+
return new URL(src, baseUrl).href;
|
|
1423
|
+
} catch {
|
|
1424
|
+
return src;
|
|
1936
1425
|
}
|
|
1937
1426
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1427
|
+
//#endregion
|
|
1428
|
+
//#region src/pipeline/turndown.ts
|
|
1429
|
+
function toMarkdown(html, options) {
|
|
1430
|
+
const service = new TurndownService({
|
|
1431
|
+
bulletListMarker: "-",
|
|
1432
|
+
codeBlockStyle: options?.codeBlockStyle ?? "fenced",
|
|
1433
|
+
emDelimiter: "_",
|
|
1434
|
+
fence: "```",
|
|
1435
|
+
headingStyle: options?.headingStyle ?? "atx",
|
|
1436
|
+
strongDelimiter: "**"
|
|
1437
|
+
});
|
|
1438
|
+
if (options?.gfm !== false) service.use(gfm);
|
|
1439
|
+
const tableFormat = options?.tables;
|
|
1440
|
+
if (tableFormat !== void 0) service.addRule("tableMatrix", {
|
|
1441
|
+
filter: (node) => node.nodeName === "TABLE",
|
|
1442
|
+
replacement: (_content, node) => {
|
|
1443
|
+
const matrix = parseTableMatrix(node);
|
|
1444
|
+
if (matrix.length === 0) return "";
|
|
1445
|
+
const body = renderTable(matrix, tableFormat);
|
|
1446
|
+
if (tableFormat === "gfm") return `\n\n${body}\n\n`;
|
|
1447
|
+
return `\n\n\`\`\`${tableFormat}\n${body}\n\`\`\`\n\n`;
|
|
1955
1448
|
}
|
|
1956
|
-
collectObjects(parsed, out);
|
|
1957
1449
|
});
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
"
|
|
1986
|
-
"
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
function nodeTypeList(node) {
|
|
1990
|
-
const type = node["@type"];
|
|
1991
|
-
if (Array.isArray(type)) return type.filter((t) => typeof t === "string");
|
|
1992
|
-
return typeof type === "string" ? [type] : [];
|
|
1450
|
+
const imageMode = options?.images;
|
|
1451
|
+
const baseUrl = options?.baseUrl;
|
|
1452
|
+
const references = [];
|
|
1453
|
+
applyImagePolicy(service, imageMode, baseUrl, references);
|
|
1454
|
+
service.addRule("anchorAbsolutize", {
|
|
1455
|
+
filter: "a",
|
|
1456
|
+
replacement: (content, node) => {
|
|
1457
|
+
const rawHref = node.getAttribute("href");
|
|
1458
|
+
if (!rawHref) return content;
|
|
1459
|
+
const href = absolutize(rawHref, baseUrl);
|
|
1460
|
+
const title = node.getAttribute("title");
|
|
1461
|
+
return `[${content}](${href}${title ? ` "${title.replace(/"/g, "\\\"")}"` : ""})`;
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
service.addRule("mathMarker", {
|
|
1465
|
+
filter: (node) => node.nodeName === "SPAN" && node.classList.contains("rdrm-math"),
|
|
1466
|
+
replacement: (_content, node) => {
|
|
1467
|
+
const tex = node.textContent.trim();
|
|
1468
|
+
if (!tex) return "";
|
|
1469
|
+
return node.getAttribute("data-display") === "true" ? `$$${tex}$$` : `$${tex}$`;
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
const fnResult = processFootnotes(html);
|
|
1473
|
+
const sourceHtml = fnResult?.html ?? html;
|
|
1474
|
+
let body = service.turndown(sourceHtml);
|
|
1475
|
+
if (fnResult) for (let n = 1; n <= fnResult.footnoteDefs.length; n++) body = body.replaceAll(`\\[^${n}\\]`, `[^${n}]`);
|
|
1476
|
+
const trailingBlocks = [];
|
|
1477
|
+
if (imageMode === "reference" && references.length > 0) trailingBlocks.push(references.map((ref, i) => `[img-${i + 1}]: ${ref}`).join("\n"));
|
|
1478
|
+
if (fnResult && fnResult.footnoteDefs.length > 0) trailingBlocks.push(fnResult.footnoteDefs.map((def, i) => `[^${i + 1}]: ${def}`).join("\n"));
|
|
1479
|
+
if (trailingBlocks.length === 0) return body;
|
|
1480
|
+
return `${body.replace(/\n+$/, "")}\n\n${trailingBlocks.join("\n")}`;
|
|
1993
1481
|
}
|
|
1994
|
-
function
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1482
|
+
function applyImagePolicy(service, mode, baseUrl, references) {
|
|
1483
|
+
switch (mode) {
|
|
1484
|
+
case "drop":
|
|
1485
|
+
service.addRule("dropImage", {
|
|
1486
|
+
filter: "img",
|
|
1487
|
+
replacement: () => ""
|
|
1488
|
+
});
|
|
1489
|
+
break;
|
|
1490
|
+
case "keep":
|
|
1491
|
+
case void 0:
|
|
1492
|
+
service.addRule("imageKeep", {
|
|
1493
|
+
filter: "img",
|
|
1494
|
+
replacement: (_content, node) => {
|
|
1495
|
+
const src = absolutize(node.getAttribute("src") ?? "", baseUrl);
|
|
1496
|
+
const alt = node.getAttribute("alt") ?? "";
|
|
1497
|
+
return src ? `` : "";
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
break;
|
|
1501
|
+
case "reference":
|
|
1502
|
+
service.addRule("imageReference", {
|
|
1503
|
+
filter: "img",
|
|
1504
|
+
replacement: (_content, node) => {
|
|
1505
|
+
const src = absolutize(node.getAttribute("src") ?? "", baseUrl);
|
|
1506
|
+
if (!src) return "";
|
|
1507
|
+
references.push(src);
|
|
1508
|
+
const id = references.length;
|
|
1509
|
+
return `![${node.getAttribute("alt") ?? ""}][img-${id}]`;
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
break;
|
|
1513
|
+
case "src-only": service.addRule("imageSrcOnly", {
|
|
1514
|
+
filter: "img",
|
|
1515
|
+
replacement: (_content, node) => {
|
|
1516
|
+
const src = absolutize(node.getAttribute("src") ?? "", baseUrl);
|
|
1517
|
+
return src ? `\n\n${src}\n\n` : "";
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1998
1520
|
}
|
|
1999
1521
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
1522
|
+
//#endregion
|
|
1523
|
+
//#region src/policy/chunk.ts
|
|
1524
|
+
var HEADING_FIRST_LINE = /^#{1,6}\s/;
|
|
1525
|
+
function splitBlocks(markdown) {
|
|
1526
|
+
const parts = markdown.split(/\n{2,}/);
|
|
1527
|
+
const blocks = [];
|
|
1528
|
+
let heading = "";
|
|
1529
|
+
for (const part of parts) {
|
|
1530
|
+
const text = part.trim();
|
|
1531
|
+
if (!text) continue;
|
|
1532
|
+
if (HEADING_FIRST_LINE.test(text)) heading = headingText(text);
|
|
1533
|
+
blocks.push({
|
|
1534
|
+
headingContext: heading,
|
|
1535
|
+
text
|
|
1536
|
+
});
|
|
2009
1537
|
}
|
|
2010
|
-
return
|
|
1538
|
+
return blocks;
|
|
2011
1539
|
}
|
|
2012
|
-
function
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1540
|
+
function splitOversizedBlock(block, maxChars) {
|
|
1541
|
+
return hardSplitLines(block.text, maxChars).map((text) => ({
|
|
1542
|
+
headingContext: block.headingContext,
|
|
1543
|
+
text
|
|
1544
|
+
}));
|
|
1545
|
+
}
|
|
1546
|
+
function toUnits(blocks, maxChars) {
|
|
1547
|
+
const units = [];
|
|
1548
|
+
for (const block of blocks) if (block.text.length <= maxChars) units.push(block);
|
|
1549
|
+
else units.push(...splitOversizedBlock(block, maxChars));
|
|
1550
|
+
return units;
|
|
1551
|
+
}
|
|
1552
|
+
function chunkMarkdownChar(markdown, options) {
|
|
1553
|
+
if (!markdown) return [];
|
|
1554
|
+
const maxChars = Math.max(1, Math.floor(options.maxTokens)) * 4;
|
|
1555
|
+
const overlapTokens = Math.max(0, Math.floor(options.overlap));
|
|
1556
|
+
const overlapChars = Math.min(overlapTokens * 4, maxChars - 1);
|
|
1557
|
+
const blocks = splitBlocks(markdown);
|
|
1558
|
+
if (blocks.length === 0) return [];
|
|
1559
|
+
const units = toUnits(blocks, maxChars);
|
|
1560
|
+
if (units.length === 0) return [];
|
|
1561
|
+
const chunks = [];
|
|
1562
|
+
let i = 0;
|
|
1563
|
+
let overlapText = "";
|
|
1564
|
+
let overlapHeading = "";
|
|
1565
|
+
while (i < units.length) {
|
|
1566
|
+
const spans = [];
|
|
1567
|
+
let chunkText = "";
|
|
1568
|
+
if (overlapText) {
|
|
1569
|
+
chunkText = overlapText;
|
|
1570
|
+
spans.push({
|
|
1571
|
+
end: overlapText.length,
|
|
1572
|
+
headingContext: overlapHeading,
|
|
1573
|
+
start: 0
|
|
1574
|
+
});
|
|
2019
1575
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
1576
|
+
const firstUnit = units[i];
|
|
1577
|
+
const sepLen = chunkText ? 2 : 0;
|
|
1578
|
+
if (chunkText && chunkText.length + sepLen + firstUnit.text.length > maxChars) {
|
|
1579
|
+
chunkText = "";
|
|
1580
|
+
spans.length = 0;
|
|
2024
1581
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
1582
|
+
{
|
|
1583
|
+
const sep = chunkText ? "\n\n" : "";
|
|
1584
|
+
const start = chunkText.length + sep.length;
|
|
1585
|
+
chunkText = chunkText + sep + firstUnit.text;
|
|
1586
|
+
spans.push({
|
|
1587
|
+
end: chunkText.length,
|
|
1588
|
+
headingContext: firstUnit.headingContext,
|
|
1589
|
+
start
|
|
1590
|
+
});
|
|
1591
|
+
i += 1;
|
|
2029
1592
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
const
|
|
2033
|
-
if (
|
|
1593
|
+
while (i < units.length) {
|
|
1594
|
+
const unit = units[i];
|
|
1595
|
+
const candidate = `${chunkText}\n\n${unit.text}`;
|
|
1596
|
+
if (candidate.length > maxChars) break;
|
|
1597
|
+
const start = chunkText.length + 2;
|
|
1598
|
+
chunkText = candidate;
|
|
1599
|
+
spans.push({
|
|
1600
|
+
end: chunkText.length,
|
|
1601
|
+
headingContext: unit.headingContext,
|
|
1602
|
+
start
|
|
1603
|
+
});
|
|
1604
|
+
i += 1;
|
|
1605
|
+
}
|
|
1606
|
+
const text = chunkText.trim();
|
|
1607
|
+
if (text) chunks.push({
|
|
1608
|
+
headingContext: spans[0]?.headingContext ?? "",
|
|
1609
|
+
index: chunks.length,
|
|
1610
|
+
text,
|
|
1611
|
+
tokenCount: Math.round(text.length / 4)
|
|
1612
|
+
});
|
|
1613
|
+
if (overlapChars > 0 && i < units.length) {
|
|
1614
|
+
const overlapStart = Math.max(0, chunkText.length - overlapChars);
|
|
1615
|
+
overlapHeading = (spans.find((span) => overlapStart >= span.start && overlapStart < span.end) ?? spans.find((span) => span.start >= overlapStart))?.headingContext ?? "";
|
|
1616
|
+
overlapText = chunkText.slice(overlapStart).replace(/^\n+/, "");
|
|
1617
|
+
} else {
|
|
1618
|
+
overlapText = "";
|
|
1619
|
+
overlapHeading = "";
|
|
2034
1620
|
}
|
|
2035
1621
|
}
|
|
2036
|
-
|
|
2037
|
-
return names.length > 0 ? names.join(", ") : void 0;
|
|
2038
|
-
}
|
|
2039
|
-
function asString(value) {
|
|
2040
|
-
return typeof value === "string" ? nonEmpty(value) : void 0;
|
|
1622
|
+
return chunks;
|
|
2041
1623
|
}
|
|
2042
|
-
function
|
|
2043
|
-
|
|
2044
|
-
|
|
1624
|
+
function parseSemanticUnits(markdown) {
|
|
1625
|
+
const units = [];
|
|
1626
|
+
const stack = [];
|
|
1627
|
+
function context() {
|
|
1628
|
+
return stack.map((h) => h.text).join(" > ");
|
|
1629
|
+
}
|
|
1630
|
+
for (const block of parseBlocks(markdown)) {
|
|
1631
|
+
const text = markdown.slice(block.start, block.end);
|
|
1632
|
+
if (block.kind === "heading") {
|
|
1633
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= block.depth) stack.pop();
|
|
1634
|
+
stack.push({
|
|
1635
|
+
level: block.depth,
|
|
1636
|
+
text: headingText(text)
|
|
1637
|
+
});
|
|
1638
|
+
units.push({
|
|
1639
|
+
headingContext: context(),
|
|
1640
|
+
kind: "heading",
|
|
1641
|
+
text
|
|
1642
|
+
});
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
units.push({
|
|
1646
|
+
headingContext: context(),
|
|
1647
|
+
kind: block.kind === "code" ? "code" : "text",
|
|
1648
|
+
text
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
return units;
|
|
2045
1652
|
}
|
|
2046
|
-
function
|
|
2047
|
-
const
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
publishedTime: first(asString(jsonLd?.datePublished), metaProperty(document, "article:published_time"), nonEmpty(document.querySelector("time[datetime]")?.getAttribute("datetime") ?? void 0), readability?.publishedTime ?? void 0),
|
|
2059
|
-
excerpt: first(asString(jsonLd?.description), metaProperty(document, "og:description"), metaName(document, "twitter:description"), metaName(document, "description"), readability?.excerpt ?? void 0),
|
|
2060
|
-
canonical: first(nonEmpty(document.querySelector("link[rel=\"canonical\"]")?.getAttribute("href") ?? void 0), metaProperty(document, "og:url")),
|
|
2061
|
-
baseUrl: input.baseUrl,
|
|
2062
|
-
wordCount: input.wordCount,
|
|
2063
|
-
readingTimeMin: input.readingTimeMin,
|
|
2064
|
-
...estimateTokens(input.textContent),
|
|
2065
|
-
...structuredRaw ? { structured: cleanStructured(structuredRaw) } : {}
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
//#endregion
|
|
2069
|
-
//#region src/output/format.ts
|
|
2070
|
-
function dropEchoedTitle(body, title) {
|
|
2071
|
-
const blocks = parseBlocks(body);
|
|
2072
|
-
if (blocks.length === 0) return body;
|
|
2073
|
-
const first = blocks[0];
|
|
2074
|
-
if (first.kind === "heading" && headingText(body.slice(first.start, first.end)) === title.trim()) return body.slice(first.end).replace(/^\n+/, "");
|
|
2075
|
-
return body;
|
|
2076
|
-
}
|
|
2077
|
-
function renderMarkdown(input) {
|
|
2078
|
-
const title = input.metadata.title?.trim();
|
|
2079
|
-
let body = input.markdown;
|
|
2080
|
-
if (title) {
|
|
2081
|
-
body = dropEchoedTitle(body, title);
|
|
2082
|
-
return `# ${title}\n\n${body}`.replace(/\n+$/, "\n");
|
|
1653
|
+
function groupSections(units) {
|
|
1654
|
+
const sections = [];
|
|
1655
|
+
let current = [];
|
|
1656
|
+
let currentContext = "";
|
|
1657
|
+
function flush() {
|
|
1658
|
+
if (current.length > 0) {
|
|
1659
|
+
sections.push({
|
|
1660
|
+
headingContext: currentContext,
|
|
1661
|
+
units: current
|
|
1662
|
+
});
|
|
1663
|
+
current = [];
|
|
1664
|
+
}
|
|
2083
1665
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
"publishedTime",
|
|
2092
|
-
"excerpt",
|
|
2093
|
-
"canonical",
|
|
2094
|
-
"baseUrl",
|
|
2095
|
-
"wordCount",
|
|
2096
|
-
"readingTimeMin",
|
|
2097
|
-
"tokenEstimate",
|
|
2098
|
-
"estimator"
|
|
2099
|
-
];
|
|
2100
|
-
function pickMetadata(metadata) {
|
|
2101
|
-
const picked = {};
|
|
2102
|
-
for (const key of METADATA_KEYS) {
|
|
2103
|
-
const value = metadata[key];
|
|
2104
|
-
if (value !== void 0) picked[key] = value;
|
|
1666
|
+
for (const unit of units) if (unit.kind === "heading") {
|
|
1667
|
+
flush();
|
|
1668
|
+
current = [unit];
|
|
1669
|
+
currentContext = unit.headingContext;
|
|
1670
|
+
} else {
|
|
1671
|
+
if (current.length === 0) currentContext = unit.headingContext;
|
|
1672
|
+
current.push(unit);
|
|
2105
1673
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
function yamlFrontmatter(metadata) {
|
|
2109
|
-
return `---\n${stringify(pickMetadata(metadata), { lineWidth: 0 })}---\n`;
|
|
1674
|
+
flush();
|
|
1675
|
+
return sections;
|
|
2110
1676
|
}
|
|
2111
|
-
function
|
|
2112
|
-
|
|
1677
|
+
function joinedLength(units) {
|
|
1678
|
+
if (units.length === 0) return 0;
|
|
1679
|
+
let total = units[0].text.length;
|
|
1680
|
+
for (let i = 1; i < units.length; i++) total += 2 + units[i].text.length;
|
|
1681
|
+
return total;
|
|
2113
1682
|
}
|
|
2114
|
-
function
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
1683
|
+
function splitOversizedTextUnit(unit, maxChars) {
|
|
1684
|
+
return hardSplitLines(unit.text, maxChars).map((text) => ({
|
|
1685
|
+
headingContext: unit.headingContext,
|
|
1686
|
+
kind: "text",
|
|
1687
|
+
text
|
|
1688
|
+
}));
|
|
2118
1689
|
}
|
|
2119
|
-
function
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
};
|
|
2129
|
-
return JSON.stringify(body, null, 2);
|
|
1690
|
+
function buildBaseGroups(sections, maxChars) {
|
|
1691
|
+
const groups = [];
|
|
1692
|
+
let current = [];
|
|
1693
|
+
let currentLen = 0;
|
|
1694
|
+
function emit() {
|
|
1695
|
+
if (current.length > 0) {
|
|
1696
|
+
groups.push(current);
|
|
1697
|
+
current = [];
|
|
1698
|
+
currentLen = 0;
|
|
2130
1699
|
}
|
|
2131
|
-
case "text":
|
|
2132
|
-
payload = input.textContent;
|
|
2133
|
-
break;
|
|
2134
|
-
default: payload = renderMarkdown(input);
|
|
2135
1700
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
const purify = DOMPurify(window);
|
|
2147
|
-
return {
|
|
2148
|
-
html: purify.sanitize(dirty),
|
|
2149
|
-
iframesRemoved: countRemoved(purify.removed, "IFRAME"),
|
|
2150
|
-
scriptsRemoved: countRemoved(purify.removed, "SCRIPT")
|
|
2151
|
-
};
|
|
2152
|
-
}
|
|
2153
|
-
//#endregion
|
|
2154
|
-
//#region src/policy/footnotes.ts
|
|
2155
|
-
var FOOTNOTE_SIGNAL_RE = /cite_note|cite_ref|class="footnotes"|class="references"|data-footnote|role="doc-endnote|<sup\b[^>]*>\s*<a\s[^>]*href="#/i;
|
|
2156
|
-
var DEFINITION_CONTAINER_SELECTORS = [
|
|
2157
|
-
"ol.footnotes",
|
|
2158
|
-
"ol[class*=\"footnotes\"]",
|
|
2159
|
-
"ol.references",
|
|
2160
|
-
"ol[class*=\"references\"]",
|
|
2161
|
-
"section[class*=\"footnote\"]",
|
|
2162
|
-
"div[class*=\"footnote\"]",
|
|
2163
|
-
"[role=\"doc-endnotes\"]",
|
|
2164
|
-
"[role=\"doc-bibliography\"]"
|
|
2165
|
-
];
|
|
2166
|
-
var STANDALONE_DEF_ID_RE = /^(?:fn|cite_note|footnote|note)[:_-]/i;
|
|
2167
|
-
var BACKREF_LEADING_RE = /^(?:↑\s?|↩\s?|\^\s|Jump to\s*)/;
|
|
2168
|
-
function cleanDefText(text) {
|
|
2169
|
-
return text.replace(/\s+/g, " ").trim().replace(BACKREF_LEADING_RE, "");
|
|
2170
|
-
}
|
|
2171
|
-
function collectDefinitions(document) {
|
|
2172
|
-
const containers = /* @__PURE__ */ new Set();
|
|
2173
|
-
const defs = /* @__PURE__ */ new Map();
|
|
2174
|
-
const standaloneIds = /* @__PURE__ */ new Set();
|
|
2175
|
-
for (const selector of DEFINITION_CONTAINER_SELECTORS) {
|
|
2176
|
-
let matched;
|
|
2177
|
-
try {
|
|
2178
|
-
matched = document.querySelectorAll(selector);
|
|
2179
|
-
} catch {
|
|
1701
|
+
function append(unit) {
|
|
1702
|
+
const sep = current.length > 0 ? 2 : 0;
|
|
1703
|
+
current.push(unit);
|
|
1704
|
+
currentLen += sep + unit.text.length;
|
|
1705
|
+
}
|
|
1706
|
+
for (const section of sections) {
|
|
1707
|
+
const sectionLen = joinedLength(section.units);
|
|
1708
|
+
const sep = current.length > 0 ? 2 : 0;
|
|
1709
|
+
if (currentLen + sep + sectionLen <= maxChars) {
|
|
1710
|
+
for (const unit of section.units) append(unit);
|
|
2180
1711
|
continue;
|
|
2181
1712
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
for (const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
1713
|
+
emit();
|
|
1714
|
+
if (sectionLen <= maxChars) {
|
|
1715
|
+
for (const unit of section.units) append(unit);
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
const [headingUnit, ...body] = section.units;
|
|
1719
|
+
current.push(headingUnit);
|
|
1720
|
+
currentLen = headingUnit.text.length;
|
|
1721
|
+
for (const unit of body) {
|
|
1722
|
+
if (unit.kind === "code" && unit.text.length > maxChars) {
|
|
1723
|
+
emit();
|
|
1724
|
+
groups.push([unit]);
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
const sepNow = current.length > 0 ? 2 : 0;
|
|
1728
|
+
if (unit.text.length <= maxChars && currentLen + sepNow + unit.text.length <= maxChars) {
|
|
1729
|
+
append(unit);
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
const pieces = unit.text.length > maxChars ? splitOversizedTextUnit(unit, maxChars) : [unit];
|
|
1733
|
+
for (const piece of pieces) {
|
|
1734
|
+
const pieceSep = current.length > 0 ? 2 : 0;
|
|
1735
|
+
if (current.length > 0 && currentLen + pieceSep + piece.text.length > maxChars) emit();
|
|
1736
|
+
append(piece);
|
|
2189
1737
|
}
|
|
2190
1738
|
}
|
|
2191
1739
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
1740
|
+
emit();
|
|
1741
|
+
return groups;
|
|
1742
|
+
}
|
|
1743
|
+
function applySemanticOverlap(groups, overlapChars) {
|
|
1744
|
+
const result = [];
|
|
1745
|
+
for (let i = 0; i < groups.length; i++) {
|
|
1746
|
+
const group = groups[i];
|
|
1747
|
+
const carrier = [];
|
|
1748
|
+
if (i > 0 && overlapChars > 0) {
|
|
1749
|
+
const prev = groups[i - 1];
|
|
1750
|
+
let end = prev.length;
|
|
1751
|
+
while (end > 0 && prev[end - 1].kind === "code") end -= 1;
|
|
1752
|
+
let carrierLen = 0;
|
|
1753
|
+
for (let j = end - 1; j >= 0; j--) {
|
|
1754
|
+
const unit = prev[j];
|
|
1755
|
+
if (unit.kind !== "text") break;
|
|
1756
|
+
const sep = carrier.length > 0 ? 2 : 0;
|
|
1757
|
+
if (carrierLen + sep + unit.text.length > overlapChars) break;
|
|
1758
|
+
carrier.unshift(unit);
|
|
1759
|
+
carrierLen += sep + unit.text.length;
|
|
1760
|
+
}
|
|
2199
1761
|
}
|
|
1762
|
+
const units = carrier.length === 0 ? group : [...carrier, ...group];
|
|
1763
|
+
result.push({
|
|
1764
|
+
headingContext: group[0]?.headingContext ?? "",
|
|
1765
|
+
units
|
|
1766
|
+
});
|
|
2200
1767
|
}
|
|
2201
|
-
return
|
|
2202
|
-
containers,
|
|
2203
|
-
defs,
|
|
2204
|
-
standaloneIds
|
|
2205
|
-
};
|
|
1768
|
+
return result;
|
|
2206
1769
|
}
|
|
2207
|
-
function
|
|
2208
|
-
if (!
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
const
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
let n = defIdToNumber.get(frag);
|
|
2227
|
-
if (n === void 0) {
|
|
2228
|
-
n = defIdToNumber.size + 1;
|
|
2229
|
-
defIdToNumber.set(frag, n);
|
|
2230
|
-
}
|
|
2231
|
-
refHits.push({
|
|
2232
|
-
defId: frag,
|
|
2233
|
-
n,
|
|
2234
|
-
sup
|
|
2235
|
-
});
|
|
2236
|
-
} catch {}
|
|
2237
|
-
}
|
|
2238
|
-
if (refHits.length === 0) return null;
|
|
2239
|
-
for (const { n, sup } of refHits) try {
|
|
2240
|
-
sup.replaceWith(document.createTextNode(`[^${n}]`));
|
|
2241
|
-
} catch {}
|
|
2242
|
-
for (const container of containers) if (container.isConnected) container.remove();
|
|
2243
|
-
for (const id of standaloneIds) {
|
|
2244
|
-
const el = document.getElementById(id);
|
|
2245
|
-
if (el?.isConnected) el.remove();
|
|
2246
|
-
}
|
|
2247
|
-
const numberToDefId = /* @__PURE__ */ new Map();
|
|
2248
|
-
for (const [id, n] of defIdToNumber) numberToDefId.set(n, id);
|
|
2249
|
-
const footnoteDefs = [];
|
|
2250
|
-
for (let n = 1; n <= numberToDefId.size; n++) {
|
|
2251
|
-
const id = numberToDefId.get(n);
|
|
2252
|
-
if (id === void 0) break;
|
|
2253
|
-
const text = defs.get(id);
|
|
2254
|
-
if (text !== void 0) footnoteDefs.push(text);
|
|
1770
|
+
function chunkMarkdownSemantic(markdown, options) {
|
|
1771
|
+
if (!markdown) return [];
|
|
1772
|
+
const maxChars = Math.max(1, Math.floor(options.maxTokens)) * 4;
|
|
1773
|
+
const overlapTokens = Math.max(0, Math.floor(options.overlap));
|
|
1774
|
+
const overlapChars = Math.min(overlapTokens * 4, maxChars - 1);
|
|
1775
|
+
const units = parseSemanticUnits(markdown);
|
|
1776
|
+
if (units.length === 0) return [];
|
|
1777
|
+
const groups = applySemanticOverlap(buildBaseGroups(groupSections(units), maxChars), overlapChars);
|
|
1778
|
+
const chunks = [];
|
|
1779
|
+
for (const group of groups) {
|
|
1780
|
+
if (group.units.length === 0) continue;
|
|
1781
|
+
const text = group.units.map((unit) => unit.text).join("\n\n").trim();
|
|
1782
|
+
if (!text) continue;
|
|
1783
|
+
chunks.push({
|
|
1784
|
+
headingContext: group.headingContext,
|
|
1785
|
+
index: chunks.length,
|
|
1786
|
+
text,
|
|
1787
|
+
tokenCount: Math.round(text.length / 4)
|
|
1788
|
+
});
|
|
2255
1789
|
}
|
|
2256
|
-
return
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
1790
|
+
return chunks;
|
|
1791
|
+
}
|
|
1792
|
+
function chunkMarkdown(markdown, options) {
|
|
1793
|
+
if (options.strategy === "semantic") return chunkMarkdownSemantic(markdown, options);
|
|
1794
|
+
return chunkMarkdownChar(markdown, options);
|
|
2260
1795
|
}
|
|
2261
1796
|
//#endregion
|
|
2262
|
-
//#region src/
|
|
2263
|
-
function
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
fence: "```",
|
|
2269
|
-
headingStyle: options?.headingStyle ?? "atx",
|
|
2270
|
-
strongDelimiter: "**"
|
|
2271
|
-
});
|
|
2272
|
-
if (options?.gfm !== false) service.use(gfm);
|
|
2273
|
-
const tableFormat = options?.tables;
|
|
2274
|
-
if (tableFormat !== void 0) service.addRule("tableMatrix", {
|
|
2275
|
-
filter: (node) => node.nodeName === "TABLE",
|
|
2276
|
-
replacement: (_content, node) => {
|
|
2277
|
-
const matrix = parseTableMatrix(node);
|
|
2278
|
-
if (matrix.length === 0) return "";
|
|
2279
|
-
const body = renderTable(matrix, tableFormat);
|
|
2280
|
-
if (tableFormat === "gfm") return `\n\n${body}\n\n`;
|
|
2281
|
-
return `\n\n\`\`\`${tableFormat}\n${body}\n\`\`\`\n\n`;
|
|
2282
|
-
}
|
|
2283
|
-
});
|
|
2284
|
-
const imageMode = options?.images;
|
|
2285
|
-
const baseUrl = options?.baseUrl;
|
|
2286
|
-
const references = [];
|
|
2287
|
-
applyImagePolicy(service, imageMode, baseUrl, references);
|
|
2288
|
-
service.addRule("anchorAbsolutize", {
|
|
2289
|
-
filter: "a",
|
|
2290
|
-
replacement: (content, node) => {
|
|
2291
|
-
const rawHref = node.getAttribute("href");
|
|
2292
|
-
if (!rawHref) return content;
|
|
2293
|
-
const href = absolutize(rawHref, baseUrl);
|
|
2294
|
-
const title = node.getAttribute("title");
|
|
2295
|
-
return `[${content}](${href}${title ? ` "${title.replace(/"/g, "\\\"")}"` : ""})`;
|
|
2296
|
-
}
|
|
2297
|
-
});
|
|
2298
|
-
service.addRule("mathMarker", {
|
|
2299
|
-
filter: (node) => node.nodeName === "SPAN" && node.classList.contains("rdrm-math"),
|
|
2300
|
-
replacement: (_content, node) => {
|
|
2301
|
-
const tex = node.textContent.trim();
|
|
2302
|
-
if (!tex) return "";
|
|
2303
|
-
return node.getAttribute("data-display") === "true" ? `$$${tex}$$` : `$${tex}$`;
|
|
2304
|
-
}
|
|
2305
|
-
});
|
|
2306
|
-
const fnResult = processFootnotes(html);
|
|
2307
|
-
const sourceHtml = fnResult?.html ?? html;
|
|
2308
|
-
let body = service.turndown(sourceHtml);
|
|
2309
|
-
if (fnResult) for (let n = 1; n <= fnResult.footnoteDefs.length; n++) body = body.replaceAll(`\\[^${n}\\]`, `[^${n}]`);
|
|
2310
|
-
const trailingBlocks = [];
|
|
2311
|
-
if (imageMode === "reference" && references.length > 0) trailingBlocks.push(references.map((ref, i) => `[img-${i + 1}]: ${ref}`).join("\n"));
|
|
2312
|
-
if (fnResult && fnResult.footnoteDefs.length > 0) trailingBlocks.push(fnResult.footnoteDefs.map((def, i) => `[^${i + 1}]: ${def}`).join("\n"));
|
|
2313
|
-
if (trailingBlocks.length === 0) return body;
|
|
2314
|
-
return `${body.replace(/\n+$/, "")}\n\n${trailingBlocks.join("\n")}`;
|
|
1797
|
+
//#region src/policy/diagnostics.ts
|
|
1798
|
+
function countElements(html, window) {
|
|
1799
|
+
if (!window || !html) return 0;
|
|
1800
|
+
const template = window.document.createElement("div");
|
|
1801
|
+
template.innerHTML = html;
|
|
1802
|
+
return template.querySelectorAll("*").length;
|
|
2315
1803
|
}
|
|
2316
|
-
function
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
1804
|
+
function assembleDiagnostics(input) {
|
|
1805
|
+
const articleElementCount = countElements(input.articleHtml ?? "", input.window);
|
|
1806
|
+
const removedNodes = Math.max(0, (input.documentElementCount ?? 0) - articleElementCount);
|
|
1807
|
+
return {
|
|
1808
|
+
readerable: input.readerable,
|
|
1809
|
+
extractedNode: input.extractedNode,
|
|
1810
|
+
fallbackUsed: input.fallbackUsed ?? false,
|
|
1811
|
+
gated: input.gated,
|
|
1812
|
+
imagesResolved: input.imagesResolved,
|
|
1813
|
+
pagination: input.pagination,
|
|
1814
|
+
removedNodes,
|
|
1815
|
+
boilerplateRemoved: input.boilerplateRemoved,
|
|
1816
|
+
chromeRemoved: input.chromeRemoved,
|
|
1817
|
+
sanitization: input.sanitization,
|
|
1818
|
+
trace: input.trace,
|
|
1819
|
+
truncated: input.truncated ?? false,
|
|
1820
|
+
...input.cache ? { cache: input.cache } : {},
|
|
1821
|
+
...input.preset ? { preset: input.preset } : {}
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
var TraceCollector = class {
|
|
1825
|
+
enabled;
|
|
1826
|
+
entries = [];
|
|
1827
|
+
constructor(enabled) {
|
|
1828
|
+
this.enabled = enabled;
|
|
1829
|
+
}
|
|
1830
|
+
collect() {
|
|
1831
|
+
return this.enabled ? this.entries : void 0;
|
|
1832
|
+
}
|
|
1833
|
+
run(stage, fn) {
|
|
1834
|
+
if (!this.enabled) return fn();
|
|
1835
|
+
const start = performance.now();
|
|
1836
|
+
try {
|
|
1837
|
+
return fn();
|
|
1838
|
+
} finally {
|
|
1839
|
+
this.entries.push({
|
|
1840
|
+
ms: performance.now() - start,
|
|
1841
|
+
stage
|
|
2345
1842
|
});
|
|
2346
|
-
|
|
2347
|
-
case "src-only": service.addRule("imageSrcOnly", {
|
|
2348
|
-
filter: "img",
|
|
2349
|
-
replacement: (_content, node) => {
|
|
2350
|
-
const src = absolutize(node.getAttribute("src") ?? "", baseUrl);
|
|
2351
|
-
return src ? `\n\n${src}\n\n` : "";
|
|
2352
|
-
}
|
|
2353
|
-
});
|
|
1843
|
+
}
|
|
2354
1844
|
}
|
|
2355
|
-
}
|
|
1845
|
+
};
|
|
2356
1846
|
//#endregion
|
|
2357
1847
|
//#region src/policy/fallback.ts
|
|
2358
1848
|
var MIN_DENSE_BLOCK_CHARS = 200;
|
|
@@ -2431,6 +1921,67 @@ function extractViaFallback(document, options) {
|
|
|
2431
1921
|
return null;
|
|
2432
1922
|
}
|
|
2433
1923
|
//#endregion
|
|
1924
|
+
//#region src/policy/gating.ts
|
|
1925
|
+
var PAYWALL_SELECTORS = [
|
|
1926
|
+
".piano",
|
|
1927
|
+
"#piano",
|
|
1928
|
+
".tp-modal",
|
|
1929
|
+
".tp-active",
|
|
1930
|
+
"[class*=\"subscribe-wall\"]",
|
|
1931
|
+
"[id*=\"subscribe-wall\"]",
|
|
1932
|
+
"[class*=\"metered-wall\"]",
|
|
1933
|
+
"[id*=\"metered-wall\"]",
|
|
1934
|
+
".leaky-paywall"
|
|
1935
|
+
];
|
|
1936
|
+
var PAYWALL_ATTR_CANDIDATES = "[class*=\"paywall\"], [id*=\"paywall\"], [class*=\"piano\"], [id*=\"piano\"]";
|
|
1937
|
+
var NEGATION_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1938
|
+
"bypass",
|
|
1939
|
+
"disabled",
|
|
1940
|
+
"exempt",
|
|
1941
|
+
"free",
|
|
1942
|
+
"ineligible",
|
|
1943
|
+
"no",
|
|
1944
|
+
"non",
|
|
1945
|
+
"not",
|
|
1946
|
+
"off",
|
|
1947
|
+
"optout",
|
|
1948
|
+
"without"
|
|
1949
|
+
]);
|
|
1950
|
+
function namesGateSurface(classAndId, noun) {
|
|
1951
|
+
for (const token of classAndId.split(/\s+/)) {
|
|
1952
|
+
const segments = token.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z]+/).filter(Boolean);
|
|
1953
|
+
if (segments[0] !== noun) continue;
|
|
1954
|
+
if (segments.some((segment) => NEGATION_SEGMENTS.has(segment))) continue;
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
var METERED_TEXT_RE = /(\d+)\s*(?:free\s*)?(?:articles?|stories?)\s*(?:left|remaining)|you\s+have\s+reached\s+(?:your\s+)?(?:free\s+)?(?:article\s+|story\s+)?limit|subscribe\s+to\s+(?:continue\s+)?reading|read\s+the\s+full\s+(?:article|story)|unlock\s+(?:this|full|all)\s+(?:article|story|content)|keep\s+reading\s+with/i;
|
|
1960
|
+
function findPaywallOverlay(document) {
|
|
1961
|
+
for (const el of document.querySelectorAll(PAYWALL_ATTR_CANDIDATES)) {
|
|
1962
|
+
const classAndId = `${el.getAttribute("class") ?? ""} ${el.getAttribute("id") ?? ""}`;
|
|
1963
|
+
if (!el.isConnected) continue;
|
|
1964
|
+
if (namesGateSurface(classAndId, "paywall") || namesGateSurface(classAndId, "piano")) return {
|
|
1965
|
+
likely: true,
|
|
1966
|
+
reason: "paywall overlay"
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
for (const selector of PAYWALL_SELECTORS) if (document.querySelector(selector)?.isConnected) return {
|
|
1970
|
+
likely: true,
|
|
1971
|
+
reason: "paywall overlay"
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
function findMeteredMessage(document) {
|
|
1975
|
+
const text = document.body.textContent;
|
|
1976
|
+
if (METERED_TEXT_RE.test(text)) return {
|
|
1977
|
+
likely: true,
|
|
1978
|
+
reason: "metered paywall message"
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
function detectGating(document) {
|
|
1982
|
+
return findPaywallOverlay(document) ?? findMeteredMessage(document);
|
|
1983
|
+
}
|
|
1984
|
+
//#endregion
|
|
2434
1985
|
//#region src/policy/images.ts
|
|
2435
1986
|
function positiveInt(value) {
|
|
2436
1987
|
if (value === null) return;
|
|
@@ -2463,25 +2014,242 @@ function collectImageInventory(html, window, baseUrl) {
|
|
|
2463
2014
|
return entries;
|
|
2464
2015
|
}
|
|
2465
2016
|
//#endregion
|
|
2466
|
-
//#region src/policy/
|
|
2467
|
-
var
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2017
|
+
//#region src/policy/metadata.ts
|
|
2018
|
+
var ARTICLE_TYPES = /* @__PURE__ */ new Set([
|
|
2019
|
+
"Article",
|
|
2020
|
+
"BlogPosting",
|
|
2021
|
+
"NewsArticle",
|
|
2022
|
+
"Report",
|
|
2023
|
+
"ScholarlyArticle",
|
|
2024
|
+
"SocialMediaPosting",
|
|
2025
|
+
"TechArticle",
|
|
2026
|
+
"WebPage"
|
|
2027
|
+
]);
|
|
2028
|
+
function first(...values) {
|
|
2029
|
+
for (const value of values) {
|
|
2030
|
+
const picked = nonEmpty(value);
|
|
2031
|
+
if (picked) return picked;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
function metaProperty(document, property) {
|
|
2035
|
+
return nonEmpty(document.querySelector(`meta[property="${property}"]`)?.getAttribute("content") ?? void 0);
|
|
2036
|
+
}
|
|
2037
|
+
function metaName(document, name) {
|
|
2038
|
+
return nonEmpty(document.querySelector(`meta[name="${name}"]`)?.getAttribute("content") ?? void 0);
|
|
2039
|
+
}
|
|
2040
|
+
function parseJsonLd(document) {
|
|
2041
|
+
const nodes = document.querySelectorAll("script[type=\"application/ld+json\"]");
|
|
2042
|
+
const out = [];
|
|
2043
|
+
nodes.forEach((node) => {
|
|
2044
|
+
const raw = node.textContent;
|
|
2045
|
+
if (!raw.trim()) return;
|
|
2046
|
+
let parsed;
|
|
2047
|
+
try {
|
|
2048
|
+
parsed = JSON.parse(raw);
|
|
2049
|
+
} catch {
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
collectObjects(parsed, out);
|
|
2053
|
+
});
|
|
2054
|
+
return out;
|
|
2055
|
+
}
|
|
2056
|
+
function collectObjects(value, out) {
|
|
2057
|
+
if (!value || typeof value !== "object") return;
|
|
2058
|
+
if (Array.isArray(value)) {
|
|
2059
|
+
for (const item of value) collectObjects(item, out);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
if ("@graph" in value) collectObjects(value["@graph"], out);
|
|
2063
|
+
out.push(value);
|
|
2064
|
+
}
|
|
2065
|
+
function typeMatches(type) {
|
|
2066
|
+
return (Array.isArray(type) ? type : [type]).some((t) => typeof t === "string" && ARTICLE_TYPES.has(t));
|
|
2067
|
+
}
|
|
2068
|
+
function pickArticleNode(candidates) {
|
|
2069
|
+
if (candidates.length === 0) return;
|
|
2070
|
+
return candidates.find((node) => typeMatches(node["@type"])) ?? candidates[0];
|
|
2071
|
+
}
|
|
2072
|
+
var STRUCTURED_PRIORITY = [
|
|
2073
|
+
"Recipe",
|
|
2074
|
+
"Product",
|
|
2075
|
+
"Event",
|
|
2076
|
+
"HowTo",
|
|
2077
|
+
"Course",
|
|
2078
|
+
"Movie",
|
|
2079
|
+
"Book",
|
|
2080
|
+
"MusicRecording",
|
|
2081
|
+
"JobPosting",
|
|
2082
|
+
"FAQPage",
|
|
2083
|
+
...ARTICLE_TYPES
|
|
2084
|
+
];
|
|
2085
|
+
function nodeTypeList(node) {
|
|
2086
|
+
const type = node["@type"];
|
|
2087
|
+
if (Array.isArray(type)) return type.filter((t) => typeof t === "string");
|
|
2088
|
+
return typeof type === "string" ? [type] : [];
|
|
2089
|
+
}
|
|
2090
|
+
function pickStructuredObject(candidates) {
|
|
2091
|
+
for (const priorityType of STRUCTURED_PRIORITY) {
|
|
2092
|
+
const hit = candidates.find((node) => nodeTypeList(node).includes(priorityType));
|
|
2093
|
+
if (hit) return hit;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
function cleanStructured(obj) {
|
|
2097
|
+
const out = {};
|
|
2098
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2099
|
+
if (key === "@context") continue;
|
|
2100
|
+
if (key === "@type") {
|
|
2101
|
+
out[key] = (Array.isArray(value) ? value.filter((t) => typeof t === "string") : typeof value === "string" ? [value] : []).join("+");
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
out[key] = value;
|
|
2105
|
+
}
|
|
2106
|
+
return out;
|
|
2107
|
+
}
|
|
2108
|
+
function resolveJsonLdAuthor(author) {
|
|
2109
|
+
const names = [];
|
|
2110
|
+
function visit(value) {
|
|
2111
|
+
if (typeof value === "string") {
|
|
2112
|
+
const trimmed = value.trim();
|
|
2113
|
+
if (trimmed) names.push(trimmed);
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
if (!value || typeof value !== "object") return;
|
|
2117
|
+
if (Array.isArray(value)) {
|
|
2118
|
+
value.forEach(visit);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const list = value["@list"];
|
|
2122
|
+
if (Array.isArray(list)) {
|
|
2123
|
+
list.forEach(visit);
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
const name = value.name;
|
|
2127
|
+
if (typeof name === "string") {
|
|
2128
|
+
const trimmed = name.trim();
|
|
2129
|
+
if (trimmed) names.push(trimmed);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
visit(author);
|
|
2133
|
+
return names.length > 0 ? names.join(", ") : void 0;
|
|
2134
|
+
}
|
|
2135
|
+
function asString(value) {
|
|
2136
|
+
return typeof value === "string" ? nonEmpty(value) : void 0;
|
|
2137
|
+
}
|
|
2138
|
+
function field(obj, key) {
|
|
2139
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return;
|
|
2140
|
+
return obj[key];
|
|
2141
|
+
}
|
|
2142
|
+
function resolveMetadata(input) {
|
|
2143
|
+
const { document, readability } = input;
|
|
2144
|
+
const jsonLdObjects = parseJsonLd(document);
|
|
2145
|
+
const jsonLd = pickArticleNode(jsonLdObjects);
|
|
2146
|
+
const structuredRaw = pickStructuredObject(jsonLdObjects);
|
|
2147
|
+
const htmlLang = nonEmpty(document.documentElement.getAttribute("lang") ?? void 0);
|
|
2148
|
+
const titleFromTitleTag = nonEmpty(document.title);
|
|
2149
|
+
return {
|
|
2150
|
+
title: first(asString(jsonLd?.headline), metaProperty(document, "og:title"), metaName(document, "twitter:title"), readability?.title ?? void 0, titleFromTitleTag),
|
|
2151
|
+
byline: first(jsonLd ? resolveJsonLdAuthor(jsonLd.author) : void 0, metaProperty(document, "article:author"), metaName(document, "author"), readability?.byline ?? void 0),
|
|
2152
|
+
siteName: first(asString(field(field(jsonLd, "publisher"), "name")), metaProperty(document, "og:site_name"), readability?.siteName ?? void 0),
|
|
2153
|
+
lang: first(asString(jsonLd?.inLanguage), htmlLang, readability?.lang ?? void 0),
|
|
2154
|
+
publishedTime: first(asString(jsonLd?.datePublished), metaProperty(document, "article:published_time"), nonEmpty(document.querySelector("time[datetime]")?.getAttribute("datetime") ?? void 0), readability?.publishedTime ?? void 0),
|
|
2155
|
+
excerpt: first(asString(jsonLd?.description), metaProperty(document, "og:description"), metaName(document, "twitter:description"), metaName(document, "description"), readability?.excerpt ?? void 0),
|
|
2156
|
+
canonical: first(nonEmpty(document.querySelector("link[rel=\"canonical\"]")?.getAttribute("href") ?? void 0), metaProperty(document, "og:url")),
|
|
2157
|
+
baseUrl: input.baseUrl,
|
|
2158
|
+
wordCount: input.wordCount,
|
|
2159
|
+
readingTimeMin: input.readingTimeMin,
|
|
2160
|
+
...estimateTokens(input.textContent),
|
|
2161
|
+
...structuredRaw ? { structured: cleanStructured(structuredRaw) } : {}
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
//#endregion
|
|
2165
|
+
//#region src/policy/pagination.ts
|
|
2166
|
+
var NEXT_LINK_TEXT_RE = /^(next(\s+page)?|older(\s+posts?)?|[›»→]|next\s*[›»→]|older\s*[›»→])$/i;
|
|
2167
|
+
function usableHref(href) {
|
|
2168
|
+
if (!href || href === "#") return;
|
|
2169
|
+
return href;
|
|
2170
|
+
}
|
|
2171
|
+
function findPaginated(document, baseUrl) {
|
|
2172
|
+
const linkNext = document.querySelector("link[rel=\"next\"][href]");
|
|
2173
|
+
if (linkNext) {
|
|
2174
|
+
const href = usableHref(linkNext.getAttribute("href"));
|
|
2175
|
+
if (href) return {
|
|
2176
|
+
type: "paginated",
|
|
2177
|
+
nextUrl: absolutize(href, baseUrl)
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
const aRelNext = document.querySelector("a[rel=\"next\"][href]");
|
|
2181
|
+
if (aRelNext) {
|
|
2182
|
+
const href = usableHref(aRelNext.getAttribute("href"));
|
|
2183
|
+
if (href) return {
|
|
2184
|
+
type: "paginated",
|
|
2185
|
+
nextUrl: absolutize(href, baseUrl)
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
for (const anchor of document.querySelectorAll("a[href]")) {
|
|
2189
|
+
const text = anchor.textContent.trim();
|
|
2190
|
+
if (!text || !NEXT_LINK_TEXT_RE.test(text)) continue;
|
|
2191
|
+
const href = usableHref(anchor.getAttribute("href"));
|
|
2192
|
+
if (!href) continue;
|
|
2193
|
+
return {
|
|
2194
|
+
type: "paginated",
|
|
2195
|
+
nextUrl: absolutize(href, baseUrl)
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
var INFINITE_ATTR_SELECTORS = [
|
|
2200
|
+
"[data-load-more]",
|
|
2201
|
+
"[data-infinite-scroll]",
|
|
2202
|
+
"[data-pagination]",
|
|
2203
|
+
"[infinite-scroll]"
|
|
2204
|
+
];
|
|
2205
|
+
var INFINITE_SUBSTRING_SELECTORS = [
|
|
2206
|
+
"[class*=\"load-more\"]",
|
|
2207
|
+
"[class*=\"loadmore\"]",
|
|
2208
|
+
"[class*=\"infinite\"]",
|
|
2209
|
+
"[id*=\"load-more\"]",
|
|
2210
|
+
"[class*=\"sentinel\"]"
|
|
2211
|
+
];
|
|
2212
|
+
var LOAD_MORE_BUTTON_RE = /^(load more|show more|view more|more results|load more comments)$/i;
|
|
2213
|
+
function findInfinite(document) {
|
|
2214
|
+
for (const selector of INFINITE_ATTR_SELECTORS) if (document.querySelector(selector)?.isConnected) return {
|
|
2215
|
+
selector,
|
|
2216
|
+
type: "infinite"
|
|
2217
|
+
};
|
|
2218
|
+
for (const selector of INFINITE_SUBSTRING_SELECTORS) if (document.querySelector(selector)?.isConnected) return {
|
|
2219
|
+
selector,
|
|
2220
|
+
type: "infinite"
|
|
2221
|
+
};
|
|
2222
|
+
for (const button of document.querySelectorAll("button")) {
|
|
2223
|
+
const text = button.textContent.trim();
|
|
2224
|
+
if (text && LOAD_MORE_BUTTON_RE.test(text)) return {
|
|
2225
|
+
selector: "button",
|
|
2226
|
+
type: "infinite"
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
function detectPagination(document, baseUrl) {
|
|
2231
|
+
return findPaginated(document, baseUrl) ?? findInfinite(document);
|
|
2232
|
+
}
|
|
2233
|
+
//#endregion
|
|
2234
|
+
//#region src/policy/truncate.ts
|
|
2235
|
+
var TRUNCATION_MARKER = "\n\n…[truncated]";
|
|
2236
|
+
function truncateMarkdown(markdown, maxChars) {
|
|
2237
|
+
if (markdown.length <= maxChars) return {
|
|
2238
|
+
text: markdown,
|
|
2239
|
+
truncated: false
|
|
2240
|
+
};
|
|
2241
|
+
const blocks = parseBlocks(markdown);
|
|
2242
|
+
let start = -1;
|
|
2243
|
+
let end = -1;
|
|
2244
|
+
let truncated = false;
|
|
2245
|
+
for (const block of blocks) {
|
|
2246
|
+
const from = start === -1 ? block.start : start;
|
|
2247
|
+
if (block.end - from > maxChars) {
|
|
2248
|
+
if (start === -1 && block.kind !== "code") return {
|
|
2249
|
+
text: (hardSplitLines(markdown.slice(block.start, block.end), maxChars)[0] ?? "").replace(/\s+$/, "") + TRUNCATION_MARKER,
|
|
2250
|
+
truncated: true
|
|
2251
|
+
};
|
|
2252
|
+
truncated = true;
|
|
2485
2253
|
break;
|
|
2486
2254
|
}
|
|
2487
2255
|
if (start === -1) start = block.start;
|
|
@@ -2497,6 +2265,348 @@ function truncateMarkdown(markdown, maxChars) {
|
|
|
2497
2265
|
};
|
|
2498
2266
|
}
|
|
2499
2267
|
//#endregion
|
|
2268
|
+
//#region src/tools/html-source.ts
|
|
2269
|
+
function readHtmlFile(localPath) {
|
|
2270
|
+
const raw = readFileSync(localPath, "utf8");
|
|
2271
|
+
const trimmed = raw.trim();
|
|
2272
|
+
if (trimmed.length < 2 || trimmed.at(0) !== "\"" || trimmed.at(-1) !== "\"") return raw;
|
|
2273
|
+
try {
|
|
2274
|
+
const parsed = JSON.parse(trimmed);
|
|
2275
|
+
return typeof parsed === "string" ? parsed : raw;
|
|
2276
|
+
} catch {
|
|
2277
|
+
return raw;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
//#endregion
|
|
2281
|
+
//#region src/tools/schemas.ts
|
|
2282
|
+
var formatSchema = z.enum([
|
|
2283
|
+
"html",
|
|
2284
|
+
"json",
|
|
2285
|
+
"markdown",
|
|
2286
|
+
"text"
|
|
2287
|
+
]);
|
|
2288
|
+
var metadataModeSchema = z.enum([
|
|
2289
|
+
"json",
|
|
2290
|
+
"none",
|
|
2291
|
+
"yaml"
|
|
2292
|
+
]);
|
|
2293
|
+
var extractionSchema = z.enum([
|
|
2294
|
+
"aggressive",
|
|
2295
|
+
"balanced",
|
|
2296
|
+
"conservative"
|
|
2297
|
+
]);
|
|
2298
|
+
var headingStyleSchema = z.enum(["atx", "setext"]);
|
|
2299
|
+
var codeBlockStyleSchema = z.enum(["fenced", "indented"]);
|
|
2300
|
+
var imageModeSchema = z.enum([
|
|
2301
|
+
"drop",
|
|
2302
|
+
"keep",
|
|
2303
|
+
"reference",
|
|
2304
|
+
"src-only"
|
|
2305
|
+
]);
|
|
2306
|
+
var tableFormatSchema = z.enum([
|
|
2307
|
+
"csv",
|
|
2308
|
+
"gfm",
|
|
2309
|
+
"json"
|
|
2310
|
+
]);
|
|
2311
|
+
var localPathField = z.string().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.");
|
|
2312
|
+
var selectorsSchema = z.object({
|
|
2313
|
+
include: z.string().optional().describe("CSS selector restricting extraction to a matching subtree (e.g. \"main\", \"article\", \".post\"). The first match replaces the document body before processing."),
|
|
2314
|
+
exclude: z.array(z.string()).optional().describe("CSS selectors for boilerplate to remove before extraction (e.g. [\"nav\", \"footer\", \"[role=banner]\"]).")
|
|
2315
|
+
}).optional().describe("Scope the extracted/converted content by CSS selector before processing.");
|
|
2316
|
+
var readabilityOverridesSchema = z.record(z.string(), z.unknown()).optional().describe("Escape hatch: a record spread verbatim into the Readability options. Unstable and unvalidated; overrides the extraction/keepClasses/maxNodes/minArticleLength knobs.");
|
|
2317
|
+
var chunkStrategySchema = z.enum(["char", "semantic"]);
|
|
2318
|
+
var chunkOptionsSchema = z.object({
|
|
2319
|
+
maxTokens: z.number().int().min(1).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)."),
|
|
2320
|
+
overlap: z.number().int().min(0).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.").default(0),
|
|
2321
|
+
strategy: chunkStrategySchema.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).").default("semantic")
|
|
2322
|
+
}).describe("Token-bounded chunking options for splitting the extracted markdown into RAG/embedding-ready slices.");
|
|
2323
|
+
var turndownOptionsShape = {
|
|
2324
|
+
debug: z.boolean().describe("Emit per-stage timings (normalize, readability, sanitize, turndown, metadata) under diagnostics.trace. Debug-only — leaves trace absent by default.").default(false),
|
|
2325
|
+
cleanChrome: z.boolean().describe("Strip browser chrome (scrollbars, consent/cookie banners, fixed nav and overlays) before conversion. These elements poison Readability density scoring and clutter fragment output.").default(true),
|
|
2326
|
+
codeBlockStyle: codeBlockStyleSchema.describe("Markdown code-block style: 'fenced' (triple backticks) or 'indented' (four-space).").default("fenced"),
|
|
2327
|
+
format: formatSchema.describe("Returned payload format: 'markdown' (default), 'html', 'text', or 'json' (emits {metadata, content, diagnostics}).").default("markdown"),
|
|
2328
|
+
gfm: z.boolean().describe("Enable GitHub-Flavored Markdown: tables, strikethrough, and task lists.").default(true),
|
|
2329
|
+
headingStyle: headingStyleSchema.describe("Markdown heading style: 'atx' (#) or 'setext' (underlining with = / -).").default("atx"),
|
|
2330
|
+
images: imageModeSchema.describe("Image handling: 'keep' (inline ), 'drop', 'src-only' (bare URL text), or 'reference' (link-reference style).").default("keep"),
|
|
2331
|
+
maxChars: z.number().int().min(0).describe("Truncate markdown/text output at a block boundary; never splits a fenced code block. Ignored for html/json formats.").optional(),
|
|
2332
|
+
metadataMode: metadataModeSchema.describe("Prepend a metadata block to the markdown/text payload: 'none' (default), 'yaml', or 'json'.").default("none"),
|
|
2333
|
+
sanitize: z.boolean().describe("Run DOMPurify over the extracted/fragment HTML before conversion (strips scripts, event handlers, and iframes).").default(true),
|
|
2334
|
+
tables: tableFormatSchema.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.").optional(),
|
|
2335
|
+
baseUrl: z.url().describe("Base URL for absolutizing relative links and images. NEVER fetched — origin context only.").optional(),
|
|
2336
|
+
wordsPerMinute: z.number().int().min(1).describe("Reading speed (words per minute) used to compute metadata.readingTimeMin.").default(200)
|
|
2337
|
+
};
|
|
2338
|
+
var extractInputShape = {
|
|
2339
|
+
localPath: localPathField,
|
|
2340
|
+
...turndownOptionsShape,
|
|
2341
|
+
cache: z.boolean().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.").default(false),
|
|
2342
|
+
extraction: extractionSchema.describe("Readability scoring aggressiveness: 'balanced' (default), 'aggressive', or 'conservative'. Maps to Readability's scorer knobs.").default("balanced"),
|
|
2343
|
+
keepClasses: z.boolean().describe("Retain all CSS classes on extracted nodes. Defaults false, which strips non-language classes.").default(false),
|
|
2344
|
+
maxNodes: z.number().int().min(0).describe("Hard cap on elements parsed (Readability maxElemsToParse). Safety/perf guard for very large documents.").optional(),
|
|
2345
|
+
minArticleLength: z.number().int().min(0).describe("Minimum article character length below which extraction falls back to the selector cascade (Readability charThreshold).").optional(),
|
|
2346
|
+
readabilityOverrides: readabilityOverridesSchema,
|
|
2347
|
+
selectors: selectorsSchema,
|
|
2348
|
+
chunk: chunkOptionsSchema.optional().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."),
|
|
2349
|
+
imageInventory: z.boolean().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.").default(false)
|
|
2350
|
+
};
|
|
2351
|
+
var extractInputSchema = z.object(extractInputShape);
|
|
2352
|
+
var htmlToMarkdownInputShape = {
|
|
2353
|
+
localPath: localPathField,
|
|
2354
|
+
...turndownOptionsShape,
|
|
2355
|
+
selectors: selectorsSchema
|
|
2356
|
+
};
|
|
2357
|
+
var htmlToMarkdownInputSchema = z.object(htmlToMarkdownInputShape);
|
|
2358
|
+
var outlineInputShape = {
|
|
2359
|
+
localPath: localPathField,
|
|
2360
|
+
baseUrl: z.url().describe("Base URL, carried through to metadata.baseUrl and used to absolutize links. NEVER fetched — origin context only.").optional(),
|
|
2361
|
+
selectors: selectorsSchema
|
|
2362
|
+
};
|
|
2363
|
+
var outlineInputSchema = z.object(outlineInputShape);
|
|
2364
|
+
var extractMetadataInputShape = {
|
|
2365
|
+
localPath: localPathField,
|
|
2366
|
+
baseUrl: z.url().describe("Base URL, carried through to metadata.baseUrl and used to absolutize links. NEVER fetched — origin context only.").optional()
|
|
2367
|
+
};
|
|
2368
|
+
var extractMetadataInputSchema = z.object(extractMetadataInputShape);
|
|
2369
|
+
var extractLinksInputShape = {
|
|
2370
|
+
localPath: localPathField,
|
|
2371
|
+
baseUrl: z.url().describe("Base URL for absolutizing relative hrefs and computing isExternal. NEVER fetched — origin context only.").optional(),
|
|
2372
|
+
sameOriginOnly: z.boolean().describe("Drop cross-origin links; keep same-origin, relative, and fragment links.").default(false),
|
|
2373
|
+
selectors: selectorsSchema
|
|
2374
|
+
};
|
|
2375
|
+
var extractLinksInputSchema = z.object(extractLinksInputShape);
|
|
2376
|
+
var chunkTextInputShape = {
|
|
2377
|
+
text: z.string().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."),
|
|
2378
|
+
maxTokens: z.number().int().min(1).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).").default(500),
|
|
2379
|
+
overlap: z.number().int().min(0).describe("Tokens to overlap between consecutive chunks (>=0). The trailing overlapChars of chunk N becomes the leading context of chunk N+1.").default(0),
|
|
2380
|
+
strategy: chunkStrategySchema.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.").default("semantic")
|
|
2381
|
+
};
|
|
2382
|
+
var chunkTextInputSchema = z.object(chunkTextInputShape);
|
|
2383
|
+
var extractSectionInputShape = {
|
|
2384
|
+
localPath: localPathField,
|
|
2385
|
+
baseUrl: z.url().describe("Base URL for absolutizing relative links and images. NEVER fetched — origin context only.").optional(),
|
|
2386
|
+
selector: z.string().describe("CSS selector scoping extraction to one subtree; passed straight through as selectors.include. Provide exactly one of selector/heading.").optional(),
|
|
2387
|
+
heading: z.string().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.").optional()
|
|
2388
|
+
};
|
|
2389
|
+
var extractSectionInputSchema = z.object(extractSectionInputShape).superRefine((value, ctx) => {
|
|
2390
|
+
if (value.selector !== void 0 === (value.heading !== void 0)) ctx.addIssue({
|
|
2391
|
+
code: "custom",
|
|
2392
|
+
message: "Provide exactly one of `selector` or `heading` (both set or both unset is invalid).",
|
|
2393
|
+
path: ["selector"]
|
|
2394
|
+
});
|
|
2395
|
+
});
|
|
2396
|
+
var extractTablesInputShape = {
|
|
2397
|
+
localPath: localPathField,
|
|
2398
|
+
baseUrl: z.url().describe("Base URL, carried through to metadata.baseUrl. NEVER fetched — origin context only.").optional(),
|
|
2399
|
+
format: tableFormatSchema.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).").default("gfm"),
|
|
2400
|
+
selectors: selectorsSchema
|
|
2401
|
+
};
|
|
2402
|
+
var extractTablesInputSchema = z.object(extractTablesInputShape);
|
|
2403
|
+
var extractListInputShape = {
|
|
2404
|
+
localPath: localPathField,
|
|
2405
|
+
baseUrl: z.url().describe("Base URL for absolutizing item hrefs against. NEVER fetched — origin context only.").optional(),
|
|
2406
|
+
selectors: selectorsSchema
|
|
2407
|
+
};
|
|
2408
|
+
var extractListInputSchema = z.object(extractListInputShape);
|
|
2409
|
+
var extractGridInputShape = {
|
|
2410
|
+
localPath: localPathField,
|
|
2411
|
+
baseUrl: z.url().describe("Base URL, carried through to metadata.baseUrl. NEVER fetched — origin context only.").optional(),
|
|
2412
|
+
format: tableFormatSchema.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).").default("gfm"),
|
|
2413
|
+
selectors: selectorsSchema,
|
|
2414
|
+
rowSelector: z.string().describe("CSS selector for repeating row containers. When set WITH cellSelector, selector mode is used (no auto-detection). Example: '[class*=\"estimate-row\"]'.").optional(),
|
|
2415
|
+
cellSelector: z.string().describe("CSS selector for cells within each row (scoped to the row subtree). Required together with rowSelector for selector mode. Example: '[class*=\"cell\"]'.").optional()
|
|
2416
|
+
};
|
|
2417
|
+
var extractGridInputSchema = z.object(extractGridInputShape).superRefine((value, ctx) => {
|
|
2418
|
+
if (value.rowSelector !== void 0 !== (value.cellSelector !== void 0)) ctx.addIssue({
|
|
2419
|
+
code: "custom",
|
|
2420
|
+
message: "Provide both `rowSelector` and `cellSelector` for selector mode, or neither for auto-detection (setting only one is invalid).",
|
|
2421
|
+
path: ["rowSelector"]
|
|
2422
|
+
});
|
|
2423
|
+
});
|
|
2424
|
+
//#endregion
|
|
2425
|
+
//#region src/tools/output-schema.ts
|
|
2426
|
+
var metadataObjectSchema = z.object({
|
|
2427
|
+
byline: z.string().optional().describe("Article author(s), resolved from JSON-LD, OpenGraph, <meta>, or Readability."),
|
|
2428
|
+
canonical: z.string().optional().describe("Declared canonical URL from <link rel=\"canonical\"> (or og:url as fallback). Distinct from baseUrl, which is the origin context passed in."),
|
|
2429
|
+
estimator: z.string().optional().describe("Name of the heuristic backing tokenEstimate (e.g. \"chars/4\")."),
|
|
2430
|
+
excerpt: z.string().optional().describe("Short article summary produced by Readability."),
|
|
2431
|
+
lang: z.string().optional().describe("Detected document language."),
|
|
2432
|
+
publishedTime: z.string().optional().describe("Publication timestamp resolved from JSON-LD, <meta>, or <time> elements."),
|
|
2433
|
+
readingTimeMin: z.number().int().optional().describe("Estimated reading time in minutes, derived from wordCount and wordsPerMinute."),
|
|
2434
|
+
siteName: z.string().optional().describe("Publishing site name, resolved from OpenGraph or <meta>."),
|
|
2435
|
+
structured: z.record(z.string(), z.unknown()).optional().describe("Parsed schema.org JSON-LD primary object (Recipe/Product/Event/HowTo/Article…) when present — the raw graph node with @context stripped and @type normalized to a \"+\"-joined string. Absent when the page has no recognizable structured data."),
|
|
2436
|
+
title: z.string().optional().describe("Article title, resolved by priority cascade (JSON-LD → OpenGraph → Twitter → <meta> → Readability → <title>)."),
|
|
2437
|
+
tokenEstimate: z.number().int().optional().describe("Rough output token count (chars/4 by default) for context budgeting."),
|
|
2438
|
+
baseUrl: z.string().optional().describe("The baseUrl passed in (origin context)."),
|
|
2439
|
+
wordCount: z.number().int().optional().describe("Number of whitespace-separated words in the extracted text.")
|
|
2440
|
+
}).describe("Resolved article metadata. Each field is the first non-empty value across a priority cascade.");
|
|
2441
|
+
var chunkObjectSchema = z.object({
|
|
2442
|
+
index: z.number().int().min(0).describe("Zero-based chunk position within the emitted sequence."),
|
|
2443
|
+
text: z.string().describe("The chunk body (markdown or text), trimmed, sized to stay within the requested token budget."),
|
|
2444
|
+
tokenCount: z.number().int().min(0).describe("Estimated token count of text (chars/4), same heuristic as metadata.tokenEstimate."),
|
|
2445
|
+
headingContext: z.string().describe("Nearest preceding markdown heading text in effect at the chunk’s first block. Empty string when the chunk precedes any heading; carried from the overlap source when a chunk begins with overlap text.")
|
|
2446
|
+
}).describe("One token-bounded slice of the extracted markdown, with its section heading for context.");
|
|
2447
|
+
var imageEntrySchema = z.object({
|
|
2448
|
+
src: z.string().describe("Absolute (resolved) image URL, absolutized against baseUrl."),
|
|
2449
|
+
alt: z.string().describe("The img alt attribute, or empty string when absent."),
|
|
2450
|
+
width: z.number().int().optional().describe("Pixel dimension from the attribute, when present."),
|
|
2451
|
+
height: z.number().int().optional().describe("Pixel dimension from the attribute, when present."),
|
|
2452
|
+
caption: z.string().describe("figcaption text from the enclosing <figure>, else alt.")
|
|
2453
|
+
}).describe("One extracted image with resolved source and caption.");
|
|
2454
|
+
var outputSchemaShape = {
|
|
2455
|
+
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
2456
|
+
content: z.string().describe("The human/LLM-readable payload — Markdown/html/text, or the serialized JSON when format=json."),
|
|
2457
|
+
chunks: z.array(chunkObjectSchema).optional().describe("Token-bounded chunks of the extracted markdown, populated by `extract` only when the `chunk` option is set and the format yields a markdown/text body. Absent for html_to_markdown and for html/json extract formats."),
|
|
2458
|
+
images: z.array(imageEntrySchema).optional().describe("Inventory of article images (absolute src, alt, dimensions, caption); populated only when imageInventory:true is passed to extract."),
|
|
2459
|
+
metadata: metadataObjectSchema,
|
|
2460
|
+
diagnostics: z.object({
|
|
2461
|
+
boilerplateRemoved: z.number().int().optional().describe("Count of boilerplate blocks (related-posts, newsletter signup, read-next) stripped before conversion."),
|
|
2462
|
+
cache: z.object({
|
|
2463
|
+
hit: z.boolean().describe("True when this call was served from the in-memory cache without re-running the pipeline."),
|
|
2464
|
+
normalizedHash: z.string().describe("sha256 of the volatility-normalized HTML (whitespace collapsed, scripts/nonce/CSP/generated ids stripped) — the actual cache key, shared across re-renders that differ only in volatile markup."),
|
|
2465
|
+
originalHash: z.string().describe("sha256 of the raw HTML as passed (trimmed). Differs from normalizedHash when volatile markup (nonce/CSP/generated ids) was collapsed; equal when the input was already stable. Used to diagnose should-have-hit-but-didn’t misses.")
|
|
2466
|
+
}).optional().describe("Cache signal — populated only by `extract` when called with cache:true. Absent otherwise (goldens, default path, and other tools never emit this field)."),
|
|
2467
|
+
chromeRemoved: z.number().int().optional().describe("Count of browser-chrome nodes stripped before conversion (scrollbars, consent banners, overlays)."),
|
|
2468
|
+
extractedNode: z.string().optional().describe("DOM root extraction came from: \"readability\" (main path), a fallback selector (e.g. \"article\", \"main\"), or \"fragment\" for html_to_markdown."),
|
|
2469
|
+
fallbackUsed: z.boolean().describe("True if Readability parse failed and a selector cascade salvaged content. Always true for html_to_markdown."),
|
|
2470
|
+
gated: z.object({
|
|
2471
|
+
likely: z.boolean().describe("True when heuristics strongly suggest the content is paywalled or truncated."),
|
|
2472
|
+
reason: z.string().describe("Short label naming the detected signal (e.g. \"paywall overlay\", \"metered paywall message\").")
|
|
2473
|
+
}).optional().describe("Likely paywall / gating signal. The extraction may be partial; the host can re-capture after authenticating. Detection only — this server never fetches or authenticates."),
|
|
2474
|
+
imagesResolved: z.number().int().optional().describe("Count of lazy/placeholder images resolved to their real src before conversion."),
|
|
2475
|
+
pagination: z.object({
|
|
2476
|
+
type: z.enum(["infinite", "paginated"]).describe("Kind of pagination signal detected in the document."),
|
|
2477
|
+
nextUrl: z.string().optional().describe("Absolute URL of the detected next page (paginated only). Mirrors the href found in the DOM; never fetched by this server."),
|
|
2478
|
+
selector: z.string().optional().describe("CSS selector of the detected load-more / infinite-scroll sentinel (infinite only).")
|
|
2479
|
+
}).optional().describe("Detected pagination or infinite-scroll signal. Detection only — the host drives loading; this server never fetches."),
|
|
2480
|
+
preset: z.object({
|
|
2481
|
+
applied: z.boolean().describe("True when the site preset’s scope was applied through the same selectors path as the `selectors` option. False when a preset existed but was not applied (see `reason`)."),
|
|
2482
|
+
reason: z.enum(["detectors-missed", "overridden"]).optional().describe("Why the preset was not applied: `overridden` — an explicit `selectors` argument beat the inferred one; `detectors-missed` — a layout fingerprint no longer matched the document (stale preset), so the normal cascade ran. Absent when `applied` is true."),
|
|
2483
|
+
site: z.string().describe("Site key the preset matched: baseUrl hostname, lowercased, leading \"www.\" stripped.")
|
|
2484
|
+
}).optional().describe("Site-preset signal — populated only by `extract`, and only when a preset exists for the document’s site; absent otherwise (other tools and preset-less sites never emit it)."),
|
|
2485
|
+
readerable: z.boolean().optional().describe("Readability isProbablyReaderable verdict on the document (extract main path only)."),
|
|
2486
|
+
removedNodes: z.number().int().optional().describe("Net element count removed across the whole pipeline (delta vs. the parsed document)."),
|
|
2487
|
+
sanitization: z.object({
|
|
2488
|
+
iframes: z.number().int().describe("<iframe> elements removed by sanitization."),
|
|
2489
|
+
scripts: z.number().int().describe("<script> and event-handler nodes removed by sanitization.")
|
|
2490
|
+
}).optional().describe("Counts of nodes removed by DOMPurify sanitization."),
|
|
2491
|
+
truncated: z.boolean().describe("True if the payload was truncated by maxChars."),
|
|
2492
|
+
trace: z.array(z.object({
|
|
2493
|
+
stage: z.string().describe("Pipeline stage name (e.g. \"normalize\", \"readability\", \"sanitize\", \"turndown\", \"metadata\")."),
|
|
2494
|
+
ms: z.number().describe("Wall-clock duration of the stage in milliseconds, measured via performance.now().")
|
|
2495
|
+
}).describe("One timed pipeline stage.")).optional().describe("Per-stage timings emitted only when debug:true is passed to extract/html_to_markdown. Stages are non-overlapping and ordered; absent otherwise.")
|
|
2496
|
+
}).describe("Pipeline telemetry describing what was extracted, sanitized, and removed.")
|
|
2497
|
+
};
|
|
2498
|
+
z.object(outputSchemaShape);
|
|
2499
|
+
var outlineOutputShape = {
|
|
2500
|
+
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
2501
|
+
content: z.string().describe("Indented-bullet table of contents, one line per heading, nested by depth."),
|
|
2502
|
+
outline: z.array(z.object({
|
|
2503
|
+
level: z.number().int().min(1).max(6).describe("Heading level (1–6)."),
|
|
2504
|
+
text: z.string().describe("Heading text content."),
|
|
2505
|
+
anchor: z.string().describe("Stable anchor id: the heading own id, a descendant permalink fragment, or a slug of the text (deduped -1, -2, … for generated slugs).")
|
|
2506
|
+
}).describe("A single document heading with its stable anchor.")).describe("Document headings (h1–h6) in document order, each with a stable anchor id."),
|
|
2507
|
+
metadata: z.object({
|
|
2508
|
+
title: z.string().optional().describe("Document title from <title>, falling back to the first <h1>."),
|
|
2509
|
+
baseUrl: z.string().optional().describe("The baseUrl passed in (origin context, never fetched).")
|
|
2510
|
+
}).describe("Outline document metadata.")
|
|
2511
|
+
};
|
|
2512
|
+
z.object(outlineOutputShape);
|
|
2513
|
+
var extractMetadataOutputShape = {
|
|
2514
|
+
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
2515
|
+
content: z.string().describe("Human-readable key:value rendering of the metadata block, so content[0].text is never empty."),
|
|
2516
|
+
metadata: metadataObjectSchema
|
|
2517
|
+
};
|
|
2518
|
+
z.object(extractMetadataOutputShape);
|
|
2519
|
+
var chunkTextOutputShape = {
|
|
2520
|
+
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
2521
|
+
content: z.string().describe("Readable index of the chunks (one numbered section per chunk, each prefixed with its heading context), so content[0].text is always scannable."),
|
|
2522
|
+
chunks: z.array(chunkObjectSchema).describe("The emitted chunks in order. Empty array when the input contains no non-whitespace content.")
|
|
2523
|
+
};
|
|
2524
|
+
z.object(chunkTextOutputShape);
|
|
2525
|
+
var linkObjectSchema = z.object({
|
|
2526
|
+
text: z.string().describe("Anchor text content, whitespace-collapsed and trimmed (capped at 300 chars)."),
|
|
2527
|
+
href: z.string().describe("Absolute href (resolved against baseUrl when provided); unchanged when baseUrl is absent or the pair fails to parse."),
|
|
2528
|
+
rel: z.string().describe("The raw rel attribute value (e.g. \"noopener noreferrer\", \"nofollow\"), or the empty string when absent."),
|
|
2529
|
+
isExternal: z.boolean().describe("True when baseUrl is provided and the href parses to a different origin than baseUrl. False for relative, fragment, same-origin, non-http(s) (mailto/tel/javascript), and malformed hrefs.")
|
|
2530
|
+
}).describe("A single anchor link with its text, absolute href, rel, and origin.");
|
|
2531
|
+
var extractLinksOutputShape = {
|
|
2532
|
+
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
2533
|
+
content: z.string().describe("Readable rendering of the link list (one `- [text](href)` line per link), so content[0].text is never empty."),
|
|
2534
|
+
links: z.array(linkObjectSchema).describe("Anchors in document order, hrefs absolutized against baseUrl. No deduplication."),
|
|
2535
|
+
metadata: z.object({ baseUrl: z.string().optional().describe("The baseUrl passed in (origin context, never fetched).") }).describe("Extract-links document metadata.")
|
|
2536
|
+
};
|
|
2537
|
+
z.object(extractLinksOutputShape);
|
|
2538
|
+
var tableEntrySchema = z.object({
|
|
2539
|
+
index: z.number().int().min(0).describe("0-based position among emitted tables."),
|
|
2540
|
+
rows: z.number().int().min(0).describe("Row count of the matrix (after rowspan/colspan resolution)."),
|
|
2541
|
+
cols: z.number().int().min(0).describe("Column count of the matrix (after colspan resolution)."),
|
|
2542
|
+
markdown: z.string().describe("The table rendered in the requested format (gfm/csv/json).")
|
|
2543
|
+
}).describe("One extracted table with its dimensions and rendered form.");
|
|
2544
|
+
var extractTablesOutputShape = {
|
|
2545
|
+
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
2546
|
+
content: z.string().describe("All tables rendered in the requested format, joined by blank lines; \"(no tables found)\" when none."),
|
|
2547
|
+
tables: z.array(tableEntrySchema).describe("Every <table> on the page (rowspan/colspan-resolved), in document order."),
|
|
2548
|
+
metadata: z.object({
|
|
2549
|
+
baseUrl: z.string().optional().describe("The baseUrl passed in (origin context)."),
|
|
2550
|
+
format: tableFormatSchema.describe("The requested render format."),
|
|
2551
|
+
tableCount: z.number().int().describe("Number of tables emitted.")
|
|
2552
|
+
}).describe("Tables-tool metadata.")
|
|
2553
|
+
};
|
|
2554
|
+
z.object(extractTablesOutputShape);
|
|
2555
|
+
var gridEntrySchema = z.object({
|
|
2556
|
+
rows: z.number().int().min(0).describe("Row count of the detected grid (after ragged-row padding)."),
|
|
2557
|
+
cols: z.number().int().min(0).describe("Column count of the detected grid (max cell width across rows)."),
|
|
2558
|
+
markdown: z.string().describe("The grid rendered in the requested format (gfm/csv/json).")
|
|
2559
|
+
}).describe("One detected grid with its dimensions and rendered form.");
|
|
2560
|
+
var extractGridOutputShape = {
|
|
2561
|
+
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
2562
|
+
content: z.string().describe("The grid rendered in the requested format, or \"(no repeating grid found)\" when no grid is detected."),
|
|
2563
|
+
grid: gridEntrySchema.describe("The detected grid (dimensions + rendered markdown). Rows/cols are 0 and markdown is empty when nothing is detected."),
|
|
2564
|
+
diagnostics: z.object({
|
|
2565
|
+
confidence: z.enum([
|
|
2566
|
+
"high",
|
|
2567
|
+
"low",
|
|
2568
|
+
"medium"
|
|
2569
|
+
]).describe("`high` when ≥6 detected data rows, `medium` when ≥3 (minRows), `low` otherwise. Counts the detected cluster only — a recovered header row is inference and does not raise it. `low` for non-grid pages."),
|
|
2570
|
+
containerSelector: z.string().describe("CSS-ish hint (tag#id.class) of the winning container, or the rowSelector in selector mode. Empty when not detected."),
|
|
2571
|
+
detected: z.boolean().describe("True when a repeating grid was found (≥3 same-shape sibling rows each with ≥2 direct element-children, outside nav/header/footer/aside)."),
|
|
2572
|
+
rowCount: z.number().int().describe("Number of rows emitted, including any recovered header row. 0 when not detected."),
|
|
2573
|
+
colCount: z.number().int().describe("Number of columns (max cell width across rows). 0 when not detected."),
|
|
2574
|
+
rowTag: z.string().describe("Uppercase DOM tag name of the repeating row container (e.g. \"DIV\", \"TR\", \"LI\"). Empty when not detected."),
|
|
2575
|
+
note: z.string().describe("Short human-readable status: the detection reason when detected, or the rejection reason when not.")
|
|
2576
|
+
}).describe("Grid-detection telemetry describing the winning candidate."),
|
|
2577
|
+
metadata: z.object({
|
|
2578
|
+
baseUrl: z.string().optional().describe("The baseUrl passed in (origin context, never fetched)."),
|
|
2579
|
+
format: tableFormatSchema.describe("The requested render format."),
|
|
2580
|
+
detected: z.boolean().describe("Mirrors diagnostics.detected.")
|
|
2581
|
+
}).describe("Extract-grid document metadata.")
|
|
2582
|
+
};
|
|
2583
|
+
z.object(extractGridOutputShape);
|
|
2584
|
+
var listItemSchema = z.object({
|
|
2585
|
+
score: z.number().int().describe("Item substance score: primary-anchor text length + non-link body text length. Long titles (real feed items) score higher than short nav labels; items with both a long title and surrounding body text (snippets, excerpts) score highest."),
|
|
2586
|
+
snippet: z.string().describe("Item body text with the title peeled off, whitespace-collapsed and clipped at 200 chars. Empty when the item is title-only."),
|
|
2587
|
+
title: z.string().describe("Primary anchor text (longest-text <a> in the item, whitespace-collapsed). Always non-empty for emitted items."),
|
|
2588
|
+
url: z.string().describe("Absolute href of the primary anchor, resolved against baseUrl. Always non-empty for emitted items.")
|
|
2589
|
+
}).describe("One detected list item with its title, URL, snippet, and score.");
|
|
2590
|
+
var extractListOutputShape = {
|
|
2591
|
+
schemaVersion: z.literal(1).describe("Structured-content schema version. Bumps only on breaking shape changes to this object."),
|
|
2592
|
+
content: z.string().describe("Readable rendering of the items (one numbered `title — url` line per item, each followed by an indented snippet), or a single `not a list: …` line when no list structure is detected."),
|
|
2593
|
+
items: z.array(listItemSchema).describe("Detected list items in document order. Empty when the page has no repeated same-shape sibling structure with anchors (e.g. article pages)."),
|
|
2594
|
+
diagnostics: z.object({
|
|
2595
|
+
confidence: z.enum([
|
|
2596
|
+
"high",
|
|
2597
|
+
"low",
|
|
2598
|
+
"medium"
|
|
2599
|
+
]).describe("`high` when ≥6 items and avg score ≥30, `medium` when ≥3 items, `low` otherwise. `low` for non-list pages."),
|
|
2600
|
+
containerSelector: z.string().describe("CSS-ish hint (tag#id.class) of the winning container. Empty when not detected."),
|
|
2601
|
+
detected: z.boolean().describe("True when a list/feed/index structure was found (≥3 same-shape siblings each with a navigation anchor, outside nav/header/footer/aside)."),
|
|
2602
|
+
itemCount: z.number().int().describe("Number of items emitted. 0 when not detected."),
|
|
2603
|
+
itemTag: z.string().describe("Uppercase DOM tag name of the winning sibling group (e.g. \"TR\", \"LI\", \"ARTICLE\", \"DIV\"). Empty when not detected."),
|
|
2604
|
+
note: z.string().describe("Short human-readable status: the detection reason when detected, or the rejection reason when not.")
|
|
2605
|
+
}).describe("List-detection telemetry describing the winning candidate."),
|
|
2606
|
+
metadata: z.object({ baseUrl: z.string().optional().describe("The baseUrl passed in (origin context, never fetched).") }).describe("Extract-list document metadata.")
|
|
2607
|
+
};
|
|
2608
|
+
z.object(extractListOutputShape);
|
|
2609
|
+
//#endregion
|
|
2500
2610
|
//#region src/tools/extract.ts
|
|
2501
2611
|
var EXTRACTED_NODE = "readability";
|
|
2502
2612
|
function extractArticle(rawArgs) {
|
|
@@ -2512,7 +2622,7 @@ function extractArticleFromHtml(input) {
|
|
|
2512
2622
|
...DEFAULTS,
|
|
2513
2623
|
...input
|
|
2514
2624
|
};
|
|
2515
|
-
const { html, baseUrl, cache: useCache, selectors, extraction, minArticleLength, maxNodes, keepClasses, readabilityOverrides, format, metadataMode, gfm, headingStyle, codeBlockStyle, images, sanitize: shouldSanitize, maxChars, wordsPerMinute, cleanChrome, tables, chunk, imageInventory, debug } = merged;
|
|
2625
|
+
const { html, baseUrl, cache: useCache, selectors, extraction, minArticleLength, maxNodes, keepClasses, readabilityOverrides, format, metadataMode, gfm, headingStyle, codeBlockStyle, images, sanitize: shouldSanitize, maxChars, wordsPerMinute, cleanChrome, tables, chunk, imageInventory, debug, resolvePreset: presetsEnabled = true } = merged;
|
|
2516
2626
|
if (useCache) {
|
|
2517
2627
|
const hit = lookup(html, merged);
|
|
2518
2628
|
if (hit) {
|
|
@@ -2539,13 +2649,14 @@ function extractArticleFromHtml(input) {
|
|
|
2539
2649
|
}
|
|
2540
2650
|
const trace = new TraceCollector(debug);
|
|
2541
2651
|
const { document, window } = buildDocument(html, baseUrl);
|
|
2542
|
-
const { gating, documentElementCount, normalizeCounts, imagesResolved, pagination } = trace.run("normalize", () => {
|
|
2652
|
+
const { gating, documentElementCount, normalizeCounts, imagesResolved, pagination, preset } = trace.run("normalize", () => {
|
|
2543
2653
|
const gating = detectGating(document);
|
|
2544
2654
|
const documentElementCount = document.querySelectorAll("*").length;
|
|
2545
2655
|
const normalizeCounts = normalizeDocument(document, { cleanChrome });
|
|
2546
2656
|
const imagesResolved = resolveLazyImages(document);
|
|
2547
2657
|
const pagination = detectPagination(document, baseUrl);
|
|
2548
|
-
|
|
2658
|
+
const resolution = presetsEnabled ? resolvePreset(document, baseUrl, selectors !== void 0) : void 0;
|
|
2659
|
+
applySelectors(document, resolution?.scope ?? selectors);
|
|
2549
2660
|
const codeBlocksCanonicalized = canonicalizeCodeBlocks(document);
|
|
2550
2661
|
if (codeBlocksCanonicalized > 0) logger.debug(`canonicalized ${codeBlocksCanonicalized} code-block language tag(s)`);
|
|
2551
2662
|
return {
|
|
@@ -2553,7 +2664,8 @@ function extractArticleFromHtml(input) {
|
|
|
2553
2664
|
gating,
|
|
2554
2665
|
imagesResolved,
|
|
2555
2666
|
normalizeCounts,
|
|
2556
|
-
pagination
|
|
2667
|
+
pagination,
|
|
2668
|
+
preset: resolution?.signal
|
|
2557
2669
|
};
|
|
2558
2670
|
});
|
|
2559
2671
|
const { readerable, article } = trace.run("readability", () => {
|
|
@@ -2652,6 +2764,7 @@ function extractArticleFromHtml(input) {
|
|
|
2652
2764
|
gated: gating,
|
|
2653
2765
|
imagesResolved,
|
|
2654
2766
|
pagination,
|
|
2767
|
+
preset,
|
|
2655
2768
|
readerable,
|
|
2656
2769
|
sanitization,
|
|
2657
2770
|
trace: trace.collect(),
|
|
@@ -2731,6 +2844,6 @@ function registerExtractTool(server) {
|
|
|
2731
2844
|
}, extractHandler);
|
|
2732
2845
|
}
|
|
2733
2846
|
//#endregion
|
|
2734
|
-
export {
|
|
2847
|
+
export { resolveLazyImages as $, selectorsSchema as A, absolutize as B, extractTablesInputSchema as C, localPathField as D, htmlToMarkdownInputShape as E, detectGating as F, sanitizeHtml as G, renderTable as H, TraceCollector as I, computeTextMetrics as J, isReaderable as K, assembleDiagnostics as L, truncateMarkdown as M, detectPagination as N, outlineInputSchema as O, resolveMetadata as P, normalizeDocument as Q, chunkMarkdown as R, extractSectionInputShape as S, htmlToMarkdownInputSchema as T, resolveHeaderKeys as U, parseTableMatrix as V, resolveCellText as W, nonEmpty as X, countWords as Y, applySelectors as Z, extractListInputSchema as _, extractLinksOutputShape as a, registerResources as at, extractMetadataInputShape as b, extractTablesOutputShape as c, presetForSite as ct, chunkTextInputSchema as d, logger as dt, resolveReadabilityOptions as et, chunkTextInputShape as f, loadConfig as ft, extractLinksInputShape as g, extractLinksInputSchema as h, extractGridOutputShape as i, toErrorResult as it, readHtmlFile as j, outlineInputShape as k, outlineOutputShape as l, removePreset as lt, extractGridInputShape as m, registerExtractTool as n, isElement as nt, extractListOutputShape as o, addPreset as ot, extractGridInputSchema as p, formatPayload as q, chunkTextOutputShape as r, ExtractionError as rt, extractMetadataOutputShape as s, normalizeSiteKey as st, extractArticleFromHtml as t, buildDocument as tt, outputSchemaShape as u, selectorMisses as ut, extractListInputShape as v, extractTablesInputShape as w, extractSectionInputSchema as x, extractMetadataInputSchema as y, toMarkdown as z };
|
|
2735
2848
|
|
|
2736
|
-
//# sourceMappingURL=extract-
|
|
2849
|
+
//# sourceMappingURL=extract-BF4w5w8k.js.map
|