webforai 2.1.0 → 3.0.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 +63 -1
- package/dist/adapters/index.js +58 -0
- package/dist/adapters/registry.js +149 -0
- package/dist/adapters/sites/declarative.js +178 -0
- package/dist/adapters/sites/hackernews.js +94 -0
- package/dist/adapters/sites/youtube.js +165 -0
- package/dist/adapters/types.js +0 -0
- package/dist/bin.js +3199 -399
- package/dist/cjs/adapters/index.js +86 -0
- package/dist/cjs/adapters/registry.js +178 -0
- package/dist/cjs/adapters/sites/declarative.js +202 -0
- package/dist/cjs/adapters/sites/hackernews.js +117 -0
- package/dist/cjs/adapters/sites/youtube.js +191 -0
- package/dist/cjs/adapters/types.js +16 -0
- package/dist/cjs/constants.js +2 -2
- package/dist/cjs/extractors/index.js +8 -0
- package/dist/cjs/extractors/lib/constants.js +186 -0
- package/dist/cjs/extractors/lib/sanitize.js +425 -0
- package/dist/cjs/extractors/lib/score.js +264 -0
- package/dist/cjs/extractors/lib/terminators.js +131 -0
- package/dist/cjs/extractors/lib/ui-chrome.js +119 -0
- package/dist/cjs/extractors/pipeExtractors.js +12 -9
- package/dist/cjs/extractors/presets/auto.js +44 -0
- package/dist/cjs/extractors/presets/minimal-filter.js +27 -48
- package/dist/cjs/extractors/presets/takumi.js +84 -122
- package/dist/cjs/html-to-markdown.js +44 -7
- package/dist/cjs/html-to-mdast.js +9 -1
- package/dist/cjs/index.js +33 -2
- package/dist/cjs/mdast-handlers/custom-a-handler.js +25 -9
- package/dist/cjs/mdast-handlers/custom-img-handler.js +23 -1
- package/dist/cjs/mdast-handlers/custom-table-handler.js +16 -0
- package/dist/cjs/mdast-handlers/definition-list-handler.js +99 -0
- package/dist/cjs/mdast-handlers/math-handler.js +12 -3
- package/dist/cjs/mdast-handlers/sup-sub-handler.js +39 -0
- package/dist/cjs/metadata/index.js +223 -0
- package/dist/cjs/normalize/index.js +168 -0
- package/dist/cjs/platform/client.js +147 -0
- package/dist/cjs/platform/error.js +36 -0
- package/dist/cjs/platform/fetch.js +37 -0
- package/dist/cjs/platform/index.js +40 -0
- package/dist/cjs/platform/types.js +34 -0
- package/dist/cjs/utils/hast-fast.js +284 -0
- package/dist/constants.js +2 -2
- package/dist/extractors/index.js +5 -0
- package/dist/extractors/lib/constants.js +150 -0
- package/dist/extractors/lib/sanitize.js +415 -0
- package/dist/extractors/lib/score.js +242 -0
- package/dist/extractors/lib/terminators.js +107 -0
- package/dist/extractors/lib/ui-chrome.js +96 -0
- package/dist/extractors/pipeExtractors.js +12 -9
- package/dist/extractors/presets/auto.js +20 -0
- package/dist/extractors/presets/minimal-filter.js +35 -47
- package/dist/extractors/presets/takumi.js +91 -122
- package/dist/html-to-markdown.js +42 -6
- package/dist/html-to-mdast.js +9 -1
- package/dist/index.js +34 -3
- package/dist/mdast-handlers/custom-a-handler.js +25 -9
- package/dist/mdast-handlers/custom-img-handler.js +23 -1
- package/dist/mdast-handlers/custom-table-handler.js +16 -0
- package/dist/mdast-handlers/definition-list-handler.js +76 -0
- package/dist/mdast-handlers/math-handler.js +12 -3
- package/dist/mdast-handlers/sup-sub-handler.js +16 -0
- package/dist/metadata/index.js +198 -0
- package/dist/normalize/index.js +149 -0
- package/dist/platform/client.js +125 -0
- package/dist/platform/error.js +13 -0
- package/dist/platform/fetch.js +14 -0
- package/dist/platform/index.js +19 -0
- package/dist/platform/types.js +9 -0
- package/dist/types/adapters/index.d.ts +24 -0
- package/dist/types/adapters/index.js.map +1 -0
- package/dist/types/adapters/registry.d.ts +35 -0
- package/dist/types/adapters/registry.js.map +1 -0
- package/dist/types/adapters/sites/declarative.d.ts +13 -0
- package/dist/types/adapters/sites/declarative.js.map +1 -0
- package/dist/types/adapters/sites/hackernews.d.ts +13 -0
- package/dist/types/adapters/sites/hackernews.js.map +1 -0
- package/dist/types/adapters/sites/youtube.d.ts +31 -0
- package/dist/types/adapters/sites/youtube.js.map +1 -0
- package/dist/types/adapters/types.d.ts +99 -0
- package/dist/types/adapters/types.js.map +1 -0
- package/dist/types/cli/bin.js.map +1 -1
- package/dist/types/cli/commands/skill/content.d.ts +9 -0
- package/dist/types/cli/commands/skill/content.js.map +1 -0
- package/dist/types/cli/commands/skill/index.d.ts +19 -0
- package/dist/types/cli/commands/skill/index.js.map +1 -0
- package/dist/types/cli/commands/webforai/interactive.d.ts +7 -0
- package/dist/types/cli/commands/webforai/interactive.js.map +1 -0
- package/dist/types/cli/commands/webforai/loadHtml.js.map +1 -1
- package/dist/types/cli/commands/webforai/options.d.ts +55 -0
- package/dist/types/cli/commands/webforai/options.js.map +1 -0
- package/dist/types/cli/commands/webforai/run.d.ts +10 -0
- package/dist/types/cli/commands/webforai/run.js.map +1 -0
- package/dist/types/cli/constants.d.ts +14 -4
- package/dist/types/cli/constants.js.map +1 -1
- package/dist/types/cli/helpers/selectExtractMode.js.map +1 -1
- package/dist/types/cli/helpers/selectLoader.js.map +1 -1
- package/dist/types/constants.d.ts +7 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/types/extractors/index.d.ts +2 -0
- package/dist/types/extractors/index.js.map +1 -1
- package/dist/types/extractors/lib/constants.d.ts +81 -0
- package/dist/types/extractors/lib/constants.js.map +1 -0
- package/dist/types/extractors/lib/sanitize.d.ts +66 -0
- package/dist/types/extractors/lib/sanitize.js.map +1 -0
- package/dist/types/extractors/lib/score.d.ts +85 -0
- package/dist/types/extractors/lib/score.js.map +1 -0
- package/dist/types/extractors/lib/terminators.d.ts +26 -0
- package/dist/types/extractors/lib/terminators.js.map +1 -0
- package/dist/types/extractors/lib/ui-chrome.d.ts +18 -0
- package/dist/types/extractors/lib/ui-chrome.js.map +1 -0
- package/dist/types/extractors/pipeExtractors.d.ts +5 -0
- package/dist/types/extractors/pipeExtractors.js.map +1 -1
- package/dist/types/extractors/presets/auto.d.ts +30 -0
- package/dist/types/extractors/presets/auto.js.map +1 -0
- package/dist/types/extractors/presets/minimal-filter.d.ts +10 -2
- package/dist/types/extractors/presets/minimal-filter.js.map +1 -1
- package/dist/types/extractors/presets/takumi.d.ts +19 -3
- package/dist/types/extractors/presets/takumi.js.map +1 -1
- package/dist/types/extractors/types.d.ts +8 -0
- package/dist/types/html-to-markdown.d.ts +38 -0
- package/dist/types/html-to-markdown.js.map +1 -1
- package/dist/types/html-to-mdast.d.ts +11 -0
- package/dist/types/html-to-mdast.js.map +1 -1
- package/dist/types/index.d.ts +6 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/mdast-handlers/custom-a-handler.js.map +1 -1
- package/dist/types/mdast-handlers/custom-img-handler.js.map +1 -1
- package/dist/types/mdast-handlers/custom-table-handler.js.map +1 -1
- package/dist/types/mdast-handlers/definition-list-handler.d.ts +13 -0
- package/dist/types/mdast-handlers/definition-list-handler.js.map +1 -0
- package/dist/types/mdast-handlers/math-handler.js.map +1 -1
- package/dist/types/mdast-handlers/sup-sub-handler.d.ts +10 -0
- package/dist/types/mdast-handlers/sup-sub-handler.js.map +1 -0
- package/dist/types/metadata/index.d.ts +47 -0
- package/dist/types/metadata/index.js.map +1 -0
- package/dist/types/normalize/index.d.ts +53 -0
- package/dist/types/normalize/index.js.map +1 -0
- package/dist/types/platform/client.d.ts +54 -0
- package/dist/types/platform/client.js.map +1 -0
- package/dist/types/platform/error.d.ts +20 -0
- package/dist/types/platform/error.js.map +1 -0
- package/dist/types/platform/fetch.d.ts +34 -0
- package/dist/types/platform/fetch.js.map +1 -0
- package/dist/types/platform/index.d.ts +4 -0
- package/dist/types/platform/index.js.map +1 -0
- package/dist/types/platform/types.d.ts +135 -0
- package/dist/types/platform/types.js.map +1 -0
- package/dist/types/utils/hast-fast.d.ts +101 -0
- package/dist/types/utils/hast-fast.js.map +1 -0
- package/dist/utils/hast-fast.js +246 -0
- package/package.json +10 -3
- package/dist/types/cli/commands/webforai/index.d.ts +0 -6
- package/dist/types/cli/commands/webforai/index.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/bin.ts
|
|
4
|
-
import { program } from "commander";
|
|
4
|
+
import { CommanderError, program } from "commander";
|
|
5
5
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "webforai",
|
|
9
|
-
version: "
|
|
9
|
+
version: "3.0.0",
|
|
10
10
|
description: "A library that provides a web interface for AI",
|
|
11
11
|
author: "inaridiy",
|
|
12
12
|
license: "Apache-2.0",
|
|
@@ -52,6 +52,11 @@ var package_default = {
|
|
|
52
52
|
import: "./dist/index.js",
|
|
53
53
|
require: "./dist/cjs/index.js"
|
|
54
54
|
},
|
|
55
|
+
"./platform": {
|
|
56
|
+
types: "./dist/types/platform/index.d.ts",
|
|
57
|
+
import: "./dist/platform/index.js",
|
|
58
|
+
require: "./dist/cjs/platform/index.js"
|
|
59
|
+
},
|
|
55
60
|
"./loaders/playwright": {
|
|
56
61
|
types: "./dist/types/loaders/playwright.d.ts",
|
|
57
62
|
import: "./dist/loaders/playwright.js",
|
|
@@ -89,6 +94,9 @@ var package_default = {
|
|
|
89
94
|
],
|
|
90
95
|
"loaders/puppeteer": [
|
|
91
96
|
"./dist/types/loaders/puppeteer.d.ts"
|
|
97
|
+
],
|
|
98
|
+
platform: [
|
|
99
|
+
"./dist/types/platform/index.d.ts"
|
|
92
100
|
]
|
|
93
101
|
}
|
|
94
102
|
},
|
|
@@ -124,8 +132,7 @@ var package_default = {
|
|
|
124
132
|
"mdast-util-to-markdown": "^2.1.0",
|
|
125
133
|
picocolors: "^1.0.1",
|
|
126
134
|
"trim-trailing-lines": "^2.1.0",
|
|
127
|
-
"unist-util-filter": "^5.0.1"
|
|
128
|
-
zx: "^8.1.5"
|
|
135
|
+
"unist-util-filter": "^5.0.1"
|
|
129
136
|
},
|
|
130
137
|
devDependencies: {
|
|
131
138
|
"@cloudflare/puppeteer": "^0.0.6",
|
|
@@ -147,12 +154,520 @@ var package_default = {
|
|
|
147
154
|
}
|
|
148
155
|
};
|
|
149
156
|
|
|
150
|
-
// src/cli/commands/
|
|
151
|
-
import
|
|
152
|
-
import
|
|
153
|
-
import
|
|
157
|
+
// src/cli/commands/skill/index.ts
|
|
158
|
+
import { spawn } from "node:child_process";
|
|
159
|
+
import fs from "node:fs/promises";
|
|
160
|
+
import os from "node:os";
|
|
161
|
+
import path from "node:path";
|
|
162
|
+
|
|
163
|
+
// src/cli/commands/skill/content.ts
|
|
164
|
+
var renderSkillMarkdown = (version) => `---
|
|
165
|
+
name: webforai
|
|
166
|
+
description: Convert web pages or local HTML files to clean, LLM-ready Markdown with the webforai CLI. Use when a task needs page content as Markdown - reading documentation or articles, scraping, summarising URLs, building text corpora - including JavaScript-heavy or bot-protected pages via the hosted platform.
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
# webforai CLI${version ? ` (v${version})` : ""}
|
|
170
|
+
|
|
171
|
+
Convert a URL or local HTML file to Markdown. Non-interactive by default: the Markdown goes
|
|
172
|
+
to stdout, every log goes to stderr, so output can be piped or captured directly.
|
|
173
|
+
|
|
174
|
+
## Basic usage
|
|
175
|
+
|
|
176
|
+
\`\`\`bash
|
|
177
|
+
npx webforai https://example.com/article # Markdown to stdout
|
|
178
|
+
npx webforai https://example.com/article -o out.md # write to a file instead
|
|
179
|
+
npx webforai ./page.html # convert a local HTML file
|
|
180
|
+
npx webforai https://example.com --json # machine-readable envelope (see below)
|
|
181
|
+
\`\`\`
|
|
182
|
+
|
|
183
|
+
## Flags
|
|
184
|
+
|
|
185
|
+
- \`-o, --output <path>\` \u2014 write Markdown to a file instead of stdout.
|
|
186
|
+
- \`-l, --loader <fetch|playwright|platform>\` \u2014 how HTML is acquired. Default: \`fetch\` for
|
|
187
|
+
URLs, automatic \`local\` for file paths. \`playwright\` renders JavaScript locally (needs
|
|
188
|
+
\`npx playwright install chromium\`). \`platform\` uses the hosted API (below).
|
|
189
|
+
- \`-m, --mode <default|ai>\` \u2014 \`ai\` strips links/tables/images down to plain text
|
|
190
|
+
(local conversion only).
|
|
191
|
+
- \`--extractor <auto|takumi|minimal|none>\` \u2014 main-content extraction preset
|
|
192
|
+
(\`none\` converts the whole page).
|
|
193
|
+
- \`--frontmatter\` \u2014 prepend YAML front matter (title, author, canonical URL, ...).
|
|
194
|
+
- \`--json\` \u2014 print a JSON envelope to stdout instead of raw Markdown.
|
|
195
|
+
- \`--engine <fetch|browser|proxy-fetch|proxy-browser>\` \u2014 platform acquisition engine
|
|
196
|
+
(implies \`-l platform\`). \`browser\`/\`proxy-browser\` render JavaScript; \`proxy-*\` engines
|
|
197
|
+
egress through rotating proxies for bot-protected sites.
|
|
198
|
+
- \`--region <auto|us|eu|uk|jp|asia>\` \u2014 platform proxy egress region (implies \`-l platform\`).
|
|
199
|
+
- \`--screenshot\` \u2014 platform browser engines only; the envelope carries an expiring
|
|
200
|
+
\`screenshotUrl\`.
|
|
201
|
+
- \`--api-key <key>\` / \`--platform-url <url>\` \u2014 platform credentials and host; usually set
|
|
202
|
+
via env vars instead.
|
|
203
|
+
- \`-i, --interactive\` \u2014 guided prompt flow (for humans; do not use from an agent).
|
|
204
|
+
- \`-d, --debug\` \u2014 verbose logs on stderr.
|
|
205
|
+
|
|
206
|
+
## JSON envelope (\`--json\`)
|
|
207
|
+
|
|
208
|
+
\`\`\`json
|
|
209
|
+
{
|
|
210
|
+
"source": "https://example.com",
|
|
211
|
+
"loader": "fetch",
|
|
212
|
+
"url": "https://example.com",
|
|
213
|
+
"markdown": "# ...",
|
|
214
|
+
"metadata": { "title": "..." },
|
|
215
|
+
"engine": "browser",
|
|
216
|
+
"region": "auto",
|
|
217
|
+
"credits": 5,
|
|
218
|
+
"screenshotUrl": "https://...",
|
|
219
|
+
"output": "out.md"
|
|
220
|
+
}
|
|
221
|
+
\`\`\`
|
|
222
|
+
|
|
223
|
+
\`engine\`/\`region\`/\`credits\`/\`screenshotUrl\` appear only for the platform loader; \`output\`
|
|
224
|
+
only when \`-o\` also wrote a file.
|
|
225
|
+
|
|
226
|
+
## Hosted platform
|
|
227
|
+
|
|
228
|
+
For JavaScript-heavy or bot-protected pages, the hosted platform fetches server-side:
|
|
229
|
+
|
|
230
|
+
\`\`\`bash
|
|
231
|
+
export WEBFORAI_API_KEY=wfa_... # from https://platform.webforai.dev/dashboard
|
|
232
|
+
npx webforai https://example.com --engine browser --json
|
|
233
|
+
\`\`\`
|
|
234
|
+
|
|
235
|
+
\`WEBFORAI_PLATFORM_URL\` overrides the host for self-hosted deployments. Requests are metered
|
|
236
|
+
in credits (fetch 1, proxy-fetch 2, browser/proxy-browser 5); failures are never billed.
|
|
237
|
+
|
|
238
|
+
## Exit codes
|
|
239
|
+
|
|
240
|
+
- \`0\` \u2014 success.
|
|
241
|
+
- \`1\` \u2014 runtime failure (network, conversion, platform error; message on stderr).
|
|
242
|
+
- \`2\` \u2014 usage error (unknown flag or invalid value; message on stderr).
|
|
243
|
+
|
|
244
|
+
## Errors worth handling
|
|
245
|
+
|
|
246
|
+
- \`payment_required\` on stderr \u2192 platform credits exhausted.
|
|
247
|
+
- \`rate_limited\` \u2192 back off (a \`retryAfter\` seconds hint is included when known).
|
|
248
|
+
- Playwright loader errors explain the exact \`npx playwright install\` command to run.
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
// src/cli/commands/skill/index.ts
|
|
252
|
+
var buildSkillsAddArgs = (source, flags) => {
|
|
253
|
+
const args = ["-y", "skills", "add", source];
|
|
254
|
+
if (flags.global) {
|
|
255
|
+
args.push("--global");
|
|
256
|
+
}
|
|
257
|
+
if (flags.agent) {
|
|
258
|
+
args.push("--agent", flags.agent);
|
|
259
|
+
}
|
|
260
|
+
if (flags.yes) {
|
|
261
|
+
args.push("-y");
|
|
262
|
+
}
|
|
263
|
+
if (flags.copy) {
|
|
264
|
+
args.push("--copy");
|
|
265
|
+
}
|
|
266
|
+
if (flags.all) {
|
|
267
|
+
args.push("--all");
|
|
268
|
+
}
|
|
269
|
+
return args;
|
|
270
|
+
};
|
|
271
|
+
var runSkillsAdd = (args) => new Promise((resolve, reject) => {
|
|
272
|
+
const child = spawn("npx", args, { stdio: ["inherit", "inherit", "inherit"] });
|
|
273
|
+
child.on("error", reject);
|
|
274
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
275
|
+
});
|
|
276
|
+
var skillCommand = async (flags) => {
|
|
277
|
+
const markdown = renderSkillMarkdown(package_default.version);
|
|
278
|
+
if (!(flags.install || flags.dir)) {
|
|
279
|
+
process.stdout.write(markdown);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (flags.dir) {
|
|
283
|
+
const target = path.join(flags.dir, "webforai", "SKILL.md");
|
|
284
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
285
|
+
await fs.writeFile(target, markdown);
|
|
286
|
+
console.error(`[webforai] Agent Skill written to ${target}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const stage = await fs.mkdtemp(path.join(os.tmpdir(), "webforai-skill-"));
|
|
290
|
+
try {
|
|
291
|
+
await fs.mkdir(path.join(stage, "webforai"), { recursive: true });
|
|
292
|
+
await fs.writeFile(path.join(stage, "webforai", "SKILL.md"), markdown);
|
|
293
|
+
const args = buildSkillsAddArgs(stage, flags);
|
|
294
|
+
console.error(`[webforai] running: npx ${args.join(" ")}`);
|
|
295
|
+
const code = await runSkillsAdd(args).catch((error) => {
|
|
296
|
+
console.error(
|
|
297
|
+
`[webforai] could not launch the skills CLI (${error instanceof Error ? error.message : String(error)}).`
|
|
298
|
+
);
|
|
299
|
+
console.error("[webforai] fallback: `webforai skill --dir .claude/skills` writes the file directly.");
|
|
300
|
+
return 1;
|
|
301
|
+
});
|
|
302
|
+
if (code !== 0) {
|
|
303
|
+
process.exitCode = 1;
|
|
304
|
+
}
|
|
305
|
+
} finally {
|
|
306
|
+
await fs.rm(stage, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/cli/commands/webforai/interactive.ts
|
|
311
|
+
import { intro, log, outro, select as select7, spinner, text as text5 } from "@clack/prompts";
|
|
154
312
|
import pc2 from "picocolors";
|
|
155
313
|
|
|
314
|
+
// src/platform/error.ts
|
|
315
|
+
var PlatformApiError = class extends Error {
|
|
316
|
+
constructor(code, message, status, retryAfter) {
|
|
317
|
+
super(message);
|
|
318
|
+
this.name = "PlatformApiError";
|
|
319
|
+
this.code = code;
|
|
320
|
+
this.status = status;
|
|
321
|
+
this.retryAfter = retryAfter;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/platform/fetch.ts
|
|
326
|
+
var resolveFetch = (custom) => {
|
|
327
|
+
if (custom) {
|
|
328
|
+
return custom;
|
|
329
|
+
}
|
|
330
|
+
const globalFetch = globalThis.fetch;
|
|
331
|
+
if (typeof globalFetch !== "function") {
|
|
332
|
+
throw new Error("No global fetch in this runtime \u2014 pass a fetch implementation to createPlatformClient({ fetch })");
|
|
333
|
+
}
|
|
334
|
+
return globalFetch.bind(globalThis);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/platform/types.ts
|
|
338
|
+
var ENGINES = ["fetch", "browser", "proxy-fetch", "proxy-browser"];
|
|
339
|
+
var REGIONS = ["auto", "us", "eu", "uk", "jp", "asia"];
|
|
340
|
+
var isStoredPageStub = (item) => item.status === "ok" && "resultUrl" in item;
|
|
341
|
+
|
|
342
|
+
// src/platform/client.ts
|
|
343
|
+
var DEFAULT_BASE_URL = "https://platform.webforai.dev";
|
|
344
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
345
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
346
|
+
if (signal?.aborted) {
|
|
347
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const timer = setTimeout(() => {
|
|
351
|
+
signal?.removeEventListener("abort", onAbort);
|
|
352
|
+
resolve();
|
|
353
|
+
}, ms);
|
|
354
|
+
const onAbort = () => {
|
|
355
|
+
clearTimeout(timer);
|
|
356
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
357
|
+
};
|
|
358
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
359
|
+
});
|
|
360
|
+
var parseRetryAfter = (body, response) => {
|
|
361
|
+
const fromBody = body?.error?.retryAfter;
|
|
362
|
+
if (typeof fromBody === "number") {
|
|
363
|
+
return fromBody;
|
|
364
|
+
}
|
|
365
|
+
const header = Number(response.headers.get("retry-after"));
|
|
366
|
+
return Number.isFinite(header) && header > 0 ? header : void 0;
|
|
367
|
+
};
|
|
368
|
+
var toApiError = (response, body) => {
|
|
369
|
+
const envelope = body;
|
|
370
|
+
const code = typeof envelope?.error?.code === "string" ? envelope.error.code : "invalid_response";
|
|
371
|
+
const message = typeof envelope?.error?.message === "string" ? envelope.error.message : `unexpected response (HTTP ${response.status})`;
|
|
372
|
+
return new PlatformApiError(code, message, response.status, parseRetryAfter(body, response));
|
|
373
|
+
};
|
|
374
|
+
var createPlatformClient = (options = {}) => {
|
|
375
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
376
|
+
const fetchImpl = resolveFetch(options.fetch);
|
|
377
|
+
const request = async (path4, init) => {
|
|
378
|
+
const headers = { accept: "application/json" };
|
|
379
|
+
if (init.body !== void 0) {
|
|
380
|
+
headers["content-type"] = "application/json";
|
|
381
|
+
}
|
|
382
|
+
if (init.auth) {
|
|
383
|
+
if (!options.apiKey) {
|
|
384
|
+
throw new PlatformApiError(
|
|
385
|
+
"missing_api_key",
|
|
386
|
+
"This endpoint needs an API key: pass `apiKey` to createPlatformClient (create one on the platform dashboard).",
|
|
387
|
+
0
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
headers.authorization = `Bearer ${options.apiKey}`;
|
|
391
|
+
}
|
|
392
|
+
const response = await fetchImpl(`${baseUrl}${path4}`, {
|
|
393
|
+
method: init.method,
|
|
394
|
+
headers,
|
|
395
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
396
|
+
});
|
|
397
|
+
const body = await response.json().catch(() => void 0);
|
|
398
|
+
if (!response.ok) {
|
|
399
|
+
throw toApiError(response, body);
|
|
400
|
+
}
|
|
401
|
+
if (body === void 0) {
|
|
402
|
+
throw new PlatformApiError("invalid_response", "response body is not JSON", response.status);
|
|
403
|
+
}
|
|
404
|
+
return body;
|
|
405
|
+
};
|
|
406
|
+
const getJob = (jobId) => request(`/v1/jobs/${encodeURIComponent(jobId)}`, { method: "GET", auth: true });
|
|
407
|
+
const getJobResults = (jobId, opts) => {
|
|
408
|
+
const query = opts?.cursor ? `?cursor=${encodeURIComponent(opts.cursor)}` : "";
|
|
409
|
+
return request(`/v1/jobs/${encodeURIComponent(jobId)}/results${query}`, { method: "GET", auth: true });
|
|
410
|
+
};
|
|
411
|
+
const resolveStored = async (resultUrl) => {
|
|
412
|
+
const response = await fetchImpl(resultUrl, { method: "GET" });
|
|
413
|
+
if (!response.ok) {
|
|
414
|
+
throw new PlatformApiError(
|
|
415
|
+
"artifact_fetch_failed",
|
|
416
|
+
`stored result fetch failed (HTTP ${response.status}) \u2014 result URLs expire after ~24h`,
|
|
417
|
+
response.status
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return await response.json();
|
|
421
|
+
};
|
|
422
|
+
return {
|
|
423
|
+
scrape: (opts) => request("/v1/scrape", { method: "POST", body: opts, auth: true }),
|
|
424
|
+
scrapeAsync: (opts) => request("/v1/scrape", { method: "POST", body: { ...opts, async: true }, auth: true }),
|
|
425
|
+
batch: (opts) => request("/v1/batch", { method: "POST", body: opts, auth: true }),
|
|
426
|
+
crawl: (opts) => request("/v1/crawl", { method: "POST", body: opts, auth: true }),
|
|
427
|
+
getJob,
|
|
428
|
+
getJobResults,
|
|
429
|
+
waitForJob: async (jobId, opts = {}) => {
|
|
430
|
+
const interval = opts.pollIntervalMs ?? 2e3;
|
|
431
|
+
const timeout = opts.timeoutMs ?? 10 * 60 * 1e3;
|
|
432
|
+
const startedAt = Date.now();
|
|
433
|
+
for (; ; ) {
|
|
434
|
+
const status = await getJob(jobId);
|
|
435
|
+
opts.onStatus?.(status);
|
|
436
|
+
if (TERMINAL_STATES.has(status.status)) {
|
|
437
|
+
return status;
|
|
438
|
+
}
|
|
439
|
+
if (Date.now() - startedAt + interval > timeout) {
|
|
440
|
+
throw new PlatformApiError("poll_timeout", `job ${jobId} still ${status.status} after ${timeout}ms`, 0);
|
|
441
|
+
}
|
|
442
|
+
await sleep(interval, opts.signal);
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
jobResults: async function* (jobId) {
|
|
446
|
+
let cursor;
|
|
447
|
+
do {
|
|
448
|
+
const page = await getJobResults(jobId, { cursor });
|
|
449
|
+
for (const item of page.results) {
|
|
450
|
+
yield isStoredPageStub(item) ? await resolveStored(item.resultUrl) : item;
|
|
451
|
+
}
|
|
452
|
+
cursor = page.cursor;
|
|
453
|
+
} while (cursor);
|
|
454
|
+
},
|
|
455
|
+
demoScrape: (opts) => request("/v1/demo/scrape", { method: "POST", body: opts, auth: false })
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// src/cli/constants.ts
|
|
460
|
+
var DEFAULT_PATH = "https://example.com";
|
|
461
|
+
var LOADERS = ["fetch", "playwright", "platform"];
|
|
462
|
+
var MODES = ["default", "ai"];
|
|
463
|
+
var EXTRACTORS = ["auto", "takumi", "minimal", "none"];
|
|
464
|
+
var API_KEY_ENV = "WEBFORAI_API_KEY";
|
|
465
|
+
var PLATFORM_URL_ENV = "WEBFORAI_PLATFORM_URL";
|
|
466
|
+
|
|
467
|
+
// src/cli/helpers/assertContinue.ts
|
|
468
|
+
import { cancel, isCancel } from "@clack/prompts";
|
|
469
|
+
function assertContinue(message, cancelMessage = "Canceled.") {
|
|
470
|
+
if (isCancel(message)) {
|
|
471
|
+
cancel(cancelMessage);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/cli/helpers/inputOutputPath.ts
|
|
477
|
+
import fs3 from "node:fs";
|
|
478
|
+
import { confirm, text } from "@clack/prompts";
|
|
479
|
+
|
|
480
|
+
// src/cli/utils.ts
|
|
481
|
+
import fs2 from "node:fs";
|
|
482
|
+
import path2 from "node:path";
|
|
483
|
+
var isUrl = (maybeUrl) => {
|
|
484
|
+
try {
|
|
485
|
+
new URL(maybeUrl);
|
|
486
|
+
return true;
|
|
487
|
+
} catch {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
function changeFileExtension(filePath, newExtension) {
|
|
492
|
+
const parsedPath = filePath.split("/");
|
|
493
|
+
const fileName = parsedPath[parsedPath.length - 1];
|
|
494
|
+
const formattedNewExtension = newExtension.startsWith(".") ? newExtension : `.${newExtension}`;
|
|
495
|
+
if (fileName.startsWith(".")) {
|
|
496
|
+
const parts = fileName.split(".");
|
|
497
|
+
if (parts.length === 2) {
|
|
498
|
+
return parsedPath.slice(0, -1).concat(`${fileName}${formattedNewExtension}`).join("/");
|
|
499
|
+
}
|
|
500
|
+
parts[parts.length - 1] = newExtension.replace(/^\./, "");
|
|
501
|
+
return parsedPath.slice(0, -1).concat(parts.join(".")).join("/");
|
|
502
|
+
}
|
|
503
|
+
const lastDotIndex = fileName.lastIndexOf(".");
|
|
504
|
+
const baseName = lastDotIndex !== -1 ? fileName.slice(0, lastDotIndex) : fileName;
|
|
505
|
+
const newFileName = `${baseName}${formattedNewExtension}`;
|
|
506
|
+
parsedPath[parsedPath.length - 1] = newFileName;
|
|
507
|
+
return parsedPath.join("/");
|
|
508
|
+
}
|
|
509
|
+
function urlToFilename(url) {
|
|
510
|
+
try {
|
|
511
|
+
const urlObj = new URL(url);
|
|
512
|
+
const domainParts = urlObj.hostname.split(".").reverse().reduce((acc, part, index) => {
|
|
513
|
+
if (index === 0) {
|
|
514
|
+
return acc;
|
|
515
|
+
}
|
|
516
|
+
if (acc.length >= 2) {
|
|
517
|
+
return acc;
|
|
518
|
+
}
|
|
519
|
+
if (part === "www") {
|
|
520
|
+
return acc;
|
|
521
|
+
}
|
|
522
|
+
return [part, ...acc];
|
|
523
|
+
}, []);
|
|
524
|
+
const domainString = domainParts.reverse().join("-");
|
|
525
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
526
|
+
const relevantPathParts = pathParts.slice(-2);
|
|
527
|
+
const pathString = relevantPathParts.map((part) => decodeURIComponent(part)).join("-");
|
|
528
|
+
let filename = [domainString, pathString].filter(Boolean).join("-");
|
|
529
|
+
filename = filename.toLowerCase().replace(/[<>:"/\\|?*\x00-\x1F]/g, "").replace(/[\s.]+/g, "-").replace(/^-+|-+$/g, "");
|
|
530
|
+
return filename || "output";
|
|
531
|
+
} catch {
|
|
532
|
+
return "output";
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
var sourcePathToOutputPath = (sourcePath) => {
|
|
536
|
+
return isUrl(sourcePath) ? `${urlToFilename(sourcePath)}.md` : changeFileExtension(sourcePath, "md");
|
|
537
|
+
};
|
|
538
|
+
function getNextAvailableFilePath(filePath) {
|
|
539
|
+
const parsedPath = path2.parse(filePath);
|
|
540
|
+
const directory = parsedPath.dir;
|
|
541
|
+
const fullName = parsedPath.base;
|
|
542
|
+
const [firstPart, ...restParts] = fullName.split(".");
|
|
543
|
+
const restName = restParts.length > 0 ? `.${restParts.join(".")}` : "";
|
|
544
|
+
const baseName = firstPart.replace(/_\d+$/, "");
|
|
545
|
+
let counter = 1;
|
|
546
|
+
let nextFilePath = filePath;
|
|
547
|
+
while (fs2.existsSync(nextFilePath)) {
|
|
548
|
+
const match = firstPart.match(/_(\d+)$/);
|
|
549
|
+
if (match) {
|
|
550
|
+
counter = Number.parseInt(match[1], 10) + 1;
|
|
551
|
+
}
|
|
552
|
+
const newName = `${baseName}_${counter}${restName}`;
|
|
553
|
+
nextFilePath = path2.join(directory, newName);
|
|
554
|
+
counter++;
|
|
555
|
+
}
|
|
556
|
+
return nextFilePath;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/cli/helpers/inputOutputPath.ts
|
|
560
|
+
var inputOutputPath = async (sourcePath) => {
|
|
561
|
+
const outputPath = await text({
|
|
562
|
+
message: "Enter the output file path:",
|
|
563
|
+
placeholder: "output.md",
|
|
564
|
+
initialValue: sourcePathToOutputPath(sourcePath),
|
|
565
|
+
validate: (value) => {
|
|
566
|
+
if (value.trim() === "") {
|
|
567
|
+
return "Output path is required";
|
|
568
|
+
}
|
|
569
|
+
if (fs3.existsSync(value) && fs3.statSync(value).isDirectory()) {
|
|
570
|
+
return "No directory can be specified.";
|
|
571
|
+
}
|
|
572
|
+
if (!fs3.existsSync(value) && value.endsWith("/")) {
|
|
573
|
+
return "No directory can be specified.";
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
assertContinue(outputPath);
|
|
578
|
+
if (!fs3.existsSync(outputPath)) {
|
|
579
|
+
return outputPath;
|
|
580
|
+
}
|
|
581
|
+
const isOutputFileOverwrite = await confirm({
|
|
582
|
+
message: "The file already exists. Overwrite?",
|
|
583
|
+
initialValue: false
|
|
584
|
+
});
|
|
585
|
+
assertContinue(isOutputFileOverwrite);
|
|
586
|
+
if (isOutputFileOverwrite) {
|
|
587
|
+
return outputPath;
|
|
588
|
+
}
|
|
589
|
+
const escapedOutputPath = await text({
|
|
590
|
+
message: "Enter the output file path:",
|
|
591
|
+
placeholder: "output.md",
|
|
592
|
+
initialValue: getNextAvailableFilePath(outputPath),
|
|
593
|
+
validate: (value) => {
|
|
594
|
+
if (value.trim() === "") {
|
|
595
|
+
return "Output path is required";
|
|
596
|
+
}
|
|
597
|
+
if (fs3.existsSync(value)) {
|
|
598
|
+
return "The file already exists";
|
|
599
|
+
}
|
|
600
|
+
if (!fs3.existsSync(value) && value.endsWith("/")) {
|
|
601
|
+
return "No directory can be specified.";
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
assertContinue(escapedOutputPath);
|
|
606
|
+
return escapedOutputPath;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/cli/helpers/inputSourcePath.ts
|
|
610
|
+
import fs4 from "node:fs";
|
|
611
|
+
import { text as text2 } from "@clack/prompts";
|
|
612
|
+
var inputSourcePath = async () => {
|
|
613
|
+
const result = await text2({
|
|
614
|
+
message: "Enter the URL or html path to be converted to markdown:",
|
|
615
|
+
placeholder: DEFAULT_PATH,
|
|
616
|
+
initialValue: "",
|
|
617
|
+
validate: (value) => {
|
|
618
|
+
if (value.trim() === "") {
|
|
619
|
+
return "Source is required";
|
|
620
|
+
}
|
|
621
|
+
if (!isUrl(value)) {
|
|
622
|
+
if (!fs4.existsSync(value)) {
|
|
623
|
+
return "It appears that you are specifying a local file, but the file cannot be found. hint: when specifying a url, start with http or https.";
|
|
624
|
+
}
|
|
625
|
+
if (fs4.statSync(value).isDirectory()) {
|
|
626
|
+
return "You are specifying a local file, but you cannot specify a directory. hint: when specifying a url, start with http or https.";
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
assertContinue(result);
|
|
632
|
+
return result;
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// src/cli/helpers/selectExtractMode.ts
|
|
636
|
+
import { select } from "@clack/prompts";
|
|
637
|
+
var selectExtractMode = async () => {
|
|
638
|
+
const result = await select({
|
|
639
|
+
message: "Select processing mode:",
|
|
640
|
+
options: MODES.map((mode) => ({ value: mode, label: mode })),
|
|
641
|
+
initialValue: MODES[0]
|
|
642
|
+
});
|
|
643
|
+
assertContinue(result);
|
|
644
|
+
return result;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/cli/helpers/selectLoader.ts
|
|
648
|
+
import { select as select2 } from "@clack/prompts";
|
|
649
|
+
var loadersHint = {
|
|
650
|
+
fetch: "Fetch HTML content from the given URL",
|
|
651
|
+
playwright: "Render the page in local headless Chromium first; Playwright must be installed",
|
|
652
|
+
platform: "Convert via the hosted webforai platform API (needs an API key)"
|
|
653
|
+
};
|
|
654
|
+
var selectLoader = async () => {
|
|
655
|
+
const result = await select2({
|
|
656
|
+
message: "Select loader:",
|
|
657
|
+
initialValue: "fetch",
|
|
658
|
+
options: LOADERS.map((loader) => ({ value: loader, label: loader, hint: loadersHint[loader] }))
|
|
659
|
+
});
|
|
660
|
+
assertContinue(result);
|
|
661
|
+
return result;
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// src/cli/commands/webforai/run.ts
|
|
665
|
+
import fs6 from "node:fs/promises";
|
|
666
|
+
import path3 from "node:path";
|
|
667
|
+
|
|
668
|
+
// src/html-to-markdown.ts
|
|
669
|
+
import { fromHtml as fromHtml2 } from "hast-util-from-html";
|
|
670
|
+
|
|
156
671
|
// src/html-to-mdast.ts
|
|
157
672
|
import { fromHtml } from "hast-util-from-html";
|
|
158
673
|
import { toMdast } from "hast-util-to-mdast";
|
|
@@ -179,192 +694,1941 @@ var extractMdast = (node) => {
|
|
|
179
694
|
return extracted;
|
|
180
695
|
};
|
|
181
696
|
|
|
182
|
-
// src/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
697
|
+
// src/utils/hast-fast.ts
|
|
698
|
+
var EMPTY_METRICS = { text: 0, link: 0, commas: 0, paragraphs: 0, elements: 0 };
|
|
699
|
+
var COMMA_REGEX = /[,、,،]/g;
|
|
700
|
+
var PARAGRAPH_LIKE = /* @__PURE__ */ new Set(["p", "pre", "li", "blockquote", "figcaption", "dd", "dt"]);
|
|
701
|
+
var NON_PROSE_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "svg", "math", "iframe", "object"]);
|
|
702
|
+
var isElement = (node) => typeof node === "object" && node !== null && node.type === "element";
|
|
703
|
+
var isParent = (node) => typeof node === "object" && node !== null && Array.isArray(node.children);
|
|
704
|
+
var classList = (element3) => {
|
|
705
|
+
const className = element3.properties?.className;
|
|
706
|
+
if (Array.isArray(className)) {
|
|
707
|
+
return className;
|
|
708
|
+
}
|
|
709
|
+
if (typeof className === "string") {
|
|
710
|
+
return className.split(/\s+/).filter(Boolean);
|
|
711
|
+
}
|
|
712
|
+
return [];
|
|
713
|
+
};
|
|
714
|
+
var matchString = (element3) => {
|
|
715
|
+
const id = element3.properties?.id;
|
|
716
|
+
const classes = classList(element3);
|
|
717
|
+
if (!id && classes.length === 0) {
|
|
718
|
+
return element3.tagName;
|
|
719
|
+
}
|
|
720
|
+
return `${element3.tagName} ${typeof id === "string" ? id : ""} ${classes.join(" ")}`;
|
|
721
|
+
};
|
|
722
|
+
var MetricsCollector = class {
|
|
723
|
+
#cache = /* @__PURE__ */ new WeakMap();
|
|
724
|
+
metrics(node) {
|
|
725
|
+
if (node.type === "text") {
|
|
726
|
+
const value = node.value;
|
|
727
|
+
if (value.trim().length === 0) {
|
|
728
|
+
return EMPTY_METRICS;
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
text: value.length,
|
|
732
|
+
link: 0,
|
|
733
|
+
commas: (value.match(COMMA_REGEX) ?? []).length,
|
|
734
|
+
paragraphs: 0,
|
|
735
|
+
elements: 0
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (!isParent(node)) {
|
|
739
|
+
return EMPTY_METRICS;
|
|
740
|
+
}
|
|
741
|
+
const cached = this.#cache.get(node);
|
|
742
|
+
if (cached) {
|
|
743
|
+
return cached;
|
|
744
|
+
}
|
|
745
|
+
if (isElement(node) && NON_PROSE_TAGS.has(node.tagName)) {
|
|
746
|
+
this.#cache.set(node, EMPTY_METRICS);
|
|
747
|
+
return EMPTY_METRICS;
|
|
748
|
+
}
|
|
749
|
+
let text6 = 0;
|
|
750
|
+
let link = 0;
|
|
751
|
+
let commas = 0;
|
|
752
|
+
let paragraphs = 0;
|
|
753
|
+
let elements = 0;
|
|
754
|
+
for (const child of node.children) {
|
|
755
|
+
const childMetrics = this.metrics(child);
|
|
756
|
+
text6 += childMetrics.text;
|
|
757
|
+
link += childMetrics.link;
|
|
758
|
+
commas += childMetrics.commas;
|
|
759
|
+
paragraphs += childMetrics.paragraphs;
|
|
760
|
+
elements += childMetrics.elements;
|
|
761
|
+
if (isElement(child)) {
|
|
762
|
+
elements += 1;
|
|
763
|
+
if (PARAGRAPH_LIKE.has(child.tagName)) {
|
|
764
|
+
paragraphs += 1;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (isElement(node) && node.tagName === "a") {
|
|
769
|
+
link = text6;
|
|
770
|
+
}
|
|
771
|
+
const metrics = { text: text6, link, commas, paragraphs, elements };
|
|
772
|
+
this.#cache.set(node, metrics);
|
|
773
|
+
return metrics;
|
|
774
|
+
}
|
|
775
|
+
textLength(node) {
|
|
776
|
+
return this.metrics(node).text;
|
|
777
|
+
}
|
|
778
|
+
/** Fraction of a subtree's text that is link anchor text. `0` for empty subtrees. */
|
|
779
|
+
linkDensity(node) {
|
|
780
|
+
const { text: text6, link } = this.metrics(node);
|
|
781
|
+
if (text6 === 0) {
|
|
782
|
+
return 0;
|
|
783
|
+
}
|
|
784
|
+
return Math.min(1, link / text6);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
var pruneInPlace = (tree, keep) => {
|
|
788
|
+
if (!isParent(tree)) {
|
|
789
|
+
return tree;
|
|
790
|
+
}
|
|
791
|
+
const stack = [tree];
|
|
792
|
+
while (stack.length > 0) {
|
|
793
|
+
const parent = stack.pop();
|
|
794
|
+
const children = parent.children;
|
|
795
|
+
let write = 0;
|
|
796
|
+
for (let read = 0; read < children.length; read++) {
|
|
797
|
+
const child = children[read];
|
|
798
|
+
if (!keep(child)) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
children[write++] = child;
|
|
802
|
+
if (isParent(child)) {
|
|
803
|
+
stack.push(child);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
children.length = write;
|
|
807
|
+
}
|
|
808
|
+
return tree;
|
|
809
|
+
};
|
|
810
|
+
var walk = (tree, visit) => {
|
|
811
|
+
const stack = [{ node: tree, parent: void 0 }];
|
|
812
|
+
while (stack.length > 0) {
|
|
813
|
+
const { node, parent } = stack.pop();
|
|
814
|
+
if (visit(node, parent) === false) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (isParent(node)) {
|
|
818
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
819
|
+
stack.push({ node: node.children[i], parent: node });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
var collectElements = (tree, predicate) => {
|
|
825
|
+
const found = [];
|
|
826
|
+
walk(tree, (node) => {
|
|
827
|
+
if (isElement(node) && predicate(node)) {
|
|
828
|
+
found.push(node);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
return found;
|
|
832
|
+
};
|
|
833
|
+
var findElement = (tree, predicate) => {
|
|
834
|
+
let found;
|
|
835
|
+
walk(tree, (node) => {
|
|
836
|
+
if (found) {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
if (isElement(node) && predicate(node)) {
|
|
840
|
+
found = node;
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
return found;
|
|
845
|
+
};
|
|
846
|
+
var cloneHast = (tree) => {
|
|
847
|
+
try {
|
|
848
|
+
return structuredClone(tree);
|
|
849
|
+
} catch {
|
|
850
|
+
return manualClone(tree);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
var manualClone = (node) => {
|
|
854
|
+
if (Array.isArray(node)) {
|
|
855
|
+
return node.map(manualClone);
|
|
856
|
+
}
|
|
857
|
+
if (typeof node !== "object" || node === null) {
|
|
858
|
+
return node;
|
|
859
|
+
}
|
|
860
|
+
const copy = {};
|
|
861
|
+
for (const [key, value] of Object.entries(node)) {
|
|
862
|
+
copy[key] = manualClone(value);
|
|
863
|
+
}
|
|
864
|
+
return copy;
|
|
865
|
+
};
|
|
866
|
+
var property = (element3, key) => {
|
|
867
|
+
const properties = element3.properties;
|
|
868
|
+
if (!properties) {
|
|
869
|
+
return void 0;
|
|
870
|
+
}
|
|
871
|
+
const direct = properties[key];
|
|
872
|
+
if (direct !== void 0) {
|
|
873
|
+
return direct;
|
|
874
|
+
}
|
|
875
|
+
if (!(key.includes("-") || key.includes(":"))) {
|
|
876
|
+
return void 0;
|
|
877
|
+
}
|
|
878
|
+
return properties[camelCaseAttribute(key)];
|
|
879
|
+
};
|
|
880
|
+
var camelCaseAttribute = (key) => key.replace(/[-:]([a-z])/g, (_, character) => character.toUpperCase());
|
|
881
|
+
var numericProperty = (element3, key) => {
|
|
882
|
+
const raw = property(element3, key);
|
|
883
|
+
if (typeof raw === "number") {
|
|
884
|
+
return Number.isFinite(raw) ? raw : void 0;
|
|
885
|
+
}
|
|
886
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
887
|
+
const parsed = Number.parseFloat(raw);
|
|
888
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
889
|
+
}
|
|
890
|
+
return void 0;
|
|
891
|
+
};
|
|
892
|
+
var stringProperty = (element3, key) => {
|
|
893
|
+
const raw = property(element3, key);
|
|
894
|
+
if (typeof raw === "string") {
|
|
895
|
+
return raw;
|
|
896
|
+
}
|
|
897
|
+
if (Array.isArray(raw)) {
|
|
898
|
+
return raw.join(" ");
|
|
899
|
+
}
|
|
900
|
+
if (raw === true) {
|
|
901
|
+
return "";
|
|
902
|
+
}
|
|
903
|
+
return void 0;
|
|
904
|
+
};
|
|
905
|
+
var isTruthyAttribute = (element3, key) => {
|
|
906
|
+
const raw = property(element3, key);
|
|
907
|
+
if (raw === void 0 || raw === null || raw === false) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
if (raw === true) {
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
if (typeof raw === "string") {
|
|
914
|
+
return raw.toLowerCase() !== "false";
|
|
915
|
+
}
|
|
916
|
+
return true;
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// src/extractors/lib/constants.ts
|
|
920
|
+
var REGEXPS = {
|
|
921
|
+
/**
|
|
922
|
+
* Containers that almost never hold the article body.
|
|
923
|
+
*
|
|
924
|
+
* Note the words that are deliberately *absent*. `header` and `footer` match the heading
|
|
925
|
+
* anchors and article furniture that documentation generators emit (`<a class="header">`,
|
|
926
|
+
* `class="post-header"`), so matching them here deletes the page's headings. The landmark
|
|
927
|
+
* versions of those elements are handled structurally instead.
|
|
928
|
+
*/
|
|
929
|
+
unlikelyCandidates: /-ad-|ai2html|banner|breadcrumb|combx|comment(?!ary)|community|cover-wrap|disqus|gdpr|legends|menu|related|remark|replies|rss|shoutbox|sidebar|site-index|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote|speechify-ignore|avatar|subscribe|newsletter|paywall|consent|cookie|toolbar|masthead|promo|share|sharing|widget|tooltip|dropdown|modal|overlay|backdrop|carousel|announcement|notification|toast|skip-link|screen-reader|visually-hidden|sr-only/i,
|
|
930
|
+
/** Rescues containers whose class merely happens to contain an unlikely substring. */
|
|
931
|
+
okMaybeItsaCandidate: /and|article|body|column|content|main|shadow|code|post|entry|story|markdown|prose|doc/i,
|
|
932
|
+
/** Very high-confidence boilerplate, not rescued by the clause above. */
|
|
933
|
+
stronglyUnlikely: /^(nav|navbar|breadcrumbs?|cookie-?(banner|consent|notice)|consent-?(banner|manager)|skip-?link|site-?header|site-?footer|global-?(header|footer)|social-?(share|links?)|share-?(bar|buttons?)|related-?(posts?|articles?)|newsletter-?(signup|form)|comment-?(form|list)|toc-?sidebar)$/i,
|
|
934
|
+
/** Class/id fragments that mark real article content. */
|
|
935
|
+
positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story|markdown|prose|documentation|doc-?content|readme/i,
|
|
936
|
+
/** Class/id fragments that mark furniture. */
|
|
937
|
+
negative: /-ad-|hidden|^hid$| hid$|^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|widget|toolbar|nav|menu|cookie|consent|subscribe|newsletter|popup|modal|overlay|breadcrumb|pagination|social/i,
|
|
938
|
+
/** Placeholder imagery that should never reach the output. */
|
|
939
|
+
hidden: /fallback-image/i,
|
|
940
|
+
byline: /byline|author|dateline|writtenby|p-author|c-author|post-meta|entry-meta/i,
|
|
941
|
+
/** Wiki/CMS chrome that generic scoring alone tends to keep. */
|
|
942
|
+
specialUnlikelyCandidates: /frb-|uls-menu|language-link|mw-editsection|noprint|navbox|metadata|catlinks|printfooter/i
|
|
943
|
+
};
|
|
944
|
+
var NON_CONTENT_TAGS = /* @__PURE__ */ new Set([
|
|
945
|
+
"script",
|
|
946
|
+
"style",
|
|
947
|
+
"link",
|
|
948
|
+
"meta",
|
|
949
|
+
"noscript",
|
|
950
|
+
"template",
|
|
951
|
+
"base",
|
|
952
|
+
"param",
|
|
953
|
+
"source",
|
|
954
|
+
"track",
|
|
955
|
+
"title"
|
|
956
|
+
]);
|
|
957
|
+
var CHROME_TAGS = /* @__PURE__ */ new Set(["nav", "aside", "dialog", "menu"]);
|
|
958
|
+
var CONTEXTUAL_LANDMARK_TAGS = /* @__PURE__ */ new Set(["header", "footer"]);
|
|
959
|
+
var HIDDEN_CLASS_NAMES = /* @__PURE__ */ new Set([
|
|
960
|
+
"hidden",
|
|
961
|
+
"invisible",
|
|
962
|
+
"is-hidden",
|
|
963
|
+
"d-none",
|
|
964
|
+
"display-none",
|
|
965
|
+
"hide",
|
|
966
|
+
"is-invisible"
|
|
967
|
+
]);
|
|
968
|
+
var RESPONSIVE_DISPLAY_OVERRIDE = /^(sm|md|lg|xl|2xl|print):(block|flex|grid|inline|inline-block|inline-flex|table|contents|list-item)$/;
|
|
969
|
+
var UNLIKELY_ROLES = /* @__PURE__ */ new Set([
|
|
970
|
+
"menu",
|
|
971
|
+
"menubar",
|
|
972
|
+
"menuitem",
|
|
973
|
+
"complementary",
|
|
974
|
+
"navigation",
|
|
975
|
+
"alert",
|
|
976
|
+
"alertdialog",
|
|
977
|
+
"dialog",
|
|
978
|
+
"banner",
|
|
979
|
+
"contentinfo",
|
|
980
|
+
"search",
|
|
981
|
+
"searchbox",
|
|
982
|
+
"toolbar",
|
|
983
|
+
"tablist",
|
|
984
|
+
"tooltip"
|
|
985
|
+
// `presentation` is deliberately absent. It marks an element as decorative *structure* — a
|
|
986
|
+
// layout table, a wrapper — while the content inside it is usually real. Treating it as
|
|
987
|
+
// chrome deletes article text.
|
|
988
|
+
]);
|
|
989
|
+
var CONTENT_ROLES = /* @__PURE__ */ new Set(["main", "article", "document"]);
|
|
990
|
+
var SCOREABLE_TAGS = /* @__PURE__ */ new Set(["p", "td", "pre", "blockquote", "dd", "li", "figure", "section", "div"]);
|
|
991
|
+
var CANDIDATE_TAGS = /* @__PURE__ */ new Set([
|
|
992
|
+
"div",
|
|
993
|
+
"section",
|
|
994
|
+
"article",
|
|
995
|
+
"main",
|
|
996
|
+
"td",
|
|
997
|
+
"pre",
|
|
998
|
+
"blockquote",
|
|
999
|
+
"form",
|
|
1000
|
+
"ul",
|
|
1001
|
+
"ol",
|
|
1002
|
+
"dl"
|
|
1003
|
+
]);
|
|
1004
|
+
var EMBEDDED_CONTENT_TAGS = /* @__PURE__ */ new Set([
|
|
1005
|
+
"img",
|
|
1006
|
+
"picture",
|
|
1007
|
+
"video",
|
|
1008
|
+
"audio",
|
|
1009
|
+
"iframe",
|
|
1010
|
+
"embed",
|
|
1011
|
+
"object",
|
|
1012
|
+
"svg",
|
|
1013
|
+
// Formulae hold no text of their own, so an empty-wrapper rule would otherwise delete them.
|
|
1014
|
+
"math",
|
|
1015
|
+
"mjx-container",
|
|
1016
|
+
"canvas"
|
|
1017
|
+
]);
|
|
1018
|
+
var DROP_WHEN_EMPTY_TAGS = /* @__PURE__ */ new Set([
|
|
1019
|
+
"p",
|
|
1020
|
+
"h1",
|
|
1021
|
+
"h2",
|
|
1022
|
+
"h3",
|
|
1023
|
+
"h4",
|
|
1024
|
+
"h5",
|
|
1025
|
+
"h6",
|
|
1026
|
+
"li",
|
|
1027
|
+
"td",
|
|
1028
|
+
"th",
|
|
1029
|
+
"blockquote",
|
|
1030
|
+
"figcaption",
|
|
1031
|
+
"dd",
|
|
1032
|
+
"dt",
|
|
1033
|
+
"a",
|
|
1034
|
+
"span",
|
|
1035
|
+
"div",
|
|
1036
|
+
"section",
|
|
1037
|
+
"article"
|
|
1038
|
+
]);
|
|
1039
|
+
var MIN_CONTENT_LENGTH = {
|
|
1040
|
+
ja: 140,
|
|
1041
|
+
zh: 140,
|
|
1042
|
+
ko: 160,
|
|
1043
|
+
th: 160,
|
|
1044
|
+
default: 400
|
|
1045
|
+
};
|
|
1046
|
+
var minContentLength = (lang) => {
|
|
1047
|
+
if (!lang) {
|
|
1048
|
+
return MIN_CONTENT_LENGTH.default;
|
|
1049
|
+
}
|
|
1050
|
+
const primary = lang.toLowerCase().split("-")[0];
|
|
1051
|
+
return MIN_CONTENT_LENGTH[primary] ?? MIN_CONTENT_LENGTH.default;
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
// src/extractors/lib/ui-chrome.ts
|
|
1055
|
+
var UI_PHRASES = /* @__PURE__ */ new Set([
|
|
1056
|
+
// Documentation and article controls
|
|
1057
|
+
"copy",
|
|
1058
|
+
"copy page",
|
|
1059
|
+
"copy link",
|
|
1060
|
+
"copy code",
|
|
1061
|
+
"copied",
|
|
1062
|
+
"edit this page",
|
|
1063
|
+
"edit on github",
|
|
1064
|
+
"on this page",
|
|
1065
|
+
"in this article",
|
|
1066
|
+
"table of contents",
|
|
1067
|
+
"contents",
|
|
1068
|
+
"print",
|
|
1069
|
+
"print this page",
|
|
1070
|
+
"back to top",
|
|
1071
|
+
"scroll to top",
|
|
1072
|
+
"skip to content",
|
|
1073
|
+
"skip to main content",
|
|
1074
|
+
"permalink",
|
|
1075
|
+
"anchor",
|
|
1076
|
+
// Feedback widgets
|
|
1077
|
+
"was this page helpful",
|
|
1078
|
+
"was this helpful",
|
|
1079
|
+
"was this article helpful",
|
|
1080
|
+
"yes no",
|
|
1081
|
+
"feedback",
|
|
1082
|
+
"send feedback",
|
|
1083
|
+
"report an issue",
|
|
1084
|
+
"rate this page",
|
|
1085
|
+
// Sharing and subscription
|
|
1086
|
+
"share",
|
|
1087
|
+
"share this",
|
|
1088
|
+
"share this article",
|
|
1089
|
+
"share on twitter",
|
|
1090
|
+
"share on facebook",
|
|
1091
|
+
"subscribe",
|
|
1092
|
+
"sign up",
|
|
1093
|
+
"sign in",
|
|
1094
|
+
"log in",
|
|
1095
|
+
"login",
|
|
1096
|
+
"newsletter",
|
|
1097
|
+
// Advertising and promotion
|
|
1098
|
+
"advertisement",
|
|
1099
|
+
"advertisements",
|
|
1100
|
+
"sponsored",
|
|
1101
|
+
"sponsored content",
|
|
1102
|
+
"promoted",
|
|
1103
|
+
// Navigation affordances
|
|
1104
|
+
"read more",
|
|
1105
|
+
"show more",
|
|
1106
|
+
"show less",
|
|
1107
|
+
"see more",
|
|
1108
|
+
"load more",
|
|
1109
|
+
"previous",
|
|
1110
|
+
"next",
|
|
1111
|
+
"previous page",
|
|
1112
|
+
"next page",
|
|
1113
|
+
"menu",
|
|
1114
|
+
"close",
|
|
1115
|
+
"search",
|
|
1116
|
+
// Japanese equivalents
|
|
1117
|
+
"\u76EE\u6B21",
|
|
1118
|
+
"\u5E83\u544A",
|
|
1119
|
+
"\u30B7\u30A7\u30A2",
|
|
1120
|
+
"\u5171\u6709",
|
|
1121
|
+
"\u3053\u306E\u8A18\u4E8B\u3092\u30B7\u30A7\u30A2",
|
|
1122
|
+
"\u7D9A\u304D\u3092\u8AAD\u3080",
|
|
1123
|
+
"\u3082\u3063\u3068\u898B\u308B",
|
|
1124
|
+
"\u95A2\u9023\u8A18\u4E8B",
|
|
1125
|
+
"\u30B9\u30DD\u30F3\u30B5\u30FC\u30EA\u30F3\u30AF",
|
|
1126
|
+
"\u30DA\u30FC\u30B8\u306E\u5148\u982D\u3078",
|
|
1127
|
+
"\u30C8\u30C3\u30D7\u3078\u623B\u308B",
|
|
1128
|
+
"\u524D\u3078",
|
|
1129
|
+
"\u6B21\u3078",
|
|
1130
|
+
"\u9589\u3058\u308B",
|
|
1131
|
+
"\u691C\u7D22",
|
|
1132
|
+
"\u30ED\u30B0\u30A4\u30F3",
|
|
1133
|
+
"\u4F1A\u54E1\u767B\u9332"
|
|
1134
|
+
]);
|
|
1135
|
+
var MAX_PHRASE_LENGTH = 40;
|
|
1136
|
+
var normalize = (text6) => text6.toLowerCase().replace(/[?!.,:;·・…"'“”‘’()[\]{}<>|/\\—–-]/g, " ").replace(/\s+/g, " ").trim();
|
|
1137
|
+
var isUiChromeText = (text6) => {
|
|
1138
|
+
if (text6.length === 0 || text6.length > MAX_PHRASE_LENGTH) {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
const normalized = normalize(text6);
|
|
1142
|
+
if (normalized.length === 0) {
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
return UI_PHRASES.has(normalized);
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// src/extractors/lib/sanitize.ts
|
|
1149
|
+
var INVISIBLE_STYLE = /(^|;)\s*(display\s*:\s*none|visibility\s*:\s*hidden)\s*(;|$)/i;
|
|
1150
|
+
var hasHiddenClass = (element3) => {
|
|
1151
|
+
const classes = classList(element3);
|
|
1152
|
+
if (!classes.some((name) => HIDDEN_CLASS_NAMES.has(name))) {
|
|
1153
|
+
return classes.some((name) => REGEXPS.hidden.test(name));
|
|
1154
|
+
}
|
|
1155
|
+
return !classes.some((name) => RESPONSIVE_DISPLAY_OVERRIDE.test(name));
|
|
1156
|
+
};
|
|
1157
|
+
var isNonContent = (node) => {
|
|
1158
|
+
if (node.type === "comment" || node.type === "doctype") {
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
return isElement(node) && NON_CONTENT_TAGS.has(node.tagName);
|
|
1162
|
+
};
|
|
1163
|
+
var isHidden = (element3) => {
|
|
1164
|
+
if (containsMath(element3)) {
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
if (isTruthyAttribute(element3, "hidden")) {
|
|
1168
|
+
return true;
|
|
1169
|
+
}
|
|
1170
|
+
const style = stringProperty(element3, "style");
|
|
1171
|
+
if (style && INVISIBLE_STYLE.test(style)) {
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1174
|
+
if (hasHiddenClass(element3)) {
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
const width = numericProperty(element3, "data-rwidth");
|
|
1178
|
+
const height = numericProperty(element3, "data-rheight");
|
|
1179
|
+
if (width === 0 && height === 0) {
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
return false;
|
|
1183
|
+
};
|
|
1184
|
+
var PLACEHOLDER_IMAGE_SRC = /^data:image\/|^about:blank$|spacer\.gif$|blank\.(gif|png)$/i;
|
|
1185
|
+
var isPlaceholderImage = (element3) => {
|
|
1186
|
+
if (element3.tagName === "img") {
|
|
1187
|
+
const src = stringProperty(element3, "src");
|
|
1188
|
+
return src !== void 0 && PLACEHOLDER_IMAGE_SRC.test(src);
|
|
1189
|
+
}
|
|
1190
|
+
if (element3.tagName !== "picture") {
|
|
1191
|
+
return false;
|
|
1192
|
+
}
|
|
1193
|
+
for (const child of element3.children) {
|
|
1194
|
+
if (isElement(child) && child.tagName === "img") {
|
|
1195
|
+
return isPlaceholderImage(child);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return false;
|
|
1199
|
+
};
|
|
1200
|
+
var hoistNoscriptImages = (tree) => {
|
|
1201
|
+
const visit = (parent) => {
|
|
1202
|
+
for (let index = 0; index < parent.children.length; index++) {
|
|
1203
|
+
const child = parent.children[index];
|
|
1204
|
+
if (!isElement(child)) {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
if (child.tagName !== "noscript") {
|
|
1208
|
+
visit(child);
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const hasImage = child.children.some(
|
|
1212
|
+
(node) => isElement(node) && (node.tagName === "img" || node.tagName === "picture")
|
|
1213
|
+
);
|
|
1214
|
+
if (!hasImage) {
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
const rescued = child.children;
|
|
1218
|
+
parent.children.splice(index, 1, ...rescued);
|
|
1219
|
+
const previous = parent.children[index - 1];
|
|
1220
|
+
if (index > 0 && isElement(previous) && isPlaceholderImage(previous)) {
|
|
1221
|
+
parent.children.splice(index - 1, 1);
|
|
1222
|
+
index -= 1;
|
|
1223
|
+
}
|
|
1224
|
+
index += rescued.length - 1;
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
if ("children" in tree) {
|
|
1228
|
+
visit(tree);
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
var containsMath = (element3) => {
|
|
1232
|
+
if (element3.tagName === "math") {
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
for (const child of element3.children) {
|
|
1236
|
+
if (isElement(child) && containsMath(child)) {
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return false;
|
|
1241
|
+
};
|
|
1242
|
+
var stripNonContent = (tree) => {
|
|
1243
|
+
hoistNoscriptImages(tree);
|
|
1244
|
+
return pruneInPlace(tree, (node) => {
|
|
1245
|
+
if (isNonContent(node)) {
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
if (isElement(node) && isHidden(node)) {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
return true;
|
|
1252
|
+
});
|
|
1253
|
+
};
|
|
1254
|
+
var findLandmarkChrome = (tree) => {
|
|
1255
|
+
const chrome = /* @__PURE__ */ new Set();
|
|
1256
|
+
const visit = (node, depth, insideContent) => {
|
|
1257
|
+
if (!("children" in node)) {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
for (const child of node.children) {
|
|
1261
|
+
if (!isElement(child)) {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
const nowInsideContent = insideContent || child.tagName === "article" || child.tagName === "main" || stringProperty(child, "role") === "main";
|
|
1265
|
+
if (CONTEXTUAL_LANDMARK_TAGS.has(child.tagName) && !insideContent && depth <= 3) {
|
|
1266
|
+
chrome.add(child);
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
visit(child, depth + 1, nowInsideContent);
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
visit(tree, 0, false);
|
|
1273
|
+
return chrome;
|
|
1274
|
+
};
|
|
1275
|
+
var isUnlikelyCandidate = (element3) => {
|
|
1276
|
+
if (element3.tagName === "article" || element3.tagName === "main" || element3.tagName === "body") {
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
const role = stringProperty(element3, "role");
|
|
1280
|
+
if (role && UNLIKELY_ROLES.has(role)) {
|
|
1281
|
+
return true;
|
|
1282
|
+
}
|
|
1283
|
+
if (CHROME_TAGS.has(element3.tagName)) {
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1286
|
+
const match = matchString(element3);
|
|
1287
|
+
if (REGEXPS.specialUnlikelyCandidates.test(match)) {
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
for (const name of [...classList(element3), stringProperty(element3, "id") ?? ""]) {
|
|
1291
|
+
if (REGEXPS.stronglyUnlikely.test(name)) {
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (REGEXPS.unlikelyCandidates.test(match) && !REGEXPS.okMaybeItsaCandidate.test(match)) {
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
return false;
|
|
1299
|
+
};
|
|
1300
|
+
var findUnlikelyElements = (tree) => {
|
|
1301
|
+
const landmarks = findLandmarkChrome(tree);
|
|
1302
|
+
const doomed = [];
|
|
1303
|
+
const visit = (node) => {
|
|
1304
|
+
if (!("children" in node)) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
for (const child of node.children) {
|
|
1308
|
+
if (!isElement(child)) {
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
if (landmarks.has(child) || isUnlikelyCandidate(child)) {
|
|
1312
|
+
doomed.push(child);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
visit(child);
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
visit(tree);
|
|
1319
|
+
return doomed;
|
|
1320
|
+
};
|
|
1321
|
+
var isEmbeddedContent = (element3) => {
|
|
1322
|
+
if (EMBEDDED_CONTENT_TAGS.has(element3.tagName)) {
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
return element3.tagName === "br" || element3.tagName === "hr" || element3.tagName === "input";
|
|
1326
|
+
};
|
|
1327
|
+
var collectPreformattedElements = (tree) => {
|
|
1328
|
+
const protectedElements = /* @__PURE__ */ new Set();
|
|
1329
|
+
const markSubtree = (node) => {
|
|
1330
|
+
if (!("children" in node)) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
for (const child of node.children) {
|
|
1334
|
+
if (isElement(child)) {
|
|
1335
|
+
protectedElements.add(child);
|
|
1336
|
+
markSubtree(child);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
walk(tree, (node) => {
|
|
1341
|
+
if (isElement(node) && (node.tagName === "pre" || node.tagName === "code")) {
|
|
1342
|
+
protectedElements.add(node);
|
|
1343
|
+
markSubtree(node);
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
return protectedElements;
|
|
1348
|
+
};
|
|
1349
|
+
var dropEmptyElements = (tree, collector) => {
|
|
1350
|
+
const preformatted = collectPreformattedElements(tree);
|
|
1351
|
+
const visit = (parent) => {
|
|
1352
|
+
for (let index = 0; index < parent.children.length; index++) {
|
|
1353
|
+
const child = parent.children[index];
|
|
1354
|
+
if (!isElement(child)) {
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
if (preformatted.has(child) || !DROP_WHEN_EMPTY_TAGS.has(child.tagName)) {
|
|
1358
|
+
visit(child);
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
if (collector.metrics(child).text > 0 || hasEmbeddedDescendant(child)) {
|
|
1362
|
+
visit(child);
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
if (holdsWhitespace(child)) {
|
|
1366
|
+
parent.children[index] = { type: "text", value: " " };
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
parent.children.splice(index, 1);
|
|
1370
|
+
index -= 1;
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
if ("children" in tree) {
|
|
1374
|
+
visit(tree);
|
|
1375
|
+
}
|
|
1376
|
+
return tree;
|
|
1377
|
+
};
|
|
1378
|
+
var holdsWhitespace = (element3) => {
|
|
1379
|
+
for (const child of element3.children) {
|
|
1380
|
+
if (child.type === "text" && /\s/.test(child.value)) {
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
if (isElement(child) && holdsWhitespace(child)) {
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return false;
|
|
1388
|
+
};
|
|
1389
|
+
var hasEmbeddedDescendant = (element3) => {
|
|
1390
|
+
for (const child of element3.children) {
|
|
1391
|
+
if (!isElement(child)) {
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
if (isEmbeddedContent(child) || hasEmbeddedDescendant(child)) {
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return false;
|
|
1399
|
+
};
|
|
1400
|
+
var REFERENCE_HEADING = /^(see also|references?|further reading|external links?|bibliography|sources|footnotes|citations|関連項目|参考文献|外部リンク|脚注|出典|参考リンク|参考資料)$/i;
|
|
1401
|
+
var MAX_DANGLING_HEADING_LENGTH = 60;
|
|
1402
|
+
var isHeadingTag = (element3) => /^h[1-6]$/.test(element3.tagName);
|
|
1403
|
+
var previousElement = (siblings, index) => {
|
|
1404
|
+
for (let cursor = index - 1; cursor >= 0; cursor--) {
|
|
1405
|
+
const sibling = siblings[cursor];
|
|
1406
|
+
if (isElement(sibling)) {
|
|
1407
|
+
return sibling;
|
|
1408
|
+
}
|
|
1409
|
+
if (sibling.type === "text" && sibling.value.trim().length > 0) {
|
|
1410
|
+
return void 0;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return void 0;
|
|
1414
|
+
};
|
|
1415
|
+
var condemnLinkBlock = (block, siblings, index, collector, doomed) => {
|
|
1416
|
+
const heading2 = previousElement(siblings, index);
|
|
1417
|
+
const isHeading = heading2 !== void 0 && isHeadingTag(heading2);
|
|
1418
|
+
if (isHeading && REFERENCE_HEADING.test(elementText(heading2).trim())) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (introducedByColon(siblings, index, collector)) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
doomed.push(block);
|
|
1425
|
+
if (isHeading && elementText(heading2).trim().length <= MAX_DANGLING_HEADING_LENGTH) {
|
|
1426
|
+
doomed.push(heading2);
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
var introducedByColon = (siblings, index, collector) => {
|
|
1430
|
+
const before = previousElement(siblings, index);
|
|
1431
|
+
if (!before || collector.metrics(before).text === 0) {
|
|
1432
|
+
return false;
|
|
1433
|
+
}
|
|
1434
|
+
return /[::]\s*$/.test(elementText(before).trim());
|
|
1435
|
+
};
|
|
1436
|
+
var findCleanupTargets = (tree, collector) => {
|
|
1437
|
+
const doomed = [];
|
|
1438
|
+
const visit = (node) => {
|
|
1439
|
+
if (!("children" in node)) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const siblings = node.children;
|
|
1443
|
+
for (let index = 0; index < siblings.length; index++) {
|
|
1444
|
+
const child = siblings[index];
|
|
1445
|
+
if (!isElement(child)) {
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
if (child.tagName === "article" || child.tagName === "main") {
|
|
1449
|
+
visit(child);
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (isLinkOnlyBlock(child, collector)) {
|
|
1453
|
+
condemnLinkBlock(child, siblings, index, collector, doomed);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (isMidArticleWidget(child)) {
|
|
1457
|
+
doomed.push(child);
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
visit(child);
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
visit(tree);
|
|
1464
|
+
return doomed;
|
|
1465
|
+
};
|
|
1466
|
+
var LINK_BLOCK_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "nav", "div", "p"]);
|
|
1467
|
+
var MIN_LINK_BLOCK_ANCHORS = 3;
|
|
1468
|
+
var isLinkOnlyBlock = (element3, collector) => {
|
|
1469
|
+
if (!LINK_BLOCK_TAGS.has(element3.tagName)) {
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
const metrics = collector.metrics(element3);
|
|
1473
|
+
if (metrics.text === 0 || metrics.commas > 0) {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
if (metrics.link / metrics.text <= 0.9) {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
return countAnchors(element3) >= MIN_LINK_BLOCK_ANCHORS;
|
|
1480
|
+
};
|
|
1481
|
+
var countAnchors = (element3) => {
|
|
1482
|
+
let count = 0;
|
|
1483
|
+
const stack = [element3];
|
|
1484
|
+
while (stack.length > 0 && count < MIN_LINK_BLOCK_ANCHORS) {
|
|
1485
|
+
const node = stack.pop();
|
|
1486
|
+
if (isElement(node) && node.tagName === "a") {
|
|
1487
|
+
count += 1;
|
|
1488
|
+
}
|
|
1489
|
+
if ("children" in node) {
|
|
1490
|
+
for (const child of node.children) {
|
|
1491
|
+
stack.push(child);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return count;
|
|
1496
|
+
};
|
|
1497
|
+
var isMidArticleWidget = (element3) => {
|
|
1498
|
+
if (element3.tagName === "form" || element3.tagName === "fieldset") {
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
if (element3.tagName === "button") {
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
return isUiChromeText(elementText(element3));
|
|
1505
|
+
};
|
|
1506
|
+
var cleanContent = (tree, collector) => {
|
|
1507
|
+
const doomed = new Set(findCleanupTargets(tree, collector));
|
|
1508
|
+
if (doomed.size > 0) {
|
|
1509
|
+
pruneInPlace(tree, (node) => !(isElement(node) && doomed.has(node)));
|
|
1510
|
+
}
|
|
1511
|
+
return dropEmptyElements(tree, new MetricsCollector());
|
|
1512
|
+
};
|
|
1513
|
+
var elementText = (node) => {
|
|
1514
|
+
if (node.type === "text") {
|
|
1515
|
+
return node.value;
|
|
1516
|
+
}
|
|
1517
|
+
if (!("children" in node)) {
|
|
1518
|
+
return "";
|
|
1519
|
+
}
|
|
1520
|
+
let text6 = "";
|
|
1521
|
+
for (const child of node.children) {
|
|
1522
|
+
text6 += elementText(child);
|
|
1523
|
+
}
|
|
1524
|
+
return text6;
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
// src/adapters/registry.ts
|
|
1528
|
+
import { select as select3, selectAll } from "hast-util-select";
|
|
1529
|
+
var EMPTY_SIGNATURE = {
|
|
1530
|
+
classes: /* @__PURE__ */ new Set(),
|
|
1531
|
+
ids: /* @__PURE__ */ new Set(),
|
|
1532
|
+
tags: /* @__PURE__ */ new Set(),
|
|
1533
|
+
generator: "",
|
|
1534
|
+
meta: ""
|
|
1535
|
+
};
|
|
1536
|
+
var buildPageSignature = (root) => {
|
|
1537
|
+
const classes = /* @__PURE__ */ new Set();
|
|
1538
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1539
|
+
const tags = /* @__PURE__ */ new Set();
|
|
1540
|
+
const generatorParts = [];
|
|
1541
|
+
const metaParts = [];
|
|
1542
|
+
walk(root, (node) => {
|
|
1543
|
+
if (!isElement(node)) {
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
tags.add(node.tagName);
|
|
1547
|
+
for (const name of classList(node)) {
|
|
1548
|
+
classes.add(name);
|
|
1549
|
+
}
|
|
1550
|
+
const id = stringProperty(node, "id");
|
|
1551
|
+
if (id) {
|
|
1552
|
+
ids.add(id);
|
|
1553
|
+
}
|
|
1554
|
+
if (node.tagName === "meta") {
|
|
1555
|
+
const key = stringProperty(node, "name") ?? stringProperty(node, "property") ?? "";
|
|
1556
|
+
const content = stringProperty(node, "content") ?? "";
|
|
1557
|
+
metaParts.push(`${key}=${content}`);
|
|
1558
|
+
if (key.toLowerCase() === "generator") {
|
|
1559
|
+
generatorParts.push(content);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
return {
|
|
1564
|
+
classes,
|
|
1565
|
+
ids,
|
|
1566
|
+
tags,
|
|
1567
|
+
generator: generatorParts.join(" ").toLowerCase(),
|
|
1568
|
+
meta: metaParts.join("\n").toLowerCase()
|
|
1569
|
+
};
|
|
1570
|
+
};
|
|
1571
|
+
var DEFAULT_MIN_LENGTH = 40;
|
|
1572
|
+
var hostOf = (url) => {
|
|
1573
|
+
if (!url) {
|
|
1574
|
+
return void 0;
|
|
1575
|
+
}
|
|
1576
|
+
try {
|
|
1577
|
+
return new URL(url).hostname;
|
|
1578
|
+
} catch {
|
|
1579
|
+
return void 0;
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
var defineSelectorAdapter = (spec) => ({
|
|
1583
|
+
id: spec.id,
|
|
1584
|
+
matches: (context) => {
|
|
1585
|
+
const host = hostOf(context.url);
|
|
1586
|
+
return Boolean(host && spec.hosts?.test(host));
|
|
1587
|
+
},
|
|
1588
|
+
matchesFingerprint: (context) => {
|
|
1589
|
+
const signature = context.signature ?? EMPTY_SIGNATURE;
|
|
1590
|
+
return spec.fingerprint?.(signature) ?? false;
|
|
1591
|
+
},
|
|
1592
|
+
extract: (context) => {
|
|
1593
|
+
for (const selector of spec.content) {
|
|
1594
|
+
const content = tryContentSelector(selector, spec, context);
|
|
1595
|
+
if (content) {
|
|
1596
|
+
return content;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return void 0;
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
var tryContentSelector = (selector, spec, context) => {
|
|
1603
|
+
const found = selectAll(selector, context.hast);
|
|
1604
|
+
if (found.length === 0) {
|
|
1605
|
+
return void 0;
|
|
1606
|
+
}
|
|
1607
|
+
for (const element3 of found) {
|
|
1608
|
+
removeAll(element3, spec.remove);
|
|
1609
|
+
}
|
|
1610
|
+
const total = found.reduce((sum, element3) => sum + new MetricsCollector().textLength(element3), 0);
|
|
1611
|
+
if (total < (spec.minLength ?? DEFAULT_MIN_LENGTH)) {
|
|
1612
|
+
return void 0;
|
|
1613
|
+
}
|
|
1614
|
+
const decorated = spec.decorate?.(found, context);
|
|
1615
|
+
if (decorated) {
|
|
1616
|
+
return decorated;
|
|
1617
|
+
}
|
|
1618
|
+
return found.length === 1 ? found[0] : { type: "root", children: found };
|
|
1619
|
+
};
|
|
1620
|
+
var removeAll = (root, selectors) => {
|
|
1621
|
+
if (!selectors || selectors.length === 0) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const doomed = /* @__PURE__ */ new Set();
|
|
1625
|
+
for (const selector of selectors) {
|
|
1626
|
+
for (const element3 of selectAll(selector, root)) {
|
|
1627
|
+
doomed.add(element3);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
if (doomed.size > 0) {
|
|
1631
|
+
pruneInPlace(root, (node) => !(isElement(node) && doomed.has(node)));
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
var resolveAdapterByHost = (context, adapters) => {
|
|
1635
|
+
for (const adapter of adapters) {
|
|
1636
|
+
try {
|
|
1637
|
+
if (adapter.matches(context)) {
|
|
1638
|
+
return adapter;
|
|
1639
|
+
}
|
|
1640
|
+
} catch {
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return void 0;
|
|
1644
|
+
};
|
|
1645
|
+
var resolveAdapterByFingerprint = (context, adapters) => {
|
|
1646
|
+
for (const adapter of adapters) {
|
|
1647
|
+
try {
|
|
1648
|
+
if (adapter.matchesFingerprint?.(context)) {
|
|
1649
|
+
return adapter;
|
|
1650
|
+
}
|
|
1651
|
+
} catch {
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return void 0;
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
// src/adapters/sites/declarative.ts
|
|
1658
|
+
var COMMON_NOISE = [
|
|
1659
|
+
".sr-only",
|
|
1660
|
+
"[aria-hidden='true'][class*='icon']",
|
|
1661
|
+
"button",
|
|
1662
|
+
".social-share",
|
|
1663
|
+
".share-buttons",
|
|
1664
|
+
".newsletter-signup"
|
|
1665
|
+
];
|
|
1666
|
+
var SPECS = [
|
|
1667
|
+
{
|
|
1668
|
+
id: "github",
|
|
1669
|
+
hosts: /(^|\.)github\.com$/,
|
|
1670
|
+
// Issues and PRs expose a testid; repository landing pages render the README instead.
|
|
1671
|
+
content: ["[data-testid='issue-body']", "article.markdown-body", ".markdown-body"],
|
|
1672
|
+
remove: [".js-comment-edit-history", ".Details-content--hidden", "clipboard-copy", ...COMMON_NOISE],
|
|
1673
|
+
minLength: 30
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
id: "stackoverflow",
|
|
1677
|
+
hosts: /(^|\.)(stackoverflow\.com|stackexchange\.com|superuser\.com|serverfault\.com|askubuntu\.com)$/,
|
|
1678
|
+
content: ["#mainbar", ".question, .answer"],
|
|
1679
|
+
remove: [
|
|
1680
|
+
".js-post-menu",
|
|
1681
|
+
".post-menu",
|
|
1682
|
+
".user-info",
|
|
1683
|
+
".votecell",
|
|
1684
|
+
".comments-link",
|
|
1685
|
+
".js-add-link",
|
|
1686
|
+
...COMMON_NOISE
|
|
1687
|
+
],
|
|
1688
|
+
minLength: 100
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
id: "npm",
|
|
1692
|
+
hosts: /(^|\.)npmjs\.com$/,
|
|
1693
|
+
content: ["#readme", "#package-tab-readme", "[class*='markdown']"],
|
|
1694
|
+
remove: COMMON_NOISE,
|
|
1695
|
+
minLength: 50
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
id: "zenn",
|
|
1699
|
+
hosts: /(^|\.)zenn\.dev$/,
|
|
1700
|
+
content: [".znc"],
|
|
1701
|
+
remove: COMMON_NOISE,
|
|
1702
|
+
minLength: 50
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
id: "qiita",
|
|
1706
|
+
hosts: /(^|\.)qiita\.com$/,
|
|
1707
|
+
content: [".it-MdContent", "[class*='mdContent']", "article"],
|
|
1708
|
+
remove: [".code-copy", ...COMMON_NOISE],
|
|
1709
|
+
minLength: 50
|
|
1710
|
+
},
|
|
1711
|
+
{
|
|
1712
|
+
id: "medium",
|
|
1713
|
+
hosts: /(^|\.)medium\.com$/,
|
|
1714
|
+
// Medium also powers custom domains; the fingerprint catches those.
|
|
1715
|
+
fingerprint: (signature) => signature.meta.includes("al:android:app_name=medium"),
|
|
1716
|
+
content: ["article section", "article", "[data-testid='storyContent']"],
|
|
1717
|
+
remove: ["[data-testid='audioPlayButton']", "[role='separator']", ...COMMON_NOISE],
|
|
1718
|
+
minLength: 200
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
id: "substack",
|
|
1722
|
+
hosts: /(^|\.)substack\.com$/,
|
|
1723
|
+
// Substack's custom domains are far more common than *.substack.com.
|
|
1724
|
+
fingerprint: (signature) => signature.classes.has("available-content"),
|
|
1725
|
+
content: [".available-content", ".body.markup", ".post-content"],
|
|
1726
|
+
remove: [".subscription-widget-wrap", ".button-wrapper", ".paywall", ...COMMON_NOISE],
|
|
1727
|
+
minLength: 200
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
id: "note",
|
|
1731
|
+
hosts: /(^|\.)note\.com$/,
|
|
1732
|
+
content: [".note-common-styles__textnote-body", "[class*='textnote-body']"],
|
|
1733
|
+
remove: COMMON_NOISE,
|
|
1734
|
+
minLength: 50
|
|
1735
|
+
},
|
|
1736
|
+
{
|
|
1737
|
+
id: "hatena",
|
|
1738
|
+
hosts: /(^|\.)(hatenablog\.com|hatenadiary\.(com|jp)|hateblo\.jp|hatenastaff\.com)$/,
|
|
1739
|
+
fingerprint: (signature) => signature.classes.has("hatenablog-entry") || signature.classes.has("hatena-body"),
|
|
1740
|
+
content: [".entry-content"],
|
|
1741
|
+
remove: [".entry-footer", ".social-buttons", ".hatena-asin-detail", ...COMMON_NOISE],
|
|
1742
|
+
minLength: 100
|
|
1743
|
+
},
|
|
1744
|
+
{
|
|
1745
|
+
id: "wordpress",
|
|
1746
|
+
// Deliberately fingerprint-only: WordPress powers a large share of the web on its own
|
|
1747
|
+
// domains, so keying off a hostname would cover almost none of it.
|
|
1748
|
+
fingerprint: (signature) => signature.generator.includes("wordpress") || signature.classes.has("wp-block-post-content") || signature.classes.has("wp-singular"),
|
|
1749
|
+
content: [".wp-block-post-content", ".entry-content", ".post-content", "article .content"],
|
|
1750
|
+
remove: [".sharedaddy", ".jp-relatedposts", ".wp-block-comments", "#comments", ...COMMON_NOISE],
|
|
1751
|
+
minLength: 150
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
id: "wikipedia",
|
|
1755
|
+
hosts: /(^|\.)(wikipedia\.org|wikimedia\.org|wikibooks\.org|wiktionary\.org|wikisource\.org)$/,
|
|
1756
|
+
fingerprint: (signature) => signature.classes.has("mw-parser-output"),
|
|
1757
|
+
content: [".mw-parser-output", "#mw-content-text"],
|
|
1758
|
+
remove: [
|
|
1759
|
+
".mw-editsection",
|
|
1760
|
+
".navbox",
|
|
1761
|
+
".navbox-inner",
|
|
1762
|
+
".vertical-navbox",
|
|
1763
|
+
".sidebar",
|
|
1764
|
+
".side-box",
|
|
1765
|
+
".metadata",
|
|
1766
|
+
".reflist",
|
|
1767
|
+
".refbegin",
|
|
1768
|
+
".sistersitebox",
|
|
1769
|
+
".ambox",
|
|
1770
|
+
".hatnote",
|
|
1771
|
+
".shortdescription",
|
|
1772
|
+
".mw-jump-link",
|
|
1773
|
+
".printfooter",
|
|
1774
|
+
".catlinks",
|
|
1775
|
+
"#toc",
|
|
1776
|
+
".toc",
|
|
1777
|
+
"table.mbox-small",
|
|
1778
|
+
...COMMON_NOISE
|
|
1779
|
+
],
|
|
1780
|
+
minLength: 200
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
id: "reddit",
|
|
1784
|
+
hosts: /(^|\.)reddit\.com$/,
|
|
1785
|
+
content: ["shreddit-post [slot='text-body']", "[data-test-id='post-content']", ".Post"],
|
|
1786
|
+
remove: ["faceplate-tracker[noun='comment_action_bar']", "shreddit-comment-action-row", ...COMMON_NOISE],
|
|
1787
|
+
// Reddit renders most of a thread into shadow roots that never reach the serialised HTML,
|
|
1788
|
+
// so a weak match here is worse than generic extraction. Decline unless a substantial
|
|
1789
|
+
// post body is actually present in the document.
|
|
1790
|
+
minLength: 600
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
id: "docusaurus",
|
|
1794
|
+
fingerprint: (signature) => signature.classes.has("theme-doc-markdown") || signature.classes.has("docusaurus-mt-lg"),
|
|
1795
|
+
content: [".theme-doc-markdown", "article .markdown"],
|
|
1796
|
+
remove: [".theme-doc-toc-mobile", ".pagination-nav", ".theme-doc-version-banner", ...COMMON_NOISE],
|
|
1797
|
+
minLength: 150
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
id: "vitepress",
|
|
1801
|
+
fingerprint: (signature) => signature.classes.has("vp-doc") || signature.classes.has("VPDoc"),
|
|
1802
|
+
content: [".vp-doc > div", ".vp-doc", ".VPDoc .content-container"],
|
|
1803
|
+
remove: [".VPDocFooter", ".vp-doc-footer", ".header-anchor", ...COMMON_NOISE],
|
|
1804
|
+
minLength: 150
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
id: "mkdocs",
|
|
1808
|
+
fingerprint: (signature) => signature.classes.has("md-content__inner"),
|
|
1809
|
+
content: [".md-content__inner", "[data-md-component='content']"],
|
|
1810
|
+
remove: [".md-source-file", ".md-feedback", ".headerlink", ...COMMON_NOISE],
|
|
1811
|
+
minLength: 150
|
|
1812
|
+
},
|
|
1813
|
+
{
|
|
1814
|
+
id: "readthedocs",
|
|
1815
|
+
fingerprint: (signature) => signature.classes.has("rst-content"),
|
|
1816
|
+
content: [".rst-content", "[role='main']"],
|
|
1817
|
+
remove: [".headerlink", ".rst-footer-buttons", ".wy-breadcrumbs", ...COMMON_NOISE],
|
|
1818
|
+
minLength: 150
|
|
1819
|
+
},
|
|
1820
|
+
{
|
|
1821
|
+
id: "gitbook",
|
|
1822
|
+
fingerprint: (signature) => signature.classes.has("gitbook-root"),
|
|
1823
|
+
content: ["[data-testid='page.contentEditor']", "main article"],
|
|
1824
|
+
remove: COMMON_NOISE,
|
|
1825
|
+
minLength: 150
|
|
1826
|
+
}
|
|
1827
|
+
];
|
|
1828
|
+
var DECLARATIVE_ADAPTERS = SPECS.map(defineSelectorAdapter);
|
|
1829
|
+
|
|
1830
|
+
// src/adapters/sites/hackernews.ts
|
|
1831
|
+
import { select as select4, selectAll as selectAll2 } from "hast-util-select";
|
|
1832
|
+
var MAX_QUOTE_DEPTH = 6;
|
|
1833
|
+
var text3 = (value) => ({ type: "text", value });
|
|
1834
|
+
var element = (tagName, children) => ({
|
|
1835
|
+
type: "element",
|
|
1836
|
+
tagName,
|
|
1837
|
+
properties: {},
|
|
1838
|
+
children
|
|
1839
|
+
});
|
|
1840
|
+
var commentDepth = (row) => {
|
|
1841
|
+
const cell = select4("td.ind", row);
|
|
1842
|
+
if (!cell) {
|
|
1843
|
+
return 0;
|
|
1844
|
+
}
|
|
1845
|
+
const indent = numericProperty(cell, "indent");
|
|
1846
|
+
if (indent !== void 0) {
|
|
1847
|
+
return indent;
|
|
1848
|
+
}
|
|
1849
|
+
const spacer = select4("img", cell);
|
|
1850
|
+
const width = spacer ? numericProperty(spacer, "width") : void 0;
|
|
1851
|
+
return width === void 0 ? 0 : Math.round(width / 40);
|
|
1852
|
+
};
|
|
1853
|
+
var nest = (content, depth) => {
|
|
1854
|
+
let current = content;
|
|
1855
|
+
for (let level = 0; level < Math.min(depth, MAX_QUOTE_DEPTH); level++) {
|
|
1856
|
+
current = [element("blockquote", current)];
|
|
1857
|
+
}
|
|
1858
|
+
return current[0];
|
|
1859
|
+
};
|
|
1860
|
+
var storyNodes = (root) => {
|
|
1861
|
+
const nodes = [];
|
|
1862
|
+
const titleLink = select4(".titleline > a", root);
|
|
1863
|
+
if (titleLink) {
|
|
1864
|
+
nodes.push(element("h1", [...titleLink.children]));
|
|
1865
|
+
const href = stringProperty(titleLink, "href");
|
|
1866
|
+
if (href && /^https?:/.test(href)) {
|
|
1867
|
+
nodes.push(element("p", [{ ...titleLink, children: [text3(href)] }]));
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
const topText = select4(".toptext", root);
|
|
1871
|
+
if (topText) {
|
|
1872
|
+
nodes.push(element("div", [...topText.children]));
|
|
1873
|
+
}
|
|
1874
|
+
return nodes;
|
|
1875
|
+
};
|
|
1876
|
+
var commentNodes = (root) => {
|
|
1877
|
+
const nodes = [];
|
|
1878
|
+
for (const row of selectAll2("tr.comtr, tr.athing.comtr", root)) {
|
|
1879
|
+
const body = select4(".commtext", row);
|
|
1880
|
+
if (!body) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
const author = select4(".hnuser", row);
|
|
1884
|
+
const header = author ? [element("strong", [...author.children])] : [];
|
|
1885
|
+
const content = [
|
|
1886
|
+
...header.length > 0 ? [element("p", header)] : [],
|
|
1887
|
+
element("div", [...body.children])
|
|
1888
|
+
];
|
|
1889
|
+
nodes.push(nest(content, commentDepth(row)));
|
|
1890
|
+
}
|
|
1891
|
+
return nodes;
|
|
1892
|
+
};
|
|
1893
|
+
var hackernewsAdapter = {
|
|
1894
|
+
id: "hackernews",
|
|
1895
|
+
matches: ({ url }) => {
|
|
1896
|
+
if (!url) {
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
try {
|
|
1900
|
+
return /(^|\.)ycombinator\.com$/.test(new URL(url).hostname);
|
|
1901
|
+
} catch {
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
},
|
|
1905
|
+
matchesFingerprint: ({ signature }) => Boolean(signature?.ids.has("hnmain") || signature?.classes.has("fatitem")),
|
|
1906
|
+
extract: ({ hast }) => {
|
|
1907
|
+
const story = storyNodes(hast);
|
|
1908
|
+
const comments = commentNodes(hast);
|
|
1909
|
+
if (story.length === 0 && comments.length === 0) {
|
|
1910
|
+
return void 0;
|
|
1911
|
+
}
|
|
1912
|
+
const children = [...story];
|
|
1913
|
+
if (comments.length > 0) {
|
|
1914
|
+
children.push(element("h2", [text3("Comments")]));
|
|
1915
|
+
children.push(...comments);
|
|
1916
|
+
}
|
|
1917
|
+
return { type: "root", children };
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
|
|
1921
|
+
// src/adapters/sites/youtube.ts
|
|
1922
|
+
var findObjectEnd = (source, start) => {
|
|
1923
|
+
let depth = 0;
|
|
1924
|
+
let inString = false;
|
|
1925
|
+
let escaped = false;
|
|
1926
|
+
for (let index = start; index < source.length; index++) {
|
|
1927
|
+
const character = source[index];
|
|
1928
|
+
if (escaped) {
|
|
1929
|
+
escaped = false;
|
|
1930
|
+
} else if (character === "\\") {
|
|
1931
|
+
escaped = true;
|
|
1932
|
+
} else if (character === '"') {
|
|
1933
|
+
inString = !inString;
|
|
1934
|
+
} else if (!inString) {
|
|
1935
|
+
if (character === "{") {
|
|
1936
|
+
depth += 1;
|
|
1937
|
+
} else if (character === "}" && --depth === 0) {
|
|
1938
|
+
return index + 1;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
return -1;
|
|
1943
|
+
};
|
|
1944
|
+
var extractAssignedJson = (source, name) => {
|
|
1945
|
+
const markerIndex = source.indexOf(`${name} =`);
|
|
1946
|
+
if (markerIndex === -1) {
|
|
1947
|
+
return void 0;
|
|
1948
|
+
}
|
|
1949
|
+
const start = source.indexOf("{", markerIndex);
|
|
1950
|
+
if (start === -1) {
|
|
1951
|
+
return void 0;
|
|
1952
|
+
}
|
|
1953
|
+
const end = findObjectEnd(source, start);
|
|
1954
|
+
if (end === -1) {
|
|
1955
|
+
return void 0;
|
|
1956
|
+
}
|
|
1957
|
+
try {
|
|
1958
|
+
return JSON.parse(source.slice(start, end));
|
|
1959
|
+
} catch {
|
|
1960
|
+
return void 0;
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
var scriptText = (root) => collectElements(root, (element3) => element3.tagName === "script").map(
|
|
1964
|
+
(script) => script.children.map((child) => child.type === "text" ? child.value : "").join("")
|
|
1965
|
+
);
|
|
1966
|
+
var findPlayerResponse = (root) => {
|
|
1967
|
+
for (const source of scriptText(root)) {
|
|
1968
|
+
if (!source.includes("ytInitialPlayerResponse")) {
|
|
1969
|
+
continue;
|
|
1970
|
+
}
|
|
1971
|
+
const parsed = extractAssignedJson(source, "ytInitialPlayerResponse");
|
|
1972
|
+
if (parsed && typeof parsed === "object") {
|
|
1973
|
+
return parsed;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return void 0;
|
|
1977
|
+
};
|
|
1978
|
+
var youtubeCaptionTracks = (root) => {
|
|
1979
|
+
const player = findPlayerResponse(root);
|
|
1980
|
+
const renderer = player?.captions?.playerCaptionsTracklistRenderer;
|
|
1981
|
+
const tracks = renderer?.captionTracks;
|
|
1982
|
+
if (!Array.isArray(tracks)) {
|
|
1983
|
+
return [];
|
|
1984
|
+
}
|
|
1985
|
+
return tracks.flatMap((track) => {
|
|
1986
|
+
const url = typeof track.baseUrl === "string" ? track.baseUrl : void 0;
|
|
1987
|
+
const languageCode = typeof track.languageCode === "string" ? track.languageCode : void 0;
|
|
1988
|
+
if (!(url && languageCode)) {
|
|
1989
|
+
return [];
|
|
1990
|
+
}
|
|
1991
|
+
const name = track.name?.simpleText;
|
|
1992
|
+
return [
|
|
1993
|
+
{
|
|
1994
|
+
url,
|
|
1995
|
+
languageCode,
|
|
1996
|
+
name: typeof name === "string" ? name : void 0,
|
|
1997
|
+
isAutomatic: track.kind === "asr"
|
|
1998
|
+
}
|
|
1999
|
+
];
|
|
2000
|
+
});
|
|
2001
|
+
};
|
|
2002
|
+
var text4 = (value) => ({ type: "text", value });
|
|
2003
|
+
var element2 = (tagName, children) => ({
|
|
2004
|
+
type: "element",
|
|
2005
|
+
tagName,
|
|
2006
|
+
properties: {},
|
|
2007
|
+
children
|
|
2008
|
+
});
|
|
2009
|
+
var heading = (depth, value) => element2(`h${depth}`, [text4(value)]);
|
|
2010
|
+
var paragraph = (value) => element2("p", [text4(value)]);
|
|
2011
|
+
var descriptionNodes = (description) => description.split(/\n{2,}/).map((block) => block.trim()).filter((block) => block.length > 0).map(
|
|
2012
|
+
(block) => element2(
|
|
2013
|
+
"p",
|
|
2014
|
+
block.split("\n").flatMap((line, index) => index === 0 ? [text4(line)] : [element2("br", []), text4(line)])
|
|
2015
|
+
)
|
|
2016
|
+
);
|
|
2017
|
+
var formatDuration = (seconds) => {
|
|
2018
|
+
const hours = Math.floor(seconds / 3600);
|
|
2019
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
2020
|
+
const remainder = seconds % 60;
|
|
2021
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
2022
|
+
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(remainder)}` : `${minutes}:${pad(remainder)}`;
|
|
2023
|
+
};
|
|
2024
|
+
var metadataList = (details) => {
|
|
2025
|
+
const items = [];
|
|
2026
|
+
if (details.author) {
|
|
2027
|
+
items.push(element2("li", [text4(`Channel: ${details.author}`)]));
|
|
2028
|
+
}
|
|
2029
|
+
const seconds = Number(details.lengthSeconds);
|
|
2030
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
2031
|
+
items.push(element2("li", [text4(`Duration: ${formatDuration(seconds)}`)]));
|
|
2032
|
+
}
|
|
2033
|
+
const views = Number(details.viewCount);
|
|
2034
|
+
if (Number.isFinite(views) && views > 0) {
|
|
2035
|
+
items.push(element2("li", [text4(`Views: ${views.toLocaleString("en-US")}`)]));
|
|
2036
|
+
}
|
|
2037
|
+
return items.length > 0 ? element2("ul", items) : void 0;
|
|
2038
|
+
};
|
|
2039
|
+
var youtubeAdapter = {
|
|
2040
|
+
id: "youtube",
|
|
2041
|
+
matches: ({ url }) => {
|
|
2042
|
+
if (!url) {
|
|
2043
|
+
return false;
|
|
2044
|
+
}
|
|
2045
|
+
try {
|
|
2046
|
+
return /(^|\.)(youtube\.com|youtu\.be|youtube-nocookie\.com)$/.test(new URL(url).hostname);
|
|
2047
|
+
} catch {
|
|
2048
|
+
return false;
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
// Scanning every script body for the JSON blob would cost a full traversal on every page, so
|
|
2052
|
+
// the fallback keys off markers the signature already collected.
|
|
2053
|
+
matchesFingerprint: ({ signature }) => Boolean(signature?.ids.has("movie_player") || signature?.classes.has("ytd-app")),
|
|
2054
|
+
extract: ({ hast }) => {
|
|
2055
|
+
const player = findPlayerResponse(hast);
|
|
2056
|
+
const details = player?.videoDetails;
|
|
2057
|
+
if (!details?.title) {
|
|
2058
|
+
return void 0;
|
|
2059
|
+
}
|
|
2060
|
+
const children = [heading(1, details.title)];
|
|
2061
|
+
const metadata = metadataList(details);
|
|
2062
|
+
if (metadata) {
|
|
2063
|
+
children.push(metadata);
|
|
2064
|
+
}
|
|
2065
|
+
const description = details.shortDescription?.trim();
|
|
2066
|
+
if (description) {
|
|
2067
|
+
children.push(heading(2, "Description"));
|
|
2068
|
+
children.push(...descriptionNodes(description));
|
|
2069
|
+
}
|
|
2070
|
+
const tracks = youtubeCaptionTracks(hast);
|
|
2071
|
+
if (tracks.length > 0) {
|
|
2072
|
+
const languages2 = [...new Set(tracks.map((track) => track.languageCode))].sort();
|
|
2073
|
+
children.push(paragraph(`Captions available: ${languages2.join(", ")}`));
|
|
2074
|
+
}
|
|
2075
|
+
return { type: "root", children };
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2079
|
+
// src/adapters/index.ts
|
|
2080
|
+
var BUILTIN_ADAPTERS = [youtubeAdapter, hackernewsAdapter, ...DECLARATIVE_ADAPTERS];
|
|
2081
|
+
var MIN_ADAPTER_TEXT = 40;
|
|
2082
|
+
var runAdapters = (context, adapters = BUILTIN_ADAPTERS) => {
|
|
2083
|
+
let matched = resolveAdapterByHost(context, adapters);
|
|
2084
|
+
let withSignature = context;
|
|
2085
|
+
if (!matched) {
|
|
2086
|
+
withSignature = { ...context, signature: context.signature ?? buildPageSignature(context.hast) };
|
|
2087
|
+
matched = resolveAdapterByFingerprint(withSignature, adapters);
|
|
2088
|
+
}
|
|
2089
|
+
const adapter = matched;
|
|
2090
|
+
if (!adapter) {
|
|
2091
|
+
return void 0;
|
|
2092
|
+
}
|
|
2093
|
+
const target = withSignature.owned ? withSignature : { ...withSignature, hast: cloneHast(withSignature.hast), owned: true };
|
|
2094
|
+
let extracted;
|
|
2095
|
+
try {
|
|
2096
|
+
extracted = adapter.extract(target);
|
|
2097
|
+
} catch {
|
|
2098
|
+
return void 0;
|
|
2099
|
+
}
|
|
2100
|
+
if (!extracted) {
|
|
2101
|
+
return void 0;
|
|
2102
|
+
}
|
|
2103
|
+
stripNonContent(extracted);
|
|
2104
|
+
return new MetricsCollector().textLength(extracted) >= MIN_ADAPTER_TEXT ? extracted : void 0;
|
|
2105
|
+
};
|
|
186
2106
|
|
|
187
|
-
// src/extractors/
|
|
188
|
-
var
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
2107
|
+
// src/extractors/lib/score.ts
|
|
2108
|
+
var TAG_BASE_SCORE = {
|
|
2109
|
+
div: 5,
|
|
2110
|
+
pre: 3,
|
|
2111
|
+
td: 3,
|
|
2112
|
+
blockquote: 3,
|
|
2113
|
+
article: 10,
|
|
2114
|
+
main: 10,
|
|
2115
|
+
section: 5,
|
|
2116
|
+
address: -3,
|
|
2117
|
+
ol: -3,
|
|
2118
|
+
ul: -3,
|
|
2119
|
+
dl: -3,
|
|
2120
|
+
dd: -3,
|
|
2121
|
+
dt: -3,
|
|
2122
|
+
li: -3,
|
|
2123
|
+
form: -3,
|
|
2124
|
+
h1: -5,
|
|
2125
|
+
h2: -5,
|
|
2126
|
+
h3: -5,
|
|
2127
|
+
h4: -5,
|
|
2128
|
+
h5: -5,
|
|
2129
|
+
h6: -5,
|
|
2130
|
+
th: -5
|
|
2131
|
+
};
|
|
2132
|
+
var MAX_ANCESTOR_DEPTH = 5;
|
|
2133
|
+
var MIN_SCOREABLE_TEXT = 25;
|
|
2134
|
+
var buildScoringContext = (root, collector, lang) => {
|
|
2135
|
+
const parents = /* @__PURE__ */ new WeakMap();
|
|
2136
|
+
const scoreable = [];
|
|
2137
|
+
let hasGeometry = false;
|
|
2138
|
+
walk(root, (node, parent) => {
|
|
2139
|
+
if (parent) {
|
|
2140
|
+
parents.set(node, parent);
|
|
2141
|
+
}
|
|
2142
|
+
if (!isElement(node)) {
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
if (!hasGeometry && numericProperty(node, "data-rheight") !== void 0) {
|
|
2146
|
+
hasGeometry = true;
|
|
2147
|
+
}
|
|
2148
|
+
if (isParagraphLike(node)) {
|
|
2149
|
+
scoreable.push(node);
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
return { collector, parents, lang, hasGeometry, scoreable };
|
|
2153
|
+
};
|
|
2154
|
+
var classWeight = (element3) => {
|
|
2155
|
+
let weight = 0;
|
|
2156
|
+
const id = stringProperty(element3, "id") ?? "";
|
|
2157
|
+
const classes = classList(element3).join(" ");
|
|
2158
|
+
if (REGEXPS.negative.test(classes)) {
|
|
2159
|
+
weight -= 25;
|
|
192
2160
|
}
|
|
193
|
-
|
|
2161
|
+
if (REGEXPS.positive.test(classes)) {
|
|
2162
|
+
weight += 25;
|
|
2163
|
+
}
|
|
2164
|
+
if (REGEXPS.negative.test(id)) {
|
|
2165
|
+
weight -= 25;
|
|
2166
|
+
}
|
|
2167
|
+
if (REGEXPS.positive.test(id)) {
|
|
2168
|
+
weight += 25;
|
|
2169
|
+
}
|
|
2170
|
+
return weight;
|
|
194
2171
|
};
|
|
195
|
-
var
|
|
196
|
-
|
|
197
|
-
|
|
2172
|
+
var semanticBonus = (element3) => {
|
|
2173
|
+
let bonus = 0;
|
|
2174
|
+
if (element3.tagName === "article") {
|
|
2175
|
+
bonus += 60;
|
|
198
2176
|
}
|
|
199
|
-
|
|
2177
|
+
if (element3.tagName === "main") {
|
|
2178
|
+
bonus += 50;
|
|
2179
|
+
}
|
|
2180
|
+
const role = stringProperty(element3, "role");
|
|
2181
|
+
if (role && CONTENT_ROLES.has(role)) {
|
|
2182
|
+
bonus += 40;
|
|
2183
|
+
}
|
|
2184
|
+
const itemprop = stringProperty(element3, "itemprop");
|
|
2185
|
+
if (itemprop?.includes("articleBody")) {
|
|
2186
|
+
bonus += 80;
|
|
2187
|
+
}
|
|
2188
|
+
const match = matchString(element3);
|
|
2189
|
+
if (/(^|[\s_-])(markdown|prose|article-?content|post-?content|entry-?content|doc-?content|md-?content)([\s_-]|$)/i.test(
|
|
2190
|
+
match
|
|
2191
|
+
)) {
|
|
2192
|
+
bonus += 35;
|
|
2193
|
+
}
|
|
2194
|
+
return bonus;
|
|
200
2195
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
var REGEXPS = {
|
|
205
|
-
hidden: /hidden|invisible|fallback-image/i,
|
|
206
|
-
byline: /byline|author|dateline|writtenby|p-author/i,
|
|
207
|
-
specialUnlikelyCandidates: /frb-|uls-menu|language-link/i,
|
|
208
|
-
unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|tooltip|disqus|extra|footer|gdpr|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote|speechify-ignore|avatar/i,
|
|
209
|
-
okMaybeItsaCandidate: /and|article|body|column|content|main|shadow|code/i
|
|
210
|
-
};
|
|
211
|
-
var BODY_SELECTORS = ["article", "#article", ".article_body", ".article-body", "#content", ".entry"];
|
|
212
|
-
var PARAGRAPH_TAGS = ["a", "p", "div", "section", "article", "main", "ul", "ol", "li"];
|
|
213
|
-
var CONTENTABLE_TAGS = ["article", "main", "section", "h1", "h2", "h3", "h4", "h5", "h6", "p"];
|
|
214
|
-
var TEXTABLE_TAGS = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "li", "ul"];
|
|
215
|
-
var BASE_MINIMAL_LENGTH = { ja: 200, en: 500 };
|
|
216
|
-
var metadataFilter = (node) => {
|
|
217
|
-
return !(["comment", "doctype"].includes(node.type) || node.type === "element" && ["script", "style", "link", "meta", "noscript", "svg", "title"].includes(node.tagName));
|
|
218
|
-
};
|
|
219
|
-
var universalElementFilter = (node) => {
|
|
220
|
-
if (node.type !== "element") {
|
|
221
|
-
return true;
|
|
2196
|
+
var geometryMultiplier = (element3, context) => {
|
|
2197
|
+
if (!context.hasGeometry) {
|
|
2198
|
+
return 1;
|
|
222
2199
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
2200
|
+
const height = numericProperty(element3, "data-rheight");
|
|
2201
|
+
const width = numericProperty(element3, "data-rwidth");
|
|
2202
|
+
if (height === void 0 || width === void 0) {
|
|
2203
|
+
return 1;
|
|
226
2204
|
}
|
|
227
|
-
if (
|
|
228
|
-
return
|
|
2205
|
+
if (height === 0 || width === 0) {
|
|
2206
|
+
return 0;
|
|
229
2207
|
}
|
|
230
|
-
if (
|
|
231
|
-
return
|
|
2208
|
+
if (width < 220) {
|
|
2209
|
+
return 0.35;
|
|
232
2210
|
}
|
|
233
|
-
|
|
234
|
-
|
|
2211
|
+
return 1;
|
|
2212
|
+
};
|
|
2213
|
+
var paragraphScore = (textLength, commas, densityDivisor) => 1 + commas + Math.min(Math.floor(textLength / densityDivisor), 3);
|
|
2214
|
+
var ancestorWeight = (level) => {
|
|
2215
|
+
if (level === 0) {
|
|
2216
|
+
return 1;
|
|
235
2217
|
}
|
|
236
|
-
if (
|
|
237
|
-
return
|
|
2218
|
+
if (level === 1) {
|
|
2219
|
+
return 2;
|
|
238
2220
|
}
|
|
239
|
-
|
|
2221
|
+
return level * 3;
|
|
2222
|
+
};
|
|
2223
|
+
var isParagraphLike = (element3) => {
|
|
2224
|
+
if (!SCOREABLE_TAGS.has(element3.tagName)) {
|
|
240
2225
|
return false;
|
|
241
2226
|
}
|
|
242
|
-
if (
|
|
243
|
-
return
|
|
2227
|
+
if (element3.tagName !== "div" && element3.tagName !== "section") {
|
|
2228
|
+
return true;
|
|
244
2229
|
}
|
|
245
|
-
|
|
246
|
-
|
|
2230
|
+
return !element3.children.some((child) => isElement(child) && CANDIDATE_TAGS.has(child.tagName));
|
|
2231
|
+
};
|
|
2232
|
+
var scoreCandidates = (_root, context) => {
|
|
2233
|
+
const { collector, parents } = context;
|
|
2234
|
+
const scores = /* @__PURE__ */ new Map();
|
|
2235
|
+
const densityDivisor = minContentLength(context.lang) / 4;
|
|
2236
|
+
for (const node of context.scoreable) {
|
|
2237
|
+
const metrics = collector.metrics(node);
|
|
2238
|
+
if (metrics.text < MIN_SCOREABLE_TEXT) {
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
const contribution = paragraphScore(metrics.text, metrics.commas, densityDivisor);
|
|
2242
|
+
let current = node;
|
|
2243
|
+
for (let level = 0; level < MAX_ANCESTOR_DEPTH && current; level++) {
|
|
2244
|
+
const parent = parents.get(current);
|
|
2245
|
+
if (!(parent && isElement(parent))) {
|
|
2246
|
+
break;
|
|
2247
|
+
}
|
|
2248
|
+
if (CANDIDATE_TAGS.has(parent.tagName) || parent.tagName === "article" || parent.tagName === "main") {
|
|
2249
|
+
const base = scores.get(parent) ?? (TAG_BASE_SCORE[parent.tagName] ?? 0) + classWeight(parent) + semanticBonus(parent);
|
|
2250
|
+
scores.set(parent, base + contribution / ancestorWeight(level));
|
|
2251
|
+
}
|
|
2252
|
+
current = parent;
|
|
2253
|
+
}
|
|
247
2254
|
}
|
|
248
|
-
|
|
2255
|
+
const candidates = [];
|
|
2256
|
+
for (const [element3, rawScore] of scores) {
|
|
2257
|
+
const density = collector.linkDensity(element3);
|
|
2258
|
+
const score = rawScore * (1 - density) * geometryMultiplier(element3, context);
|
|
2259
|
+
if (score > 0) {
|
|
2260
|
+
candidates.push({ element: element3, score });
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
2264
|
+
return candidates;
|
|
249
2265
|
};
|
|
250
|
-
var
|
|
251
|
-
|
|
252
|
-
|
|
2266
|
+
var collectArticleNodes = (top, candidates, context) => {
|
|
2267
|
+
const { collector, parents } = context;
|
|
2268
|
+
const parent = parents.get(top.element);
|
|
2269
|
+
if (!(parent && isElement(parent))) {
|
|
2270
|
+
return [top.element];
|
|
253
2271
|
}
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
2272
|
+
const scoreOf = new Map(candidates.map((candidate) => [candidate.element, candidate.score]));
|
|
2273
|
+
const threshold = Math.max(10, top.score * 0.2);
|
|
2274
|
+
const selected = [];
|
|
2275
|
+
for (const child of parent.children) {
|
|
2276
|
+
if (!isElement(child)) {
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
if (child === top.element) {
|
|
2280
|
+
selected.push(child);
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
const siblingScore = scoreOf.get(child) ?? 0;
|
|
2284
|
+
if (siblingScore >= threshold) {
|
|
2285
|
+
selected.push(child);
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
if (child.tagName === "p" || child.tagName === "section") {
|
|
2289
|
+
const metrics = collector.metrics(child);
|
|
2290
|
+
const density = collector.linkDensity(child);
|
|
2291
|
+
if (metrics.text > 80 && density < 0.25) {
|
|
2292
|
+
selected.push(child);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
257
2295
|
}
|
|
258
|
-
|
|
259
|
-
|
|
2296
|
+
return selected.length > 0 ? selected : [top.element];
|
|
2297
|
+
};
|
|
2298
|
+
var MAX_CLIMB_HOPS = 5;
|
|
2299
|
+
var MIN_CLIMB_RELATIVE_GAIN = 0.08;
|
|
2300
|
+
var MIN_CLIMB_ABSOLUTE_GAIN = 500;
|
|
2301
|
+
var MAX_CLIMB_GAIN_LINK_DENSITY = 1 / 3;
|
|
2302
|
+
var CLIMB_SCORE_FLOOR = 1 / 3;
|
|
2303
|
+
var climbToArticleBoundary = (top, candidates, context) => {
|
|
2304
|
+
const { collector, parents } = context;
|
|
2305
|
+
const scoreOf = new Map(candidates.map((candidate) => [candidate.element, candidate.score]));
|
|
2306
|
+
const scoreFloor = top.score * CLIMB_SCORE_FLOOR;
|
|
2307
|
+
let current = top.element;
|
|
2308
|
+
for (let hop = 0; hop < MAX_CLIMB_HOPS; hop++) {
|
|
2309
|
+
const parent = parents.get(current);
|
|
2310
|
+
if (!(parent && isElement(parent))) {
|
|
2311
|
+
break;
|
|
2312
|
+
}
|
|
2313
|
+
const parentScore = scoreOf.get(parent);
|
|
2314
|
+
if (parentScore === void 0 || parentScore < scoreFloor) {
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
const currentMetrics = collector.metrics(current);
|
|
2318
|
+
const parentMetrics = collector.metrics(parent);
|
|
2319
|
+
const gainedText = parentMetrics.text - currentMetrics.text;
|
|
2320
|
+
if (gainedText < Math.max(MIN_CLIMB_ABSOLUTE_GAIN, currentMetrics.text * MIN_CLIMB_RELATIVE_GAIN)) {
|
|
2321
|
+
break;
|
|
2322
|
+
}
|
|
2323
|
+
const gainedLink = parentMetrics.link - currentMetrics.link;
|
|
2324
|
+
if (gainedLink / gainedText > MAX_CLIMB_GAIN_LINK_DENSITY) {
|
|
2325
|
+
break;
|
|
2326
|
+
}
|
|
2327
|
+
current = parent;
|
|
2328
|
+
}
|
|
2329
|
+
return current;
|
|
2330
|
+
};
|
|
2331
|
+
|
|
2332
|
+
// src/extractors/lib/terminators.ts
|
|
2333
|
+
var TERMINATOR_PATTERNS = [
|
|
2334
|
+
/^was this (page|article|document) helpful/i,
|
|
2335
|
+
/^thanks for the feedback/i,
|
|
2336
|
+
/^(this page was )?last (modified|updated) (on|at|:)?\s/i,
|
|
2337
|
+
/^learn how to contribute/i,
|
|
2338
|
+
/^help (us )?improve\b/i,
|
|
2339
|
+
// The stranded Yes/No pair of a feedback widget whose question was removed separately.
|
|
2340
|
+
/^yes ?no$/i,
|
|
2341
|
+
/^(related (articles?|posts?|stories|content)|you might also like|recommended for you|more from|read next)$/i,
|
|
2342
|
+
/^(comments?|leave a (comment|reply)|join the (conversation|discussion))$/i,
|
|
2343
|
+
/^(この(ページ|記事)は(参考|役に立ち)ました?か)/,
|
|
2344
|
+
/^(関連記事|あわせて読みたい|こちらもおすすめ|おすすめ記事)$/,
|
|
2345
|
+
/^(コメントを(残す|書く)|コメント一覧)$/,
|
|
2346
|
+
/^最終更新日?[::\s]/
|
|
2347
|
+
];
|
|
2348
|
+
var TAIL_START_FRACTION = 0.8;
|
|
2349
|
+
var MAX_TERMINATOR_BLOCK_LENGTH = 500;
|
|
2350
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set(["p", "div", "section", "footer", "aside", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol"]);
|
|
2351
|
+
var isTerminatorBlock = (element3, start, tailStart) => {
|
|
2352
|
+
if (!BLOCK_TAGS.has(element3.tagName) || start < tailStart) {
|
|
260
2353
|
return false;
|
|
261
2354
|
}
|
|
262
|
-
|
|
2355
|
+
const text6 = textOf(element3).replace(/\s+/g, " ").trim();
|
|
2356
|
+
if (text6.length === 0 || text6.length > MAX_TERMINATOR_BLOCK_LENGTH) {
|
|
263
2357
|
return false;
|
|
264
2358
|
}
|
|
265
|
-
return
|
|
2359
|
+
return TERMINATOR_PATTERNS.some((pattern) => pattern.test(text6));
|
|
266
2360
|
};
|
|
267
|
-
var
|
|
268
|
-
if (node.type
|
|
269
|
-
return
|
|
2361
|
+
var textOf = (node) => {
|
|
2362
|
+
if (node.type === "text") {
|
|
2363
|
+
return node.value;
|
|
270
2364
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
2365
|
+
if (!isParent(node)) {
|
|
2366
|
+
return "";
|
|
2367
|
+
}
|
|
2368
|
+
let text6 = "";
|
|
2369
|
+
for (const child of node.children) {
|
|
2370
|
+
text6 += textOf(child);
|
|
274
2371
|
}
|
|
275
|
-
|
|
2372
|
+
return text6;
|
|
2373
|
+
};
|
|
2374
|
+
var truncateTrailingBoilerplate = (tree) => {
|
|
2375
|
+
const starts = /* @__PURE__ */ new Map();
|
|
2376
|
+
let total = 0;
|
|
2377
|
+
const measure = (node) => {
|
|
2378
|
+
starts.set(node, total);
|
|
2379
|
+
if (node.type === "text") {
|
|
2380
|
+
total += node.value.trim().length;
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
if (isParent(node)) {
|
|
2384
|
+
for (const child of node.children) {
|
|
2385
|
+
measure(child);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
};
|
|
2389
|
+
measure(tree);
|
|
2390
|
+
if (total === 0) {
|
|
276
2391
|
return false;
|
|
277
2392
|
}
|
|
278
|
-
|
|
2393
|
+
const tailStart = total * TAIL_START_FRACTION;
|
|
2394
|
+
let cutoff = -1;
|
|
2395
|
+
const find = (node) => {
|
|
2396
|
+
if (cutoff !== -1 || !isParent(node)) {
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
for (const child of node.children) {
|
|
2400
|
+
if (cutoff !== -1) {
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
if (!isElement(child)) {
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
const start = starts.get(child) ?? 0;
|
|
2407
|
+
if (isTerminatorBlock(child, start, tailStart)) {
|
|
2408
|
+
cutoff = start;
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
find(child);
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
2414
|
+
find(tree);
|
|
2415
|
+
if (cutoff === -1) {
|
|
279
2416
|
return false;
|
|
280
2417
|
}
|
|
2418
|
+
pruneInPlace(tree, (node) => {
|
|
2419
|
+
if (node === tree) {
|
|
2420
|
+
return true;
|
|
2421
|
+
}
|
|
2422
|
+
const start = starts.get(node);
|
|
2423
|
+
if (start === void 0) {
|
|
2424
|
+
return true;
|
|
2425
|
+
}
|
|
2426
|
+
if (start >= cutoff) {
|
|
2427
|
+
return false;
|
|
2428
|
+
}
|
|
2429
|
+
return true;
|
|
2430
|
+
});
|
|
281
2431
|
return true;
|
|
282
2432
|
};
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
let extractedTree = baseTree;
|
|
299
|
-
let extractedText = baseText;
|
|
300
|
-
for (const selector of BODY_SELECTORS) {
|
|
301
|
-
const content = { type: "root", children: selectAll(selector, baseFilterd) };
|
|
302
|
-
const contentText = hastToString(content);
|
|
303
|
-
if (contentText.length < 25) {
|
|
2433
|
+
|
|
2434
|
+
// src/extractors/presets/takumi.ts
|
|
2435
|
+
var MIN_RETAINED_FRACTION = 0.25;
|
|
2436
|
+
var SEMANTIC_SELECTORS = [
|
|
2437
|
+
(element3) => stringProperty(element3, "itemprop")?.includes("articleBody") ?? false,
|
|
2438
|
+
(element3) => element3.tagName === "article",
|
|
2439
|
+
(element3) => element3.tagName === "main",
|
|
2440
|
+
(element3) => stringProperty(element3, "role") === "main"
|
|
2441
|
+
];
|
|
2442
|
+
var findBody = (hast) => findElement(hast, (element3) => element3.tagName === "body") ?? hast;
|
|
2443
|
+
var semanticShortcut = (tree, collector, minLength) => {
|
|
2444
|
+
for (const matches of SEMANTIC_SELECTORS) {
|
|
2445
|
+
const found = [];
|
|
2446
|
+
collectMatching(tree, matches, found);
|
|
2447
|
+
if (found.length !== 1) {
|
|
304
2448
|
continue;
|
|
305
2449
|
}
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
2450
|
+
const candidate = found[0];
|
|
2451
|
+
const metrics = collector.metrics(candidate);
|
|
2452
|
+
if (metrics.text >= minLength && collector.linkDensity(candidate) < 0.5) {
|
|
2453
|
+
return candidate;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return void 0;
|
|
2457
|
+
};
|
|
2458
|
+
var collectMatching = (tree, matches, into) => {
|
|
2459
|
+
const stack = [tree];
|
|
2460
|
+
while (stack.length > 0 && into.length < 2) {
|
|
2461
|
+
const node = stack.pop();
|
|
2462
|
+
if (isElement(node) && matches(node)) {
|
|
2463
|
+
into.push(node);
|
|
310
2464
|
continue;
|
|
311
2465
|
}
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
2466
|
+
if ("children" in node) {
|
|
2467
|
+
for (const child of node.children) {
|
|
2468
|
+
stack.push(child);
|
|
2469
|
+
}
|
|
316
2470
|
}
|
|
317
2471
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
2472
|
+
};
|
|
2473
|
+
var asRootOf = (nodes) => ({ type: "root", children: nodes });
|
|
2474
|
+
var scoredSelection = (tree, collector, lang) => {
|
|
2475
|
+
const context = buildScoringContext(tree, collector, lang);
|
|
2476
|
+
const candidates = scoreCandidates(tree, context);
|
|
2477
|
+
const top = candidates[0];
|
|
2478
|
+
if (!top) {
|
|
2479
|
+
return void 0;
|
|
2480
|
+
}
|
|
2481
|
+
const promoted = climbToArticleBoundary(top, candidates, context);
|
|
2482
|
+
const anchor = promoted === top.element ? top : { element: promoted, score: top.score };
|
|
2483
|
+
const selected = collectArticleNodes(anchor, candidates, context);
|
|
2484
|
+
return selected.length === 1 ? selected[0] : asRootOf(selected);
|
|
2485
|
+
};
|
|
2486
|
+
var takumiExtractor = (params) => {
|
|
2487
|
+
const { hast, lang, owned } = params;
|
|
2488
|
+
const body = owned ? findBody(hast) : cloneHast(findBody(hast));
|
|
2489
|
+
stripNonContent(body);
|
|
2490
|
+
let collector = new MetricsCollector();
|
|
2491
|
+
const baseText = collector.textLength(body);
|
|
2492
|
+
if (baseText === 0) {
|
|
2493
|
+
return body;
|
|
2494
|
+
}
|
|
2495
|
+
const minLength = Math.min(minContentLength(lang), Math.max(0, baseText - 200));
|
|
2496
|
+
const shortcut = semanticShortcut(body, collector, minLength);
|
|
2497
|
+
const searchRoot = shortcut ?? body;
|
|
2498
|
+
const searchText = collector.textLength(searchRoot);
|
|
2499
|
+
const doomed = findUnlikelyElements(searchRoot);
|
|
2500
|
+
const doomedText = doomed.reduce((sum, element3) => sum + collector.metrics(element3).text, 0);
|
|
2501
|
+
if (searchText - doomedText > Math.max(minLength, searchText * MIN_RETAINED_FRACTION)) {
|
|
2502
|
+
const doomedSet = new Set(doomed);
|
|
2503
|
+
pruneInPlace(searchRoot, (node) => !(isElement(node) && doomedSet.has(node)));
|
|
2504
|
+
collector = new MetricsCollector();
|
|
2505
|
+
}
|
|
2506
|
+
const scoringRoot = searchRoot;
|
|
2507
|
+
const scoringRootText = collector.textLength(scoringRoot);
|
|
2508
|
+
const selection = shortcut ? scoringRoot : scoredSelection(scoringRoot, collector, lang) ?? scoringRoot;
|
|
2509
|
+
const selectionText = collector.textLength(selection);
|
|
2510
|
+
const selectionIsSound = selectionText >= minLength || selectionText > scoringRootText * MIN_RETAINED_FRACTION;
|
|
2511
|
+
const chosen = selectionIsSound ? selection : scoringRoot;
|
|
2512
|
+
truncateTrailingBoilerplate(chosen);
|
|
2513
|
+
const cleanupCollector = new MetricsCollector();
|
|
2514
|
+
const chosenText = cleanupCollector.textLength(chosen);
|
|
2515
|
+
const widgets = findCleanupTargets(chosen, cleanupCollector);
|
|
2516
|
+
const widgetText = widgets.reduce((sum, element3) => sum + cleanupCollector.metrics(element3).text, 0);
|
|
2517
|
+
const remainingText = chosenText - widgetText;
|
|
2518
|
+
if (remainingText < chosenText * MIN_RETAINED_FRACTION && remainingText < minLength) {
|
|
2519
|
+
return chosen;
|
|
2520
|
+
}
|
|
2521
|
+
return cleanContent(chosen, cleanupCollector);
|
|
2522
|
+
};
|
|
2523
|
+
|
|
2524
|
+
// src/extractors/presets/auto.ts
|
|
2525
|
+
var createAutoExtractor = (options = {}) => {
|
|
2526
|
+
const { adapters } = options;
|
|
2527
|
+
return (params) => {
|
|
2528
|
+
if (adapters !== false) {
|
|
2529
|
+
const extracted = runAdapters({ hast: params.hast, url: params.url, lang: params.lang }, adapters ?? void 0);
|
|
2530
|
+
if (extracted) {
|
|
2531
|
+
return extracted;
|
|
2532
|
+
}
|
|
324
2533
|
}
|
|
325
|
-
return
|
|
326
|
-
}
|
|
327
|
-
const finalTree = hastToString(finalFilteredTree).length > extractedText.length / 3 ? finalFilteredTree : extractedTree;
|
|
328
|
-
return finalTree;
|
|
2534
|
+
return takumiExtractor(params);
|
|
2535
|
+
};
|
|
329
2536
|
};
|
|
2537
|
+
var autoExtractor = createAutoExtractor();
|
|
330
2538
|
|
|
331
2539
|
// src/constants.ts
|
|
332
|
-
var DEFAULT_EXTRACTORS = [
|
|
2540
|
+
var DEFAULT_EXTRACTORS = [autoExtractor];
|
|
333
2541
|
|
|
334
2542
|
// src/extractors/pipeExtractors.ts
|
|
335
2543
|
var pipeExtractors = (params, extractors = DEFAULT_EXTRACTORS) => {
|
|
336
|
-
const { hast, lang } = params;
|
|
337
|
-
const
|
|
338
|
-
|
|
2544
|
+
const { hast, lang, url, owned } = params;
|
|
2545
|
+
const list = Array.isArray(extractors) ? extractors : [extractors];
|
|
2546
|
+
let current = hast;
|
|
2547
|
+
let currentOwned = owned ?? false;
|
|
2548
|
+
for (const extractor of list) {
|
|
339
2549
|
if (extractor === false) {
|
|
340
|
-
|
|
2550
|
+
continue;
|
|
341
2551
|
}
|
|
342
|
-
if (typeof extractor
|
|
343
|
-
|
|
2552
|
+
if (typeof extractor !== "function") {
|
|
2553
|
+
throw new Error(`Invalid extractor: ${String(extractor)}`);
|
|
344
2554
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
2555
|
+
current = extractor({ hast: current, lang, url, owned: currentOwned });
|
|
2556
|
+
currentOwned = true;
|
|
2557
|
+
}
|
|
2558
|
+
return current;
|
|
2559
|
+
};
|
|
2560
|
+
|
|
2561
|
+
// src/extractors/presets/minimal-filter.ts
|
|
2562
|
+
var MIN_RETAINED_FRACTION2 = 1 / 3;
|
|
2563
|
+
var ALWAYS_ACCEPT_LENGTH = 5e3;
|
|
2564
|
+
var isChrome = (element3) => {
|
|
2565
|
+
if (element3.tagName === "aside" || element3.tagName === "nav" || element3.tagName === "dialog") {
|
|
2566
|
+
return true;
|
|
2567
|
+
}
|
|
2568
|
+
const role = stringProperty(element3, "role");
|
|
2569
|
+
if (role && UNLIKELY_ROLES.has(role)) {
|
|
2570
|
+
return true;
|
|
2571
|
+
}
|
|
2572
|
+
if (role === "dialog" && isTruthyAttribute(element3, "aria-modal")) {
|
|
2573
|
+
return true;
|
|
2574
|
+
}
|
|
2575
|
+
if (stringProperty(element3, "rel") === "author") {
|
|
2576
|
+
return true;
|
|
2577
|
+
}
|
|
2578
|
+
if (REGEXPS.byline.test(matchString(element3))) {
|
|
2579
|
+
return true;
|
|
2580
|
+
}
|
|
2581
|
+
return false;
|
|
2582
|
+
};
|
|
2583
|
+
var minimalFilter = (params) => {
|
|
2584
|
+
const { hast } = params;
|
|
2585
|
+
const body = cloneHast(findElement(hast, (element3) => element3.tagName === "body") ?? hast);
|
|
2586
|
+
stripNonContent(body);
|
|
2587
|
+
const collector = new MetricsCollector();
|
|
2588
|
+
const baseText = collector.textLength(body);
|
|
2589
|
+
if (baseText === 0) {
|
|
2590
|
+
return body;
|
|
2591
|
+
}
|
|
2592
|
+
const filtered = cloneHast(body);
|
|
2593
|
+
pruneInPlace(filtered, (node) => !(isElement(node) && isChrome(node)));
|
|
2594
|
+
const filteredText = new MetricsCollector().textLength(filtered);
|
|
2595
|
+
const isOverExtracted = filteredText < baseText * MIN_RETAINED_FRACTION2 && filteredText < ALWAYS_ACCEPT_LENGTH;
|
|
2596
|
+
return isOverExtracted ? body : filtered;
|
|
348
2597
|
};
|
|
349
2598
|
|
|
350
2599
|
// src/mdast-handlers/custom-a-handler.ts
|
|
351
2600
|
import { defaultHandlers } from "hast-util-to-mdast";
|
|
352
|
-
import { toString as
|
|
2601
|
+
import { toString as hastToString } from "hast-util-to-string";
|
|
2602
|
+
var ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "ftp:", "ftps:"]);
|
|
2603
|
+
var isFollowableHref = (href) => {
|
|
2604
|
+
const trimmed = href?.trim();
|
|
2605
|
+
if (!trimmed) {
|
|
2606
|
+
return false;
|
|
2607
|
+
}
|
|
2608
|
+
const scheme = trimmed.match(/^[a-z][a-z0-9+.-]*:/i)?.[0];
|
|
2609
|
+
if (!scheme) {
|
|
2610
|
+
return true;
|
|
2611
|
+
}
|
|
2612
|
+
return ALLOWED_SCHEMES.has(scheme.toLowerCase());
|
|
2613
|
+
};
|
|
353
2614
|
var customAHandler = (options) => (state, node) => {
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
2615
|
+
const asPlainText = () => {
|
|
2616
|
+
const text6 = hastToString(node);
|
|
2617
|
+
if (text6.length === 0) {
|
|
357
2618
|
return void 0;
|
|
358
2619
|
}
|
|
359
|
-
const
|
|
360
|
-
state.patch(node,
|
|
361
|
-
return
|
|
2620
|
+
const result = { type: "text", value: text6 };
|
|
2621
|
+
state.patch(node, result);
|
|
2622
|
+
return result;
|
|
2623
|
+
};
|
|
2624
|
+
if (options?.asText) {
|
|
2625
|
+
return hastToString(node).length <= 3 ? void 0 : asPlainText();
|
|
362
2626
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return link;
|
|
2627
|
+
if (!isFollowableHref(stringProperty(node, "href"))) {
|
|
2628
|
+
return asPlainText();
|
|
366
2629
|
}
|
|
367
|
-
|
|
2630
|
+
const link = defaultHandlers.a(state, node);
|
|
2631
|
+
return link.children.length > 0 ? link : void 0;
|
|
368
2632
|
};
|
|
369
2633
|
|
|
370
2634
|
// src/mdast-handlers/custom-code-handler.ts
|
|
@@ -435,9 +2699,9 @@ var customCodeHandler = (state, node) => {
|
|
|
435
2699
|
};
|
|
436
2700
|
|
|
437
2701
|
// src/mdast-handlers/custom-div-handler.ts
|
|
438
|
-
import { select as
|
|
2702
|
+
import { select as select5 } from "hast-util-select";
|
|
439
2703
|
import { defaultHandlers as defaultHandlers2 } from "hast-util-to-mdast";
|
|
440
|
-
import { toString as
|
|
2704
|
+
import { toString as hastToString2 } from "hast-util-to-string";
|
|
441
2705
|
import { toText as toText2 } from "hast-util-to-text";
|
|
442
2706
|
import { trimTrailingLines as trimTrailingLines2 } from "trim-trailing-lines";
|
|
443
2707
|
var CODE_BLOCK_REGEX = /highlight-source|language-|codegroup|codeblock|code-block/i;
|
|
@@ -472,8 +2736,8 @@ var customDivHandler = (state, node) => {
|
|
|
472
2736
|
if (codeBlock && classNames.some((className) => CODE_BLOCK_REGEX.test(className))) {
|
|
473
2737
|
const codeBlockClassNames = codeBlock.type === "element" ? codeBlock.properties.className ?? [] : [];
|
|
474
2738
|
const codeValue = trimTrailingLines2(toText2(codeBlock)).trim();
|
|
475
|
-
const filenameElement =
|
|
476
|
-
const fileLang = filenameElement ?
|
|
2739
|
+
const filenameElement = select5(CODE_FILENAME_SELECTORS, node);
|
|
2740
|
+
const fileLang = filenameElement ? hastToString2(filenameElement).match(/\.(\w+)$/)?.[1] : null;
|
|
477
2741
|
const classLang = [...classNames, ...codeBlockClassNames].map((className) => {
|
|
478
2742
|
const match = LANGUAGE_MATCH_REGEX2.map((regex) => className.match(regex)).find((match2) => match2);
|
|
479
2743
|
return match?.[1];
|
|
@@ -488,51 +2752,317 @@ var customDivHandler = (state, node) => {
|
|
|
488
2752
|
|
|
489
2753
|
// src/mdast-handlers/custom-img-handler.ts
|
|
490
2754
|
import { defaultHandlers as defaultHandlers3 } from "hast-util-to-mdast";
|
|
2755
|
+
var MAX_DATA_URL_LENGTH = 256;
|
|
2756
|
+
var hasNoUsableSource = (node) => {
|
|
2757
|
+
const src = stringProperty(node, "src")?.trim();
|
|
2758
|
+
if (!src) {
|
|
2759
|
+
return true;
|
|
2760
|
+
}
|
|
2761
|
+
if (src.startsWith("data:")) {
|
|
2762
|
+
return src.length > MAX_DATA_URL_LENGTH;
|
|
2763
|
+
}
|
|
2764
|
+
return false;
|
|
2765
|
+
};
|
|
491
2766
|
var customImgHandler = (options) => (state, node) => {
|
|
492
2767
|
if (options?.hideImage) {
|
|
493
2768
|
return void 0;
|
|
494
2769
|
}
|
|
495
|
-
|
|
2770
|
+
if (hasNoUsableSource(node)) {
|
|
2771
|
+
return void 0;
|
|
2772
|
+
}
|
|
2773
|
+
const image = defaultHandlers3.image(state, node);
|
|
2774
|
+
if (!image.alt) {
|
|
2775
|
+
const title = stringProperty(node, "title");
|
|
2776
|
+
if (title) {
|
|
2777
|
+
image.alt = title;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
return image;
|
|
496
2781
|
};
|
|
497
2782
|
|
|
498
2783
|
// src/mdast-handlers/custom-table-handler.ts
|
|
499
2784
|
import { defaultHandlers as defaultHandlers4 } from "hast-util-to-mdast";
|
|
500
2785
|
import { toText as toText3 } from "hast-util-to-text";
|
|
2786
|
+
var MAX_SPAN = 1e3;
|
|
2787
|
+
var clampSpans = (node) => {
|
|
2788
|
+
walk(node, (candidate) => {
|
|
2789
|
+
if (!isElement(candidate) || candidate.tagName !== "td" && candidate.tagName !== "th") {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
for (const attribute of ["colSpan", "rowSpan"]) {
|
|
2793
|
+
const span = numericProperty(candidate, attribute);
|
|
2794
|
+
if (span !== void 0 && span > MAX_SPAN) {
|
|
2795
|
+
candidate.properties[attribute] = MAX_SPAN;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
};
|
|
501
2800
|
var customTableHandler = (options) => (state, node) => {
|
|
502
2801
|
if (options?.asText) {
|
|
503
|
-
const
|
|
504
|
-
state.patch(node,
|
|
505
|
-
return
|
|
2802
|
+
const paragraph2 = { type: "paragraph", children: [{ type: "text", value: toText3(node) }] };
|
|
2803
|
+
state.patch(node, paragraph2);
|
|
2804
|
+
return paragraph2;
|
|
506
2805
|
}
|
|
2806
|
+
clampSpans(node);
|
|
507
2807
|
return defaultHandlers4.table(state, node);
|
|
508
2808
|
};
|
|
509
2809
|
|
|
2810
|
+
// src/mdast-handlers/definition-list-handler.ts
|
|
2811
|
+
var hoistBoundaryWhitespace = (nodes) => {
|
|
2812
|
+
const result = [];
|
|
2813
|
+
for (const node of nodes) {
|
|
2814
|
+
if (node.type !== "emphasis" && node.type !== "strong") {
|
|
2815
|
+
result.push(node);
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
node.children = hoistBoundaryWhitespace(node.children);
|
|
2819
|
+
const first = node.children[0];
|
|
2820
|
+
if (first?.type === "text" && /^\s/.test(first.value)) {
|
|
2821
|
+
result.push({ type: "text", value: " " });
|
|
2822
|
+
first.value = first.value.replace(/^\s+/, "");
|
|
2823
|
+
}
|
|
2824
|
+
let trailing = false;
|
|
2825
|
+
const last = node.children[node.children.length - 1];
|
|
2826
|
+
if (last?.type === "text" && /\s$/.test(last.value)) {
|
|
2827
|
+
last.value = last.value.replace(/\s+$/, "");
|
|
2828
|
+
trailing = true;
|
|
2829
|
+
}
|
|
2830
|
+
node.children = node.children.filter((child) => !(child.type === "text" && child.value.length === 0));
|
|
2831
|
+
if (node.children.length > 0) {
|
|
2832
|
+
result.push(node);
|
|
2833
|
+
}
|
|
2834
|
+
if (trailing) {
|
|
2835
|
+
result.push({ type: "text", value: " " });
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
return result;
|
|
2839
|
+
};
|
|
2840
|
+
var definitionListHandler = (state, node) => {
|
|
2841
|
+
const items = [];
|
|
2842
|
+
let current;
|
|
2843
|
+
const flush = () => {
|
|
2844
|
+
if (!current) {
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
const heading2 = {
|
|
2848
|
+
type: "paragraph",
|
|
2849
|
+
children: current.term.length > 0 ? [{ type: "strong", children: current.term }] : []
|
|
2850
|
+
};
|
|
2851
|
+
items.push({ type: "listItem", spread: false, children: [heading2, ...current.definitions] });
|
|
2852
|
+
current = void 0;
|
|
2853
|
+
};
|
|
2854
|
+
for (const child of node.children) {
|
|
2855
|
+
if (child.type !== "element") {
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2858
|
+
const element3 = child;
|
|
2859
|
+
if (element3.tagName === "dt") {
|
|
2860
|
+
if (current && current.definitions.length > 0) {
|
|
2861
|
+
flush();
|
|
2862
|
+
}
|
|
2863
|
+
const phrasing = hoistBoundaryWhitespace(state.all(element3));
|
|
2864
|
+
current = current ? { term: [...current.term, { type: "text", value: ", " }, ...phrasing], definitions: [] } : { term: phrasing, definitions: [] };
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
if (element3.tagName === "dd") {
|
|
2868
|
+
if (!current) {
|
|
2869
|
+
current = { term: [], definitions: [] };
|
|
2870
|
+
}
|
|
2871
|
+
const content = state.toFlow(state.all(element3));
|
|
2872
|
+
current.definitions.push(...content);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
flush();
|
|
2876
|
+
if (items.length === 0) {
|
|
2877
|
+
return void 0;
|
|
2878
|
+
}
|
|
2879
|
+
const result = { type: "list", ordered: false, spread: false, children: items };
|
|
2880
|
+
state.patch(node, result);
|
|
2881
|
+
return result;
|
|
2882
|
+
};
|
|
2883
|
+
|
|
510
2884
|
// src/mdast-handlers/math-handler.ts
|
|
511
2885
|
import { toHtml } from "hast-util-to-html";
|
|
512
2886
|
import { MathMLToLaTeX } from "mathml-to-latex";
|
|
2887
|
+
var readAnnotation = (node) => {
|
|
2888
|
+
const alttext = stringProperty(node, "alttext");
|
|
2889
|
+
if (!alttext) {
|
|
2890
|
+
return void 0;
|
|
2891
|
+
}
|
|
2892
|
+
const unwrapped = alttext.trim().replace(/^\{\\displaystyle\s*([\s\S]*)\}$/, "$1").trim();
|
|
2893
|
+
return unwrapped.length > 0 ? unwrapped : void 0;
|
|
2894
|
+
};
|
|
2895
|
+
var isDisplayMath = (node) => stringProperty(node, "display") === "block";
|
|
513
2896
|
var mathHandler = (state, node) => {
|
|
514
|
-
const
|
|
515
|
-
const
|
|
516
|
-
|
|
2897
|
+
const latex = readAnnotation(node) ?? MathMLToLaTeX.convert(toHtml(node));
|
|
2898
|
+
const result = isDisplayMath(node) ? { type: "math", value: latex, meta: null } : { type: "inlineMath", value: latex };
|
|
2899
|
+
state.patch(node, result);
|
|
2900
|
+
return result;
|
|
2901
|
+
};
|
|
2902
|
+
|
|
2903
|
+
// src/mdast-handlers/sup-sub-handler.ts
|
|
2904
|
+
import { toString as hastToString3 } from "hast-util-to-string";
|
|
2905
|
+
var MAX_ANNOTATION_LENGTH = 32;
|
|
2906
|
+
var supSubHandler = (tagName) => (state, node) => {
|
|
2907
|
+
const inner = hastToString3(node).trim();
|
|
2908
|
+
if (inner.length === 0 || inner.length > MAX_ANNOTATION_LENGTH) {
|
|
2909
|
+
return state.all(node);
|
|
2910
|
+
}
|
|
2911
|
+
const escaped = inner.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2912
|
+
const result = { type: "html", value: `<${tagName}>${escaped}</${tagName}>` };
|
|
517
2913
|
state.patch(node, result);
|
|
518
2914
|
return result;
|
|
519
2915
|
};
|
|
520
2916
|
|
|
2917
|
+
// src/normalize/index.ts
|
|
2918
|
+
var LAZY_SRC_ATTRIBUTES = [
|
|
2919
|
+
"data-src",
|
|
2920
|
+
"data-original",
|
|
2921
|
+
"data-lazy-src",
|
|
2922
|
+
"data-original-src",
|
|
2923
|
+
"data-actualsrc",
|
|
2924
|
+
"data-hi-res-src",
|
|
2925
|
+
"data-echo"
|
|
2926
|
+
];
|
|
2927
|
+
var PLACEHOLDER_SRC = /^$|^data:image\/|^about:blank$|spacer\.gif$|blank\.(gif|png)$/i;
|
|
2928
|
+
var readSrcset = (element3, attribute) => {
|
|
2929
|
+
const raw = property(element3, attribute);
|
|
2930
|
+
if (typeof raw === "string") {
|
|
2931
|
+
return raw;
|
|
2932
|
+
}
|
|
2933
|
+
if (Array.isArray(raw)) {
|
|
2934
|
+
return raw.join(", ");
|
|
2935
|
+
}
|
|
2936
|
+
return void 0;
|
|
2937
|
+
};
|
|
2938
|
+
var bestFromSrcset = (srcset) => {
|
|
2939
|
+
let best;
|
|
2940
|
+
for (const entry of srcset.split(",")) {
|
|
2941
|
+
const parts = entry.trim().split(/\s+/);
|
|
2942
|
+
const url = parts[0];
|
|
2943
|
+
if (!url) {
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
const descriptor = parts[1];
|
|
2947
|
+
let weight = 1;
|
|
2948
|
+
if (descriptor?.endsWith("w")) {
|
|
2949
|
+
weight = Number.parseFloat(descriptor) || 1;
|
|
2950
|
+
} else if (descriptor?.endsWith("x")) {
|
|
2951
|
+
weight = (Number.parseFloat(descriptor) || 1) * 1e3;
|
|
2952
|
+
}
|
|
2953
|
+
if (!best || weight > best.weight) {
|
|
2954
|
+
best = { url, weight };
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
return best?.url;
|
|
2958
|
+
};
|
|
2959
|
+
var resolveImageSources = (tree) => {
|
|
2960
|
+
walk(tree, (node) => {
|
|
2961
|
+
if (!isElement(node) || node.tagName !== "img") {
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
const current = stringProperty(node, "src");
|
|
2965
|
+
if (current && !PLACEHOLDER_SRC.test(current)) {
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
for (const attribute of LAZY_SRC_ATTRIBUTES) {
|
|
2969
|
+
const candidate = stringProperty(node, attribute);
|
|
2970
|
+
if (candidate && !PLACEHOLDER_SRC.test(candidate)) {
|
|
2971
|
+
node.properties.src = candidate;
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
for (const attribute of ["srcSet", "srcset", "data-srcset"]) {
|
|
2976
|
+
const srcset = readSrcset(node, attribute);
|
|
2977
|
+
const best = srcset ? bestFromSrcset(srcset) : void 0;
|
|
2978
|
+
if (best) {
|
|
2979
|
+
node.properties.src = best;
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
};
|
|
2985
|
+
var katexDuplicates = (node) => {
|
|
2986
|
+
const classes = classList(node);
|
|
2987
|
+
if (!(classes.includes("katex") || classes.includes("katex-display"))) {
|
|
2988
|
+
return [];
|
|
2989
|
+
}
|
|
2990
|
+
const hasMathml = node.children.some((child) => isElement(child) && classList(child).includes("katex-mathml"));
|
|
2991
|
+
if (!hasMathml) {
|
|
2992
|
+
return [];
|
|
2993
|
+
}
|
|
2994
|
+
return node.children.filter((child) => isElement(child) && classList(child).includes("katex-html"));
|
|
2995
|
+
};
|
|
2996
|
+
var mediawikiDuplicates = (node) => {
|
|
2997
|
+
if (!classList(node).includes("mwe-math-element")) {
|
|
2998
|
+
return [];
|
|
2999
|
+
}
|
|
3000
|
+
return node.children.filter(
|
|
3001
|
+
(child) => isElement(child) && classList(child).some((name) => name.startsWith("mwe-math-fallback-image"))
|
|
3002
|
+
);
|
|
3003
|
+
};
|
|
3004
|
+
var mathjaxDuplicates = (node) => {
|
|
3005
|
+
if (node.tagName !== "mjx-container") {
|
|
3006
|
+
return [];
|
|
3007
|
+
}
|
|
3008
|
+
return node.children.filter((child) => isElement(child) && child.tagName !== "mjx-assistive-mml");
|
|
3009
|
+
};
|
|
3010
|
+
var dedupeRenderedMath = (tree) => {
|
|
3011
|
+
const doomed = /* @__PURE__ */ new Set();
|
|
3012
|
+
walk(tree, (node) => {
|
|
3013
|
+
if (!isElement(node)) {
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
for (const duplicate of [...katexDuplicates(node), ...mathjaxDuplicates(node), ...mediawikiDuplicates(node)]) {
|
|
3017
|
+
doomed.add(duplicate);
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
3020
|
+
if (doomed.size > 0) {
|
|
3021
|
+
pruneInPlace(tree, (node) => !(isElement(node) && doomed.has(node)));
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
var promoteAriaHeadings = (tree) => {
|
|
3025
|
+
walk(tree, (node) => {
|
|
3026
|
+
if (!isElement(node) || stringProperty(node, "role") !== "heading") {
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
if (/^h[1-6]$/.test(node.tagName)) {
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
const level = numericProperty(node, "aria-level") ?? 2;
|
|
3033
|
+
const clamped = Math.min(6, Math.max(1, Math.round(level)));
|
|
3034
|
+
node.tagName = `h${clamped}`;
|
|
3035
|
+
});
|
|
3036
|
+
};
|
|
3037
|
+
var normalizeHast = (tree, options = {}) => {
|
|
3038
|
+
const { images = true, math = true, ariaHeadings = true } = options;
|
|
3039
|
+
if (images) {
|
|
3040
|
+
resolveImageSources(tree);
|
|
3041
|
+
}
|
|
3042
|
+
if (math) {
|
|
3043
|
+
dedupeRenderedMath(tree);
|
|
3044
|
+
}
|
|
3045
|
+
if (ariaHeadings) {
|
|
3046
|
+
promoteAriaHeadings(tree);
|
|
3047
|
+
}
|
|
3048
|
+
return tree;
|
|
3049
|
+
};
|
|
3050
|
+
|
|
521
3051
|
// src/utils/hast-utils.ts
|
|
522
|
-
import { select as
|
|
3052
|
+
import { select as select6, selectAll as selectAll3 } from "hast-util-select";
|
|
523
3053
|
var getLangFromHast = (node) => {
|
|
524
|
-
const html =
|
|
3054
|
+
const html = select6("html", node);
|
|
525
3055
|
if (html && typeof html.properties.lang === "string") {
|
|
526
3056
|
return html.properties.lang;
|
|
527
3057
|
}
|
|
528
3058
|
if (node.type !== "element") {
|
|
529
3059
|
return;
|
|
530
3060
|
}
|
|
531
|
-
const
|
|
532
|
-
if (
|
|
3061
|
+
const element3 = node;
|
|
3062
|
+
if (element3.tagName !== "html") {
|
|
533
3063
|
return;
|
|
534
3064
|
}
|
|
535
|
-
const langAttr =
|
|
3065
|
+
const langAttr = element3.properties.lang || element3.properties["xml:lang"];
|
|
536
3066
|
if (langAttr) {
|
|
537
3067
|
return langAttr;
|
|
538
3068
|
}
|
|
@@ -550,10 +3080,10 @@ var getUrlFromHast = (node) => {
|
|
|
550
3080
|
return void 0;
|
|
551
3081
|
}
|
|
552
3082
|
const metaTagAttributes = ["og:url", "twitter:url"];
|
|
553
|
-
const metaTags =
|
|
3083
|
+
const metaTags = selectAll3("meta", node);
|
|
554
3084
|
for (const meta of metaTags) {
|
|
555
|
-
const
|
|
556
|
-
if (typeof
|
|
3085
|
+
const property2 = meta.properties.property || meta.properties.name;
|
|
3086
|
+
if (typeof property2 === "string" && metaTagAttributes.includes(property2)) {
|
|
557
3087
|
return typeof meta.properties.content === "string" ? meta.properties.content : void 0;
|
|
558
3088
|
}
|
|
559
3089
|
}
|
|
@@ -564,13 +3094,18 @@ var getUrlFromHast = (node) => {
|
|
|
564
3094
|
var htmlToMdast = (htmlOrHast, options) => {
|
|
565
3095
|
const { extractors, url: defaultUrl, lang: defaultLang } = options || {};
|
|
566
3096
|
const [lang, hast] = typeof htmlOrHast === "string" ? [defaultLang || getLangFromStr(htmlOrHast), fromHtml(htmlOrHast, { fragment: true })] : [defaultLang || getLangFromHast(htmlOrHast), htmlOrHast];
|
|
3097
|
+
const isOwned = typeof htmlOrHast === "string" || (options?.owned ?? false);
|
|
567
3098
|
const url = defaultUrl || getUrlFromHast(hast);
|
|
568
|
-
const extractedHast = pipeExtractors({ hast, lang, url }, extractors);
|
|
3099
|
+
const extractedHast = pipeExtractors({ hast, lang, url, owned: isOwned }, extractors);
|
|
3100
|
+
normalizeHast(extractedHast, options?.normalize);
|
|
569
3101
|
const mdast = toMdast(extractedHast, {
|
|
570
3102
|
handlers: {
|
|
571
3103
|
math: mathHandler,
|
|
572
3104
|
div: customDivHandler,
|
|
573
3105
|
pre: customCodeHandler,
|
|
3106
|
+
dl: definitionListHandler,
|
|
3107
|
+
sup: supSubHandler("sup"),
|
|
3108
|
+
sub: supSubHandler("sub"),
|
|
574
3109
|
a: customAHandler({ asText: options?.linkAsText }),
|
|
575
3110
|
img: customImgHandler({ hideImage: options?.hideImage }),
|
|
576
3111
|
table: customTableHandler({ asText: options?.tableAsText })
|
|
@@ -623,222 +3158,241 @@ var mdastToMarkdown = (mdast, options) => {
|
|
|
623
3158
|
return markdown;
|
|
624
3159
|
};
|
|
625
3160
|
|
|
626
|
-
// src/
|
|
627
|
-
var
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
3161
|
+
// src/metadata/index.ts
|
|
3162
|
+
var readMetaTags = (tree) => {
|
|
3163
|
+
const tags = /* @__PURE__ */ new Map();
|
|
3164
|
+
for (const meta of collectElements(tree, (element3) => element3.tagName === "meta")) {
|
|
3165
|
+
const key = stringProperty(meta, "property") ?? stringProperty(meta, "name") ?? stringProperty(meta, "itemprop");
|
|
3166
|
+
const content = stringProperty(meta, "content");
|
|
3167
|
+
if (!(key && content)) {
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
const normalized = key.toLowerCase();
|
|
3171
|
+
if (!tags.has(normalized)) {
|
|
3172
|
+
tags.set(normalized, content);
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
return tags;
|
|
3176
|
+
};
|
|
3177
|
+
var readJsonLd = (tree) => {
|
|
3178
|
+
const entries = [];
|
|
3179
|
+
for (const script of collectElements(tree, (element3) => element3.tagName === "script")) {
|
|
3180
|
+
const type = stringProperty(script, "type");
|
|
3181
|
+
if (type?.toLowerCase() !== "application/ld+json") {
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
3184
|
+
const source = script.children.map((child) => child.type === "text" ? child.value : "").join("").trim();
|
|
3185
|
+
if (source.length === 0) {
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
3188
|
+
let parsed;
|
|
3189
|
+
try {
|
|
3190
|
+
parsed = JSON.parse(source);
|
|
3191
|
+
} catch {
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
for (const entry of flattenJsonLd(parsed)) {
|
|
3195
|
+
entries.push(entry);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
return entries;
|
|
3199
|
+
};
|
|
3200
|
+
var flattenJsonLd = (value) => {
|
|
3201
|
+
if (Array.isArray(value)) {
|
|
3202
|
+
return value.flatMap(flattenJsonLd);
|
|
3203
|
+
}
|
|
3204
|
+
if (typeof value !== "object" || value === null) {
|
|
3205
|
+
return [];
|
|
3206
|
+
}
|
|
3207
|
+
const record = value;
|
|
3208
|
+
const graph = record["@graph"];
|
|
3209
|
+
if (graph) {
|
|
3210
|
+
return flattenJsonLd(graph);
|
|
3211
|
+
}
|
|
3212
|
+
return [record];
|
|
632
3213
|
};
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
3214
|
+
var ARTICLE_TYPES = /* @__PURE__ */ new Set([
|
|
3215
|
+
"article",
|
|
3216
|
+
"newsarticle",
|
|
3217
|
+
"blogposting",
|
|
3218
|
+
"techarticle",
|
|
3219
|
+
"scholarlyarticle",
|
|
3220
|
+
"report",
|
|
3221
|
+
"webpage"
|
|
3222
|
+
]);
|
|
3223
|
+
var jsonLdType = (entry) => {
|
|
3224
|
+
const type = entry["@type"];
|
|
3225
|
+
if (typeof type === "string") {
|
|
3226
|
+
return [type.toLowerCase()];
|
|
3227
|
+
}
|
|
3228
|
+
if (Array.isArray(type)) {
|
|
3229
|
+
return type.filter((value) => typeof value === "string").map((value) => value.toLowerCase());
|
|
647
3230
|
}
|
|
3231
|
+
return [];
|
|
648
3232
|
};
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const formattedNewExtension = newExtension.startsWith(".") ? newExtension : `.${newExtension}`;
|
|
653
|
-
if (fileName.startsWith(".")) {
|
|
654
|
-
const parts = fileName.split(".");
|
|
655
|
-
if (parts.length === 2) {
|
|
656
|
-
return parsedPath.slice(0, -1).concat(`${fileName}${formattedNewExtension}`).join("/");
|
|
657
|
-
}
|
|
658
|
-
parts[parts.length - 1] = newExtension.replace(/^\./, "");
|
|
659
|
-
return parsedPath.slice(0, -1).concat(parts.join(".")).join("/");
|
|
3233
|
+
var readName = (value) => {
|
|
3234
|
+
if (typeof value === "string") {
|
|
3235
|
+
return value;
|
|
660
3236
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
function urlToFilename(url) {
|
|
668
|
-
try {
|
|
669
|
-
const urlObj = new URL(url);
|
|
670
|
-
const domainParts = urlObj.hostname.split(".").reverse().reduce((acc, part, index) => {
|
|
671
|
-
if (index === 0) {
|
|
672
|
-
return acc;
|
|
673
|
-
}
|
|
674
|
-
if (acc.length >= 2) {
|
|
675
|
-
return acc;
|
|
676
|
-
}
|
|
677
|
-
if (part === "www") {
|
|
678
|
-
return acc;
|
|
3237
|
+
if (Array.isArray(value)) {
|
|
3238
|
+
for (const entry of value) {
|
|
3239
|
+
const name = readName(entry);
|
|
3240
|
+
if (name) {
|
|
3241
|
+
return name;
|
|
679
3242
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const domainString = domainParts.reverse().join("-");
|
|
683
|
-
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
684
|
-
const relevantPathParts = pathParts.slice(-2);
|
|
685
|
-
const pathString = relevantPathParts.map((part) => decodeURIComponent(part)).join("-");
|
|
686
|
-
let filename = [domainString, pathString].filter(Boolean).join("-");
|
|
687
|
-
filename = filename.toLowerCase().replace(/[<>:"/\\|?*\x00-\x1F]/g, "").replace(/[\s.]+/g, "-").replace(/^-+|-+$/g, "");
|
|
688
|
-
return filename || "output";
|
|
689
|
-
} catch {
|
|
690
|
-
return "output";
|
|
3243
|
+
}
|
|
3244
|
+
return void 0;
|
|
691
3245
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
3246
|
+
if (typeof value === "object" && value !== null) {
|
|
3247
|
+
const name = value.name;
|
|
3248
|
+
return typeof name === "string" ? name : void 0;
|
|
3249
|
+
}
|
|
3250
|
+
return void 0;
|
|
695
3251
|
};
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
while (fs.existsSync(nextFilePath)) {
|
|
706
|
-
const match = firstPart.match(/_(\d+)$/);
|
|
707
|
-
if (match) {
|
|
708
|
-
counter = Number.parseInt(match[1], 10) + 1;
|
|
709
|
-
}
|
|
710
|
-
const newName = `${baseName}_${counter}${restName}`;
|
|
711
|
-
nextFilePath = path.join(directory, newName);
|
|
712
|
-
counter++;
|
|
3252
|
+
var asString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3253
|
+
var MAX_TITLE_LENGTH = 160;
|
|
3254
|
+
var asTitle = (value) => {
|
|
3255
|
+
const cleaned = clean(value);
|
|
3256
|
+
return cleaned && cleaned.length <= MAX_TITLE_LENGTH ? cleaned : void 0;
|
|
3257
|
+
};
|
|
3258
|
+
var clean = (value) => {
|
|
3259
|
+
if (value === void 0) {
|
|
3260
|
+
return void 0;
|
|
713
3261
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
cancel(cancelMessage);
|
|
722
|
-
process.exit(1);
|
|
3262
|
+
const trimmed = value.replace(/\s+/g, " ").trim();
|
|
3263
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
3264
|
+
};
|
|
3265
|
+
var documentTitle = (tree) => {
|
|
3266
|
+
const title = findElement(tree, (element3) => element3.tagName === "title");
|
|
3267
|
+
if (!title) {
|
|
3268
|
+
return void 0;
|
|
723
3269
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
if (fs2.existsSync(value) && fs2.statSync(value).isDirectory()) {
|
|
737
|
-
return "No directory can be specified.";
|
|
738
|
-
}
|
|
739
|
-
if (!fs2.existsSync(value) && value.endsWith("/")) {
|
|
740
|
-
return "No directory can be specified.";
|
|
741
|
-
}
|
|
3270
|
+
return title.children.map((child) => child.type === "text" ? child.value : "").join("");
|
|
3271
|
+
};
|
|
3272
|
+
var firstHeading = (tree) => {
|
|
3273
|
+
const heading2 = findElement(tree, (element3) => element3.tagName === "h1");
|
|
3274
|
+
if (!heading2) {
|
|
3275
|
+
return void 0;
|
|
3276
|
+
}
|
|
3277
|
+
let text6 = "";
|
|
3278
|
+
walk(heading2, (node) => {
|
|
3279
|
+
if (node.type === "text") {
|
|
3280
|
+
text6 += node.value;
|
|
742
3281
|
}
|
|
743
3282
|
});
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
3283
|
+
return text6;
|
|
3284
|
+
};
|
|
3285
|
+
var linkHref = (tree, rel) => {
|
|
3286
|
+
const link = findElement(
|
|
3287
|
+
tree,
|
|
3288
|
+
(element3) => element3.tagName === "link" && (stringProperty(element3, "rel") ?? "").toLowerCase().includes(rel)
|
|
3289
|
+
);
|
|
3290
|
+
return link ? stringProperty(link, "href") : void 0;
|
|
3291
|
+
};
|
|
3292
|
+
var documentLang = (tree) => {
|
|
3293
|
+
const html = findElement(tree, (element3) => element3.tagName === "html");
|
|
3294
|
+
if (html) {
|
|
3295
|
+
const declared = stringProperty(html, "lang") ?? stringProperty(html, "xml:lang");
|
|
3296
|
+
if (declared) {
|
|
3297
|
+
return declared;
|
|
3298
|
+
}
|
|
747
3299
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (isOutputFileOverwrite) {
|
|
754
|
-
return outputPath;
|
|
3300
|
+
if (isElement(tree)) {
|
|
3301
|
+
const own = stringProperty(tree, "lang");
|
|
3302
|
+
if (own) {
|
|
3303
|
+
return own;
|
|
3304
|
+
}
|
|
755
3305
|
}
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
return "No directory can be specified.";
|
|
3306
|
+
const tagged = findElement(tree, (element3) => Boolean(stringProperty(element3, "lang")));
|
|
3307
|
+
return tagged ? stringProperty(tagged, "lang") : void 0;
|
|
3308
|
+
};
|
|
3309
|
+
var extractMetadata = (tree) => {
|
|
3310
|
+
const meta = readMetaTags(tree);
|
|
3311
|
+
const jsonLd = readJsonLd(tree);
|
|
3312
|
+
const article = jsonLd.find((entry) => jsonLdType(entry).some((type) => ARTICLE_TYPES.has(type))) ?? jsonLd[0];
|
|
3313
|
+
const get = (...keys) => {
|
|
3314
|
+
for (const key of keys) {
|
|
3315
|
+
const value = meta.get(key);
|
|
3316
|
+
if (value) {
|
|
3317
|
+
return value;
|
|
769
3318
|
}
|
|
770
3319
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
3320
|
+
return void 0;
|
|
3321
|
+
};
|
|
3322
|
+
const title = asTitle(get("og:title", "twitter:title")) ?? asTitle(asString(article?.headline)) ?? asTitle(asString(article?.name)) ?? asTitle(documentTitle(tree)) ?? asTitle(firstHeading(tree));
|
|
3323
|
+
return {
|
|
3324
|
+
title,
|
|
3325
|
+
description: clean(get("og:description", "twitter:description", "description")) ?? clean(asString(article?.description)),
|
|
3326
|
+
author: clean(readName(article?.author)) ?? clean(get("author", "article:author", "twitter:creator")),
|
|
3327
|
+
published: clean(asString(article?.datePublished)) ?? clean(get("article:published_time", "date", "pubdate")),
|
|
3328
|
+
modified: clean(asString(article?.dateModified)) ?? clean(get("article:modified_time", "og:updated_time")),
|
|
3329
|
+
siteName: clean(get("og:site_name", "application-name")) ?? clean(readName(article?.publisher)),
|
|
3330
|
+
canonicalUrl: clean(linkHref(tree, "canonical")) ?? clean(get("og:url", "twitter:url")),
|
|
3331
|
+
lang: clean(documentLang(tree)),
|
|
3332
|
+
image: clean(get("og:image", "twitter:image", "twitter:image:src")),
|
|
3333
|
+
type: clean(get("og:type"))
|
|
3334
|
+
};
|
|
774
3335
|
};
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
var DEFAULT_PATH = "https://example.com";
|
|
782
|
-
var LOADERS = ["fetch", "playwright"];
|
|
783
|
-
var MODES = ["default", "ai"];
|
|
784
|
-
|
|
785
|
-
// src/cli/helpers/inputSourcePath.ts
|
|
786
|
-
var inputSourcePath = async () => {
|
|
787
|
-
const result = await text2({
|
|
788
|
-
message: "Enter the URL or html path to be converted to markdown:",
|
|
789
|
-
placeholder: DEFAULT_PATH,
|
|
790
|
-
initialValue: "",
|
|
791
|
-
validate: (value) => {
|
|
792
|
-
if (value.trim() === "") {
|
|
793
|
-
return "Source is required";
|
|
794
|
-
}
|
|
795
|
-
if (!isUrl(value)) {
|
|
796
|
-
if (!fs3.existsSync(value)) {
|
|
797
|
-
return "It appears that you are specifying a local file, but the file cannot be found. hint: when specifying a url, start with http or https.";
|
|
798
|
-
}
|
|
799
|
-
if (fs3.statSync(value).isDirectory()) {
|
|
800
|
-
return "You are specifying a local file, but you cannot specify a directory. hint: when specifying a url, start with http or https.";
|
|
801
|
-
}
|
|
802
|
-
}
|
|
3336
|
+
var yamlString = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
3337
|
+
var toFrontmatter = (metadata) => {
|
|
3338
|
+
const lines = [];
|
|
3339
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
3340
|
+
if (typeof value === "string" && value.length > 0) {
|
|
3341
|
+
lines.push(`${key}: ${yamlString(value)}`);
|
|
803
3342
|
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
3343
|
+
}
|
|
3344
|
+
if (lines.length === 0) {
|
|
3345
|
+
return "";
|
|
3346
|
+
}
|
|
3347
|
+
return `---
|
|
3348
|
+
${lines.join("\n")}
|
|
3349
|
+
---
|
|
3350
|
+
|
|
3351
|
+
`;
|
|
807
3352
|
};
|
|
808
3353
|
|
|
809
|
-
// src/
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
3354
|
+
// src/html-to-markdown.ts
|
|
3355
|
+
var htmlToMarkdownWithMetadata = (htmlOrHast, options) => {
|
|
3356
|
+
const { baseUrl, frontmatter, title, formatting: toMarkdownOptions, ...toMdastOptions } = options || {};
|
|
3357
|
+
const hast = typeof htmlOrHast === "string" ? fromHtml2(htmlOrHast, { fragment: true }) : htmlOrHast;
|
|
3358
|
+
const isOwned = typeof htmlOrHast === "string";
|
|
3359
|
+
const declaredLang = typeof htmlOrHast === "string" ? getLangFromStr(htmlOrHast) : void 0;
|
|
3360
|
+
const metadata = extractMetadata(hast);
|
|
3361
|
+
if (declaredLang && !metadata.lang) {
|
|
3362
|
+
metadata.lang = declaredLang;
|
|
3363
|
+
}
|
|
3364
|
+
const mdast = htmlToMdast(hast, {
|
|
3365
|
+
...toMdastOptions,
|
|
3366
|
+
owned: isOwned,
|
|
3367
|
+
lang: toMdastOptions.lang ?? metadata.lang,
|
|
3368
|
+
url: toMdastOptions.url ?? metadata.canonicalUrl
|
|
816
3369
|
});
|
|
817
|
-
|
|
818
|
-
|
|
3370
|
+
const body = mdastToMarkdown(mdast, { baseUrl, ...toMarkdownOptions });
|
|
3371
|
+
const titled = title === false ? body : withTitle(body, metadata.title);
|
|
3372
|
+
const markdown = frontmatter ? `${toFrontmatter(metadata)}${titled}` : titled;
|
|
3373
|
+
return { markdown, metadata };
|
|
819
3374
|
};
|
|
3375
|
+
var TITLE_LOOKAHEAD = 400;
|
|
3376
|
+
var withTitle = (body, title) => {
|
|
3377
|
+
if (!title) {
|
|
3378
|
+
return body;
|
|
3379
|
+
}
|
|
3380
|
+
const opening = body.slice(0, TITLE_LOOKAHEAD);
|
|
3381
|
+
if (/^#\s+\S/m.test(opening)) {
|
|
3382
|
+
return body;
|
|
3383
|
+
}
|
|
3384
|
+
if (normalizeForCompare(opening).includes(normalizeForCompare(title))) {
|
|
3385
|
+
return body;
|
|
3386
|
+
}
|
|
3387
|
+
return `# ${title}
|
|
820
3388
|
|
|
821
|
-
|
|
822
|
-
import { select as select5 } from "@clack/prompts";
|
|
823
|
-
var loadersHint = {
|
|
824
|
-
fetch: "Fetch HTML content from the given URL",
|
|
825
|
-
playwright: "Retrieve HTML content after rendering using Playwright; Playwright must be installed in advance.",
|
|
826
|
-
puppeteer: "Retrieve HTML content after rendering using Puppeteer; Puppeteer must be installed in advance."
|
|
827
|
-
};
|
|
828
|
-
var selectLoader = async () => {
|
|
829
|
-
const result = await select5({
|
|
830
|
-
message: "Select loader:",
|
|
831
|
-
initialValue: "fetch",
|
|
832
|
-
options: LOADERS.map((mode) => ({ value: mode, label: mode, hint: loadersHint[mode] || "" }))
|
|
833
|
-
});
|
|
834
|
-
assertContinue(result);
|
|
835
|
-
return result;
|
|
3389
|
+
${body}`;
|
|
836
3390
|
};
|
|
3391
|
+
var normalizeForCompare = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
837
3392
|
|
|
838
3393
|
// src/cli/commands/webforai/loadHtml.ts
|
|
839
|
-
import
|
|
3394
|
+
import fs5 from "node:fs/promises";
|
|
840
3395
|
import { fileURLToPath } from "node:url";
|
|
841
|
-
import { log } from "@clack/prompts";
|
|
842
3396
|
import boxen from "boxen";
|
|
843
3397
|
import pc from "picocolors";
|
|
844
3398
|
import { chromium as chromium2 } from "playwright-core";
|
|
@@ -885,10 +3439,10 @@ var loadHtml2 = async (url, options) => {
|
|
|
885
3439
|
await page.goto(url, { waitUntil: waitUntil ?? "load", timeout });
|
|
886
3440
|
await page.evaluate(() => {
|
|
887
3441
|
const elements = document.querySelectorAll("*");
|
|
888
|
-
for (const
|
|
889
|
-
const rect =
|
|
890
|
-
|
|
891
|
-
|
|
3442
|
+
for (const element3 of elements) {
|
|
3443
|
+
const rect = element3.getBoundingClientRect();
|
|
3444
|
+
element3.setAttribute("data-rwidth", rect.width.toString());
|
|
3445
|
+
element3.setAttribute("data-rheight", rect.height.toString());
|
|
892
3446
|
}
|
|
893
3447
|
});
|
|
894
3448
|
const html = await page.content();
|
|
@@ -902,37 +3456,42 @@ var loadHtml2 = async (url, options) => {
|
|
|
902
3456
|
};
|
|
903
3457
|
|
|
904
3458
|
// src/cli/commands/webforai/loadHtml.ts
|
|
3459
|
+
var debugLog = (enabled, message) => {
|
|
3460
|
+
if (enabled) {
|
|
3461
|
+
console.error(`[webforai] ${message}`);
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
905
3464
|
var checkPlaywrightAvailable = async () => {
|
|
906
|
-
const
|
|
3465
|
+
const path4 = chromium2.executablePath();
|
|
907
3466
|
try {
|
|
908
|
-
await
|
|
3467
|
+
await fs5.access(path4);
|
|
909
3468
|
return true;
|
|
910
3469
|
} catch {
|
|
911
3470
|
return false;
|
|
912
3471
|
}
|
|
913
3472
|
};
|
|
914
3473
|
var getPlaywrightVersion = async () => {
|
|
915
|
-
const
|
|
916
|
-
const pwPackageJson = await
|
|
3474
|
+
const path4 = await import.meta.resolve("playwright-core/package.json");
|
|
3475
|
+
const pwPackageJson = await fs5.readFile(fileURLToPath(path4), "utf-8").then((res) => JSON.parse(res.toString())).catch(() => null);
|
|
917
3476
|
return pwPackageJson?.version;
|
|
918
3477
|
};
|
|
919
3478
|
var loadHtml3 = async (sourcePath, loader, options) => {
|
|
920
3479
|
if (loader === "local") {
|
|
921
|
-
options.debug
|
|
922
|
-
const content = await
|
|
923
|
-
options.debug
|
|
3480
|
+
debugLog(options.debug, `Loading HTML from local file: ${sourcePath}`);
|
|
3481
|
+
const content = await fs5.readFile(sourcePath, "utf-8");
|
|
3482
|
+
debugLog(options.debug, `HTML loaded: ${content.slice(0, 100)}`);
|
|
924
3483
|
return content;
|
|
925
3484
|
}
|
|
926
3485
|
if (loader === "fetch") {
|
|
927
|
-
options.debug
|
|
3486
|
+
debugLog(options.debug, `Loading HTML from URL: ${sourcePath}`);
|
|
928
3487
|
const content = await loadHtml(sourcePath);
|
|
929
|
-
options.debug
|
|
3488
|
+
debugLog(options.debug, `HTML loaded: ${content.slice(0, 100)}`);
|
|
930
3489
|
return content;
|
|
931
3490
|
}
|
|
932
3491
|
if (loader === "playwright") {
|
|
933
|
-
options.debug
|
|
3492
|
+
debugLog(options.debug, `Loading HTML with playwright: ${sourcePath}`);
|
|
934
3493
|
const isPlaywrightAvailable = await checkPlaywrightAvailable();
|
|
935
|
-
options.debug
|
|
3494
|
+
debugLog(options.debug, `Playwright available: ${isPlaywrightAvailable}`);
|
|
936
3495
|
const pwVersion = await getPlaywrightVersion();
|
|
937
3496
|
if (!isPlaywrightAvailable) {
|
|
938
3497
|
const message = [
|
|
@@ -952,56 +3511,297 @@ var loadHtml3 = async (sourcePath, loader, options) => {
|
|
|
952
3511
|
"",
|
|
953
3512
|
` sudo npx playwright@${pwVersion} install-deps`
|
|
954
3513
|
];
|
|
955
|
-
|
|
3514
|
+
console.error(boxen(message.join("\n"), { padding: 1, borderStyle: "round" }));
|
|
956
3515
|
throw new Error("Playwright is not available");
|
|
957
3516
|
}
|
|
958
3517
|
const content = await loadHtml2(sourcePath);
|
|
959
|
-
options.debug
|
|
3518
|
+
debugLog(options.debug, `HTML loaded: ${content.slice(0, 100)}`);
|
|
960
3519
|
return content;
|
|
961
3520
|
}
|
|
962
3521
|
throw new Error(`Unsupported loader: ${loader}`);
|
|
963
3522
|
};
|
|
964
3523
|
|
|
965
|
-
// src/cli/commands/webforai/
|
|
966
|
-
var
|
|
967
|
-
|
|
968
|
-
|
|
3524
|
+
// src/cli/commands/webforai/run.ts
|
|
3525
|
+
var debugLog2 = (enabled, message) => {
|
|
3526
|
+
if (enabled) {
|
|
3527
|
+
console.error(`[webforai] ${message}`);
|
|
3528
|
+
}
|
|
3529
|
+
};
|
|
3530
|
+
var AI_MODE_OPTIONS = { linkAsText: true, tableAsText: true, hideImage: true };
|
|
3531
|
+
var extractorOptions = (name) => {
|
|
3532
|
+
switch (name) {
|
|
3533
|
+
case "takumi":
|
|
3534
|
+
return takumiExtractor;
|
|
3535
|
+
case "minimal":
|
|
3536
|
+
return minimalFilter;
|
|
3537
|
+
case "none":
|
|
3538
|
+
return false;
|
|
3539
|
+
default:
|
|
3540
|
+
return void 0;
|
|
3541
|
+
}
|
|
3542
|
+
};
|
|
3543
|
+
var convertViaPlatform = async (run) => {
|
|
3544
|
+
const platform = createPlatformClient({ apiKey: run.apiKey, baseUrl: run.platformUrl });
|
|
3545
|
+
if (run.mode !== "default") {
|
|
3546
|
+
console.error("[webforai] --mode is applied by local conversion only and is ignored by the platform loader");
|
|
3547
|
+
}
|
|
3548
|
+
debugLog2(run.debug, `platform scrape: ${run.source} (engine=${run.engine ?? "fetch"})`);
|
|
3549
|
+
const result = await platform.scrape({
|
|
3550
|
+
url: run.source,
|
|
3551
|
+
engine: run.engine,
|
|
3552
|
+
region: run.region,
|
|
3553
|
+
screenshot: run.screenshot ? true : void 0,
|
|
3554
|
+
convert: { extractor: run.extractor, frontmatter: run.frontmatter }
|
|
3555
|
+
});
|
|
3556
|
+
return {
|
|
3557
|
+
source: run.source,
|
|
3558
|
+
loader: "platform",
|
|
3559
|
+
url: result.url,
|
|
3560
|
+
engine: result.engine,
|
|
3561
|
+
region: run.region,
|
|
3562
|
+
markdown: result.markdown,
|
|
3563
|
+
metadata: result.metadata,
|
|
3564
|
+
credits: result.credits,
|
|
3565
|
+
screenshotUrl: result.screenshotUrl
|
|
3566
|
+
};
|
|
3567
|
+
};
|
|
3568
|
+
var convertLocally = async (run) => {
|
|
3569
|
+
const html = await loadHtml3(run.source, run.loader, { debug: run.debug });
|
|
3570
|
+
const sourceUrl = isUrl(run.source) ? run.source : void 0;
|
|
3571
|
+
const { markdown, metadata } = htmlToMarkdownWithMetadata(html, {
|
|
3572
|
+
baseUrl: sourceUrl,
|
|
3573
|
+
url: sourceUrl,
|
|
3574
|
+
frontmatter: run.frontmatter,
|
|
3575
|
+
extractors: extractorOptions(run.extractor),
|
|
3576
|
+
...run.mode === "ai" ? AI_MODE_OPTIONS : {}
|
|
3577
|
+
});
|
|
3578
|
+
return {
|
|
3579
|
+
source: run.source,
|
|
3580
|
+
loader: run.loader,
|
|
3581
|
+
url: sourceUrl,
|
|
3582
|
+
markdown,
|
|
3583
|
+
metadata
|
|
3584
|
+
};
|
|
3585
|
+
};
|
|
3586
|
+
var executeRun = (run) => run.loader === "platform" ? convertViaPlatform(run) : convertLocally(run);
|
|
3587
|
+
var writeOutputFile = async (outputPath, markdown) => {
|
|
3588
|
+
await fs6.mkdir(path3.dirname(path3.resolve(outputPath)), { recursive: true });
|
|
3589
|
+
await fs6.writeFile(outputPath, markdown);
|
|
3590
|
+
};
|
|
3591
|
+
var runCommand = async (run) => {
|
|
3592
|
+
const envelope = await executeRun(run);
|
|
3593
|
+
if (run.output) {
|
|
3594
|
+
await writeOutputFile(run.output, envelope.markdown);
|
|
3595
|
+
envelope.output = run.output;
|
|
3596
|
+
console.error(`[webforai] markdown written to ${run.output}`);
|
|
3597
|
+
}
|
|
3598
|
+
if (run.json) {
|
|
3599
|
+
process.stdout.write(`${JSON.stringify(envelope, null, 2)}
|
|
3600
|
+
`);
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3603
|
+
if (!run.output) {
|
|
3604
|
+
process.stdout.write(envelope.markdown.endsWith("\n") ? envelope.markdown : `${envelope.markdown}
|
|
3605
|
+
`);
|
|
3606
|
+
}
|
|
3607
|
+
if (envelope.screenshotUrl) {
|
|
3608
|
+
console.error(`[webforai] screenshot (expires ~24h): ${envelope.screenshotUrl}`);
|
|
3609
|
+
}
|
|
3610
|
+
if (envelope.credits !== void 0) {
|
|
3611
|
+
console.error(`[webforai] credits used: ${envelope.credits}`);
|
|
3612
|
+
}
|
|
3613
|
+
};
|
|
3614
|
+
|
|
3615
|
+
// src/cli/commands/webforai/interactive.ts
|
|
3616
|
+
var ENGINE_HINTS = {
|
|
3617
|
+
fetch: "Plain fetch \xB7 1 credit",
|
|
3618
|
+
browser: "Browser rendering, screenshots \xB7 5 credits",
|
|
3619
|
+
"proxy-fetch": "Rotating-proxy egress \xB7 2 credits",
|
|
3620
|
+
"proxy-browser": "Browser rendering behind the proxy \xB7 5 credits"
|
|
3621
|
+
};
|
|
3622
|
+
var promptPlatformOptions = async (flags, env) => {
|
|
3623
|
+
const engine = flags.engine ?? await select7({
|
|
3624
|
+
message: "Select platform engine:",
|
|
3625
|
+
initialValue: "fetch",
|
|
3626
|
+
options: ENGINES.map((value) => ({ value, label: value, hint: ENGINE_HINTS[value] }))
|
|
3627
|
+
});
|
|
3628
|
+
assertContinue(engine);
|
|
3629
|
+
const region = flags.region ?? await select7({
|
|
3630
|
+
message: "Select egress region (proxy engines only):",
|
|
3631
|
+
initialValue: "auto",
|
|
3632
|
+
options: REGIONS.map((value) => ({ value, label: value }))
|
|
3633
|
+
});
|
|
3634
|
+
assertContinue(region);
|
|
3635
|
+
let apiKey = flags.apiKey ?? env[API_KEY_ENV];
|
|
3636
|
+
if (!apiKey) {
|
|
3637
|
+
const entered = await text5({
|
|
3638
|
+
message: `Enter your platform API key (or set ${API_KEY_ENV}):`,
|
|
3639
|
+
placeholder: "wfa_...",
|
|
3640
|
+
validate: (value) => value.trim() === "" ? "An API key is required for the platform loader" : void 0
|
|
3641
|
+
});
|
|
3642
|
+
assertContinue(entered);
|
|
3643
|
+
apiKey = entered;
|
|
3644
|
+
}
|
|
3645
|
+
return { engine, region, apiKey };
|
|
3646
|
+
};
|
|
3647
|
+
var interactiveCommand = async (initialSource, flags, env = process.env) => {
|
|
969
3648
|
intro(pc2.bold(pc2.green(`webforai CLI version ${package_default.version}`)));
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
3649
|
+
const source = initialSource ?? await inputSourcePath();
|
|
3650
|
+
const loader = isUrl(source) ? flags.loader ?? await selectLoader() : "local";
|
|
3651
|
+
const run = {
|
|
3652
|
+
source,
|
|
3653
|
+
loader,
|
|
3654
|
+
mode: "default",
|
|
3655
|
+
extractor: flags.extractor ?? "auto",
|
|
3656
|
+
frontmatter: flags.frontmatter ?? false,
|
|
3657
|
+
json: false,
|
|
3658
|
+
debug: flags.debug ?? false,
|
|
3659
|
+
screenshot: flags.screenshot ?? false
|
|
3660
|
+
};
|
|
3661
|
+
if (loader === "platform") {
|
|
3662
|
+
const platform = await promptPlatformOptions(flags, env);
|
|
3663
|
+
run.engine = platform.engine;
|
|
3664
|
+
run.region = platform.region;
|
|
3665
|
+
run.apiKey = platform.apiKey;
|
|
3666
|
+
run.platformUrl = flags.platformUrl;
|
|
3667
|
+
} else {
|
|
3668
|
+
run.mode = flags.mode ?? await selectExtractMode();
|
|
3669
|
+
}
|
|
3670
|
+
const outputPath = flags.output ?? await inputOutputPath(source);
|
|
979
3671
|
const s = spinner();
|
|
3672
|
+
s.start(loader === "platform" ? "Converting via the platform..." : "Loading and converting...");
|
|
3673
|
+
let markdown;
|
|
980
3674
|
try {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
s.stop(pc2.green("
|
|
3675
|
+
const envelope = await executeRun(run);
|
|
3676
|
+
markdown = envelope.markdown;
|
|
3677
|
+
s.stop(pc2.green("Converted!"));
|
|
3678
|
+
if (envelope.credits !== void 0) {
|
|
3679
|
+
log.info(`Credits used: ${envelope.credits}`);
|
|
3680
|
+
}
|
|
3681
|
+
if (envelope.screenshotUrl) {
|
|
3682
|
+
log.info(`Screenshot (expires ~24h): ${envelope.screenshotUrl}`);
|
|
3683
|
+
}
|
|
984
3684
|
} catch (error) {
|
|
985
|
-
s.stop(pc2.red("
|
|
986
|
-
|
|
987
|
-
process.exit(1);
|
|
988
|
-
}
|
|
989
|
-
options.debug && log2.info(`html: ${html}`);
|
|
990
|
-
const markdown = htmlToMarkdown(html, {
|
|
991
|
-
baseUrl: isUrl(sourcePath) ? sourcePath : void 0,
|
|
992
|
-
...mode === "ai" ? aiModeOptions : readabilityModeOptions
|
|
993
|
-
});
|
|
994
|
-
options.debug && log2.info(`markdown: ${markdown}`);
|
|
995
|
-
const directory = path2.dirname(outputPath);
|
|
996
|
-
const isDirectoryExists = await fs5.stat(directory).then((stat) => stat.isDirectory());
|
|
997
|
-
if (!isDirectoryExists) {
|
|
998
|
-
await fs5.mkdir(directory, { recursive: true });
|
|
3685
|
+
s.stop(pc2.red("Conversion failed!"));
|
|
3686
|
+
throw error;
|
|
999
3687
|
}
|
|
1000
|
-
await
|
|
3688
|
+
await writeOutputFile(outputPath, markdown);
|
|
1001
3689
|
outro(pc2.green(`${pc2.bold("Done!")} Markdown saved to ${outputPath}`));
|
|
1002
3690
|
};
|
|
1003
3691
|
|
|
3692
|
+
// src/cli/commands/webforai/options.ts
|
|
3693
|
+
import fs7 from "node:fs";
|
|
3694
|
+
var UsageError = class extends Error {
|
|
3695
|
+
};
|
|
3696
|
+
var oneOf = (name, value, allowed) => {
|
|
3697
|
+
if (!allowed.includes(value)) {
|
|
3698
|
+
throw new UsageError(`invalid --${name} "${value}" (expected: ${allowed.join(" | ")})`);
|
|
3699
|
+
}
|
|
3700
|
+
return value;
|
|
3701
|
+
};
|
|
3702
|
+
var impliesPlatform = (flags) => flags.engine !== void 0 || flags.region !== void 0;
|
|
3703
|
+
var resolveLoader = (sourceIsUrl, flags) => {
|
|
3704
|
+
if (flags.loader) {
|
|
3705
|
+
return oneOf("loader", flags.loader, LOADERS);
|
|
3706
|
+
}
|
|
3707
|
+
if (impliesPlatform(flags)) {
|
|
3708
|
+
return "platform";
|
|
3709
|
+
}
|
|
3710
|
+
return sourceIsUrl ? "fetch" : "local";
|
|
3711
|
+
};
|
|
3712
|
+
var assertUsableLocalSource = (source, loader, flags) => {
|
|
3713
|
+
if (loader !== "local" && flags.loader) {
|
|
3714
|
+
throw new UsageError(`--loader ${loader} needs a URL source, but "${source}" is a local path`);
|
|
3715
|
+
}
|
|
3716
|
+
if (impliesPlatform(flags)) {
|
|
3717
|
+
throw new UsageError("--engine/--region need a URL source (the platform cannot read local files)");
|
|
3718
|
+
}
|
|
3719
|
+
if (!fs7.existsSync(source)) {
|
|
3720
|
+
throw new UsageError(`source not found: "${source}" (URLs must start with http:// or https://)`);
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
var applyPlatformFields = (resolved, flags, env) => {
|
|
3724
|
+
resolved.engine = flags.engine ? oneOf("engine", flags.engine, ENGINES) : void 0;
|
|
3725
|
+
resolved.region = flags.region ? oneOf("region", flags.region, REGIONS) : void 0;
|
|
3726
|
+
resolved.apiKey = flags.apiKey ?? env[API_KEY_ENV];
|
|
3727
|
+
resolved.platformUrl = flags.platformUrl ?? env[PLATFORM_URL_ENV];
|
|
3728
|
+
if (!resolved.apiKey) {
|
|
3729
|
+
throw new UsageError(
|
|
3730
|
+
`the platform loader needs an API key: pass --api-key or set ${API_KEY_ENV} (create one at https://platform.webforai.dev/dashboard)`
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
3734
|
+
var resolveRunOptions = (source, flags, env = process.env) => {
|
|
3735
|
+
const sourceIsUrl = isUrl(source);
|
|
3736
|
+
const loader = resolveLoader(sourceIsUrl, flags);
|
|
3737
|
+
if (!sourceIsUrl) {
|
|
3738
|
+
assertUsableLocalSource(source, loader, flags);
|
|
3739
|
+
}
|
|
3740
|
+
const resolved = {
|
|
3741
|
+
source,
|
|
3742
|
+
loader: sourceIsUrl ? loader : "local",
|
|
3743
|
+
mode: flags.mode ? oneOf("mode", flags.mode, MODES) : "default",
|
|
3744
|
+
extractor: flags.extractor ? oneOf("extractor", flags.extractor, EXTRACTORS) : "auto",
|
|
3745
|
+
frontmatter: flags.frontmatter ?? false,
|
|
3746
|
+
output: flags.output,
|
|
3747
|
+
json: flags.json ?? false,
|
|
3748
|
+
debug: flags.debug ?? false,
|
|
3749
|
+
screenshot: flags.screenshot ?? false
|
|
3750
|
+
};
|
|
3751
|
+
if (resolved.loader === "platform") {
|
|
3752
|
+
applyPlatformFields(resolved, flags, env);
|
|
3753
|
+
} else if (flags.screenshot) {
|
|
3754
|
+
throw new UsageError("--screenshot is only available with the platform loader (browser engines)");
|
|
3755
|
+
}
|
|
3756
|
+
return resolved;
|
|
3757
|
+
};
|
|
3758
|
+
|
|
1004
3759
|
// src/cli/bin.ts
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
3760
|
+
var HELP_EPILOGUE = `
|
|
3761
|
+
Examples:
|
|
3762
|
+
$ webforai https://example.com/article Markdown to stdout
|
|
3763
|
+
$ webforai https://example.com -o article.md write to a file
|
|
3764
|
+
$ webforai ./page.html --extractor none convert a local file, no extraction
|
|
3765
|
+
$ webforai https://example.com --json | jq .markdown
|
|
3766
|
+
$ webforai https://spa.example.com -l playwright render JS in local Chromium
|
|
3767
|
+
$ webforai https://example.com --engine browser render via the hosted platform
|
|
3768
|
+
$ webforai -i guided interactive mode
|
|
3769
|
+
$ webforai skill --install install the Agent Skill for this CLI
|
|
3770
|
+
|
|
3771
|
+
Environment:
|
|
3772
|
+
${API_KEY_ENV} platform API key (used by the platform loader)
|
|
3773
|
+
${PLATFORM_URL_ENV} platform base URL override (self-hosted deployments)
|
|
3774
|
+
|
|
3775
|
+
Exit codes:
|
|
3776
|
+
0 success \xB7 1 runtime failure \xB7 2 usage error
|
|
3777
|
+
|
|
3778
|
+
Output contract:
|
|
3779
|
+
stdout carries only the Markdown (or the --json envelope); all logs go to stderr.
|
|
3780
|
+
`;
|
|
3781
|
+
program.name("webforai").description("Convert web pages and local HTML to clean, LLM-ready Markdown.").version(package_default.version, "-v, --version", "output the current version").addHelpText("after", HELP_EPILOGUE).showSuggestionAfterError();
|
|
3782
|
+
program.argument("[source]", "URL or local HTML file to convert").option("-o, --output <path>", "write Markdown to a file instead of stdout").option("-l, --loader <loader>", `how HTML is acquired (${LOADERS.join(" | ")}; local files are automatic)`).option("-m, --mode <mode>", `conversion mode (${MODES.join(" | ")}); ai strips links/tables/images`).option("--extractor <extractor>", `main-content extraction preset (${EXTRACTORS.join(" | ")})`, void 0).option("--frontmatter", "prepend YAML front matter built from the page metadata").option("--json", "print a JSON envelope {source, loader, url, markdown, metadata, ...} to stdout").option("--engine <engine>", "platform engine (fetch | browser | proxy-fetch | proxy-browser); implies -l platform").option("--region <region>", "platform proxy egress region (auto | us | eu | uk | jp | asia); implies -l platform").option("--screenshot", "platform browser engines: also capture a screenshot (expiring URL)").option("--api-key <key>", `platform API key (defaults to $${API_KEY_ENV})`).option("--platform-url <url>", `platform base URL (defaults to $${PLATFORM_URL_ENV} or the hosted instance)`).option("-i, --interactive", "guided prompt flow (the pre-v3 default)").option("-d, --debug", "verbose logs on stderr").action(async (source, flags) => {
|
|
3783
|
+
if (flags.interactive) {
|
|
3784
|
+
await interactiveCommand(source, flags);
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
3787
|
+
if (!source) {
|
|
3788
|
+
program.help({ error: true });
|
|
3789
|
+
}
|
|
3790
|
+
await runCommand(resolveRunOptions(source, flags));
|
|
3791
|
+
});
|
|
3792
|
+
program.command("skill").description("print the Agent Skill (SKILL.md) for this CLI, or install it via the skills CLI").option("--install", "install through `npx skills add` (interactive agent picker; flags below forward to it)").option("-g, --global", "forward to skills add: install user-level instead of project-level").option("-a, --agent <agents>", "forward to skills add: agents to install to ('*' for all)").option("-y, --yes", "forward to skills add: skip confirmation prompts").option("--copy", "forward to skills add: copy files instead of symlinking").option("--all", "forward to skills add: all skills, all agents, no prompts").option("--dir <path>", "offline fallback: write <path>/webforai/SKILL.md directly, without the skills CLI").addHelpText("after", "\nAlso installable straight from the repository: npx skills add inaridiy/webforai\n").action(async (flags) => {
|
|
3793
|
+
await skillCommand(flags);
|
|
3794
|
+
});
|
|
3795
|
+
var fail = (error) => {
|
|
3796
|
+
if (error instanceof CommanderError) {
|
|
3797
|
+
process.exit(error.exitCode === 0 ? 0 : 2);
|
|
3798
|
+
}
|
|
3799
|
+
if (error instanceof UsageError) {
|
|
3800
|
+
console.error(`error: ${error.message}`);
|
|
3801
|
+
process.exit(2);
|
|
3802
|
+
}
|
|
3803
|
+
console.error(error instanceof Error ? `error: ${error.message}` : `error: ${String(error)}`);
|
|
3804
|
+
process.exit(1);
|
|
3805
|
+
};
|
|
3806
|
+
program.exitOverride();
|
|
3807
|
+
program.parseAsync().catch(fail);
|