@v1nvn/readability 0.45.0 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +4 -4
- package/src/cli.js +99 -0
- package/src/config.js +29 -0
- package/src/dev.js +55 -0
- package/src/errors.js +16 -0
- package/src/handler.js +4 -0
- package/src/host-sampling.js +57 -0
- package/src/index.js +23 -0
- package/src/index.ts +0 -0
- package/src/logger.js +2 -0
- package/src/output/format.js +92 -0
- package/src/pipeline/cell-text.js +51 -0
- package/src/pipeline/context.js +1 -0
- package/src/pipeline/dom.js +16 -0
- package/src/pipeline/footnotes.js +148 -0
- package/src/pipeline/math.js +113 -0
- package/src/pipeline/normalize.js +507 -0
- package/src/pipeline/readability.js +10 -0
- package/src/pipeline/resolver.js +113 -0
- package/src/pipeline/sanitize.js +26 -0
- package/src/pipeline/tables.js +239 -0
- package/src/pipeline/turndown.js +153 -0
- package/src/pipeline/urls.js +14 -0
- package/src/policy/chunk.js +365 -0
- package/src/policy/diagnostics.js +53 -0
- package/src/policy/explain.js +104 -0
- package/src/policy/fallback.js +68 -0
- package/src/policy/gating.js +90 -0
- package/src/policy/grid-detector.js +207 -0
- package/src/policy/identifiers.js +62 -0
- package/src/policy/images.js +49 -0
- package/src/policy/list-detector.js +232 -0
- package/src/policy/lost-signal.js +44 -0
- package/src/policy/markdown.js +58 -0
- package/src/policy/metadata.js +216 -0
- package/src/policy/outline-chains.js +165 -0
- package/src/policy/outline.js +71 -0
- package/src/policy/pagination.js +79 -0
- package/src/policy/presets.js +100 -0
- package/src/policy/section.js +111 -0
- package/src/policy/selector-lint.js +143 -0
- package/src/policy/sibling-scan.js +92 -0
- package/src/policy/text.js +18 -0
- package/src/policy/truncate.js +45 -0
- package/src/preset-cache.js +165 -0
- package/src/resources.js +227 -0
- package/src/sampling.js +45 -0
- package/src/server.js +73 -0
- package/src/tools/chunk_text.js +40 -0
- package/src/tools/explain.js +200 -0
- package/src/tools/extract.js +263 -0
- package/src/tools/extract_grid.js +56 -0
- package/src/tools/extract_links.js +98 -0
- package/src/tools/extract_list.js +66 -0
- package/src/tools/extract_metadata.js +67 -0
- package/src/tools/extract_section.js +56 -0
- package/src/tools/extract_tables.js +59 -0
- package/src/tools/html-source.js +30 -0
- package/src/tools/html_to_markdown.js +113 -0
- package/src/tools/outline.js +47 -0
- package/src/tools/output-schema.js +504 -0
- package/src/tools/schemas.js +288 -0
- package/src/tools/suggest-preset.js +504 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
export function readHtmlFile(localPath) {
|
|
3
|
+
const raw = readFileSync(localPath, 'utf8');
|
|
4
|
+
const trimmed = raw.trim();
|
|
5
|
+
// A top-level JSON string scalar is a chrome-devtools capture (its JSON-encoded
|
|
6
|
+
// return value written to disk), not raw markup — unwrap it before parsing.
|
|
7
|
+
if (trimmed.length < 2 || trimmed.at(0) !== '"' || trimmed.at(-1) !== '"') {
|
|
8
|
+
return raw;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(trimmed);
|
|
12
|
+
return typeof parsed === 'string' ? parsed : raw;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return raw;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function fromLocalPath(input) {
|
|
19
|
+
const { localPath, ...rest } = input;
|
|
20
|
+
return { ...rest, html: readHtmlFile(localPath) };
|
|
21
|
+
}
|
|
22
|
+
const schemaDefaults = new WeakMap();
|
|
23
|
+
export function withSchemaDefaults(schema, input) {
|
|
24
|
+
let defaults = schemaDefaults.get(schema);
|
|
25
|
+
if (defaults === undefined) {
|
|
26
|
+
defaults = schema.parse({ localPath: '' });
|
|
27
|
+
schemaDefaults.set(schema, defaults);
|
|
28
|
+
}
|
|
29
|
+
return { ...defaults, ...input };
|
|
30
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { tool } from "../handler.js";
|
|
2
|
+
import { formatPayload } from "../output/format.js";
|
|
3
|
+
import { buildDocument } from "../pipeline/dom.js";
|
|
4
|
+
import { applySelectors, normalizeDocument, resolveLazyImages, } from "../pipeline/normalize.js";
|
|
5
|
+
import { sanitizeHtml } from "../pipeline/sanitize.js";
|
|
6
|
+
import { toMarkdown } from "../pipeline/turndown.js";
|
|
7
|
+
import { assembleDiagnostics, TraceCollector } from "../policy/diagnostics.js";
|
|
8
|
+
import { computeTextMetrics, nonEmpty } from "../policy/text.js";
|
|
9
|
+
import { truncateMarkdown } from "../policy/truncate.js";
|
|
10
|
+
import { fromLocalPath, withSchemaDefaults } from "./html-source.js";
|
|
11
|
+
import { outputSchemaShape } from "./output-schema.js";
|
|
12
|
+
import { htmlToMarkdownInputSchema, htmlToMarkdownInputShape, } from "./schemas.js";
|
|
13
|
+
const EXTRACTED_NODE = 'fragment';
|
|
14
|
+
export function htmlToMarkdownFromHtml(input) {
|
|
15
|
+
const { html, baseUrl, selectors, format, metadataMode, gfm, headingStyle, codeBlockStyle, images, sanitize: shouldSanitize, maxChars, wordsPerMinute, cleanChrome, tables, debug, } = withSchemaDefaults(htmlToMarkdownInputSchema, input);
|
|
16
|
+
const trace = new TraceCollector(debug);
|
|
17
|
+
const { document, window } = buildDocument(html, baseUrl);
|
|
18
|
+
const { documentElementCount, normalizeCounts, imagesResolved, textContent, rawHtml, } = trace.run('normalize', () => {
|
|
19
|
+
const documentElementCount = document.querySelectorAll('*').length;
|
|
20
|
+
const normalizeCounts = normalizeDocument(document, { cleanChrome });
|
|
21
|
+
const imagesResolved = resolveLazyImages(document);
|
|
22
|
+
applySelectors(document, selectors);
|
|
23
|
+
const body = document.body;
|
|
24
|
+
return {
|
|
25
|
+
documentElementCount,
|
|
26
|
+
imagesResolved,
|
|
27
|
+
normalizeCounts,
|
|
28
|
+
rawHtml: body.innerHTML,
|
|
29
|
+
textContent: body.textContent,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
const { html: sanitizedHtml, counts: sanitizeCounts } = trace.run('sanitize', () => {
|
|
33
|
+
if (!shouldSanitize) {
|
|
34
|
+
return { counts: { iframes: 0, scripts: 0 }, html: rawHtml };
|
|
35
|
+
}
|
|
36
|
+
const res = sanitizeHtml(rawHtml, window);
|
|
37
|
+
return {
|
|
38
|
+
counts: { iframes: res.iframesRemoved, scripts: res.scriptsRemoved },
|
|
39
|
+
html: res.html,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
const markdown = trace.run('turndown', () => toMarkdown(sanitizedHtml, {
|
|
43
|
+
codeBlockStyle,
|
|
44
|
+
gfm,
|
|
45
|
+
headingStyle,
|
|
46
|
+
images,
|
|
47
|
+
tables,
|
|
48
|
+
baseUrl,
|
|
49
|
+
}));
|
|
50
|
+
const { metadata } = trace.run('metadata', () => {
|
|
51
|
+
const firstHeading = nonEmpty(document.body.querySelector('h1, h2, h3, h4, h5, h6')?.textContent);
|
|
52
|
+
const metadata = {
|
|
53
|
+
title: firstHeading,
|
|
54
|
+
baseUrl,
|
|
55
|
+
...computeTextMetrics(textContent, wordsPerMinute),
|
|
56
|
+
};
|
|
57
|
+
return { metadata };
|
|
58
|
+
});
|
|
59
|
+
const sanitization = {
|
|
60
|
+
iframes: normalizeCounts.iframes + sanitizeCounts.iframes,
|
|
61
|
+
scripts: normalizeCounts.scripts + sanitizeCounts.scripts,
|
|
62
|
+
};
|
|
63
|
+
const baseDiagnostics = assembleDiagnostics({
|
|
64
|
+
articleHtml: sanitizedHtml,
|
|
65
|
+
boilerplateRemoved: normalizeCounts.boilerplateRemoved,
|
|
66
|
+
chromeRemoved: normalizeCounts.chromeRemoved,
|
|
67
|
+
documentElementCount,
|
|
68
|
+
extractedNode: EXTRACTED_NODE,
|
|
69
|
+
fallbackUsed: true,
|
|
70
|
+
imagesResolved,
|
|
71
|
+
sanitization,
|
|
72
|
+
trace: trace.collect(),
|
|
73
|
+
truncated: false,
|
|
74
|
+
window,
|
|
75
|
+
});
|
|
76
|
+
let payload = formatPayload({
|
|
77
|
+
diagnostics: baseDiagnostics,
|
|
78
|
+
format,
|
|
79
|
+
markdown,
|
|
80
|
+
metadata,
|
|
81
|
+
metadataMode,
|
|
82
|
+
sanitizedHtml,
|
|
83
|
+
textContent,
|
|
84
|
+
});
|
|
85
|
+
let truncated = false;
|
|
86
|
+
if (maxChars !== undefined && (format === 'markdown' || format === 'text')) {
|
|
87
|
+
const res = truncateMarkdown(payload, maxChars);
|
|
88
|
+
payload = res.text;
|
|
89
|
+
truncated = res.truncated;
|
|
90
|
+
}
|
|
91
|
+
const diagnostics = truncated
|
|
92
|
+
? { ...baseDiagnostics, truncated }
|
|
93
|
+
: baseDiagnostics;
|
|
94
|
+
return {
|
|
95
|
+
content: [{ text: payload, type: 'text' }],
|
|
96
|
+
structuredContent: {
|
|
97
|
+
schemaVersion: 1,
|
|
98
|
+
content: payload,
|
|
99
|
+
metadata,
|
|
100
|
+
diagnostics,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export const HTML_TO_MARKDOWN_TOOL_DESCRIPTION = `Convert an arbitrary HTML fragment to Markdown WITHOUT Readability article extraction (e.g. a snippet already isolated via chrome-devtools). Same Turndown + DOMPurify path as \`extract\`. The server fetches nothing: \`localPath\` is the only source, and \`baseUrl\` (optional) absolutizes relative links.`;
|
|
105
|
+
export const htmlToMarkdownHandler = tool('html_to_markdown', input => htmlToMarkdownFromHtml(fromLocalPath(input)), htmlToMarkdownInputSchema);
|
|
106
|
+
export function registerHtmlToMarkdownTool(server) {
|
|
107
|
+
return server.registerTool('html_to_markdown', {
|
|
108
|
+
title: 'Convert HTML fragment to Markdown',
|
|
109
|
+
description: HTML_TO_MARKDOWN_TOOL_DESCRIPTION,
|
|
110
|
+
inputSchema: htmlToMarkdownInputShape,
|
|
111
|
+
outputSchema: outputSchemaShape,
|
|
112
|
+
}, htmlToMarkdownHandler);
|
|
113
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { tool } from "../handler.js";
|
|
2
|
+
import { buildDocument } from "../pipeline/dom.js";
|
|
3
|
+
import { applySelectors, normalizeDocument } from "../pipeline/normalize.js";
|
|
4
|
+
import { resolveOutline } from "../policy/outline.js";
|
|
5
|
+
import { fromLocalPath } from "./html-source.js";
|
|
6
|
+
import { outlineOutputShape } from "./output-schema.js";
|
|
7
|
+
import { outlineInputSchema, outlineInputShape, } from "./schemas.js";
|
|
8
|
+
// One line per heading, nested by depth; never blank so content[0].text is always scannable.
|
|
9
|
+
function renderOutlineToc(outline) {
|
|
10
|
+
if (outline.length === 0) {
|
|
11
|
+
return '(no headings found)';
|
|
12
|
+
}
|
|
13
|
+
return outline
|
|
14
|
+
.map(entry => `${' '.repeat(entry.level - 1)}- ${entry.text}`)
|
|
15
|
+
.join('\n');
|
|
16
|
+
}
|
|
17
|
+
export function outlineDocumentFromHtml(input) {
|
|
18
|
+
const { html, baseUrl, selectors } = input;
|
|
19
|
+
const { document } = buildDocument(html, baseUrl);
|
|
20
|
+
normalizeDocument(document);
|
|
21
|
+
applySelectors(document, selectors);
|
|
22
|
+
const outline = resolveOutline(document);
|
|
23
|
+
const title = document.title.trim() ||
|
|
24
|
+
document.querySelector('h1')?.textContent.replace(/\s+/g, ' ').trim() ||
|
|
25
|
+
undefined;
|
|
26
|
+
const metadata = { title, baseUrl };
|
|
27
|
+
const content = renderOutlineToc(outline);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ text: content, type: 'text' }],
|
|
30
|
+
structuredContent: {
|
|
31
|
+
schemaVersion: 1,
|
|
32
|
+
content,
|
|
33
|
+
outline,
|
|
34
|
+
metadata,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export const OUTLINE_TOOL_DESCRIPTION = `Return the document outline (h1-h6 headings with stable anchor ids) of already-rendered (post-JavaScript) HTML as a cheap pre-check before full extraction. No Readability scoring, no Turndown, no sanitization — a pure heading walk. The server fetches nothing: \`localPath\` is the only source, and \`baseUrl\` is origin context only (never fetched).`;
|
|
39
|
+
export const outlineHandler = tool('outline', input => outlineDocumentFromHtml(fromLocalPath(input)), outlineInputSchema);
|
|
40
|
+
export function registerOutlineTool(server) {
|
|
41
|
+
return server.registerTool('outline', {
|
|
42
|
+
title: 'Get document outline (heading TOC)',
|
|
43
|
+
description: OUTLINE_TOOL_DESCRIPTION,
|
|
44
|
+
inputSchema: outlineInputShape,
|
|
45
|
+
outputSchema: outlineOutputShape,
|
|
46
|
+
}, outlineHandler);
|
|
47
|
+
}
|