webforai 2.1.1 → 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 +3197 -402
- 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 +5 -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 +31 -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 +3 -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 +31 -2
- 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 +1 -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,197 +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
|
+
};
|
|
186
918
|
|
|
187
|
-
// src/extractors/
|
|
188
|
-
var
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
};
|
|
2106
|
+
|
|
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;
|
|
348
2559
|
};
|
|
349
2560
|
|
|
350
2561
|
// src/extractors/presets/minimal-filter.ts
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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;
|
|
2597
|
+
};
|
|
354
2598
|
|
|
355
2599
|
// src/mdast-handlers/custom-a-handler.ts
|
|
356
2600
|
import { defaultHandlers } from "hast-util-to-mdast";
|
|
357
|
-
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
|
+
};
|
|
358
2614
|
var customAHandler = (options) => (state, node) => {
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
if (
|
|
2615
|
+
const asPlainText = () => {
|
|
2616
|
+
const text6 = hastToString(node);
|
|
2617
|
+
if (text6.length === 0) {
|
|
362
2618
|
return void 0;
|
|
363
2619
|
}
|
|
364
|
-
const
|
|
365
|
-
state.patch(node,
|
|
366
|
-
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();
|
|
367
2626
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return link;
|
|
2627
|
+
if (!isFollowableHref(stringProperty(node, "href"))) {
|
|
2628
|
+
return asPlainText();
|
|
371
2629
|
}
|
|
372
|
-
|
|
2630
|
+
const link = defaultHandlers.a(state, node);
|
|
2631
|
+
return link.children.length > 0 ? link : void 0;
|
|
373
2632
|
};
|
|
374
2633
|
|
|
375
2634
|
// src/mdast-handlers/custom-code-handler.ts
|
|
@@ -440,9 +2699,9 @@ var customCodeHandler = (state, node) => {
|
|
|
440
2699
|
};
|
|
441
2700
|
|
|
442
2701
|
// src/mdast-handlers/custom-div-handler.ts
|
|
443
|
-
import { select as
|
|
2702
|
+
import { select as select5 } from "hast-util-select";
|
|
444
2703
|
import { defaultHandlers as defaultHandlers2 } from "hast-util-to-mdast";
|
|
445
|
-
import { toString as
|
|
2704
|
+
import { toString as hastToString2 } from "hast-util-to-string";
|
|
446
2705
|
import { toText as toText2 } from "hast-util-to-text";
|
|
447
2706
|
import { trimTrailingLines as trimTrailingLines2 } from "trim-trailing-lines";
|
|
448
2707
|
var CODE_BLOCK_REGEX = /highlight-source|language-|codegroup|codeblock|code-block/i;
|
|
@@ -477,8 +2736,8 @@ var customDivHandler = (state, node) => {
|
|
|
477
2736
|
if (codeBlock && classNames.some((className) => CODE_BLOCK_REGEX.test(className))) {
|
|
478
2737
|
const codeBlockClassNames = codeBlock.type === "element" ? codeBlock.properties.className ?? [] : [];
|
|
479
2738
|
const codeValue = trimTrailingLines2(toText2(codeBlock)).trim();
|
|
480
|
-
const filenameElement =
|
|
481
|
-
const fileLang = filenameElement ?
|
|
2739
|
+
const filenameElement = select5(CODE_FILENAME_SELECTORS, node);
|
|
2740
|
+
const fileLang = filenameElement ? hastToString2(filenameElement).match(/\.(\w+)$/)?.[1] : null;
|
|
482
2741
|
const classLang = [...classNames, ...codeBlockClassNames].map((className) => {
|
|
483
2742
|
const match = LANGUAGE_MATCH_REGEX2.map((regex) => className.match(regex)).find((match2) => match2);
|
|
484
2743
|
return match?.[1];
|
|
@@ -493,51 +2752,317 @@ var customDivHandler = (state, node) => {
|
|
|
493
2752
|
|
|
494
2753
|
// src/mdast-handlers/custom-img-handler.ts
|
|
495
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
|
+
};
|
|
496
2766
|
var customImgHandler = (options) => (state, node) => {
|
|
497
2767
|
if (options?.hideImage) {
|
|
498
2768
|
return void 0;
|
|
499
2769
|
}
|
|
500
|
-
|
|
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;
|
|
501
2781
|
};
|
|
502
2782
|
|
|
503
2783
|
// src/mdast-handlers/custom-table-handler.ts
|
|
504
2784
|
import { defaultHandlers as defaultHandlers4 } from "hast-util-to-mdast";
|
|
505
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
|
+
};
|
|
506
2800
|
var customTableHandler = (options) => (state, node) => {
|
|
507
2801
|
if (options?.asText) {
|
|
508
|
-
const
|
|
509
|
-
state.patch(node,
|
|
510
|
-
return
|
|
2802
|
+
const paragraph2 = { type: "paragraph", children: [{ type: "text", value: toText3(node) }] };
|
|
2803
|
+
state.patch(node, paragraph2);
|
|
2804
|
+
return paragraph2;
|
|
511
2805
|
}
|
|
2806
|
+
clampSpans(node);
|
|
512
2807
|
return defaultHandlers4.table(state, node);
|
|
513
2808
|
};
|
|
514
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
|
+
|
|
515
2884
|
// src/mdast-handlers/math-handler.ts
|
|
516
2885
|
import { toHtml } from "hast-util-to-html";
|
|
517
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";
|
|
518
2896
|
var mathHandler = (state, node) => {
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
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}>` };
|
|
522
2913
|
state.patch(node, result);
|
|
523
2914
|
return result;
|
|
524
2915
|
};
|
|
525
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
|
+
|
|
526
3051
|
// src/utils/hast-utils.ts
|
|
527
|
-
import { select as
|
|
3052
|
+
import { select as select6, selectAll as selectAll3 } from "hast-util-select";
|
|
528
3053
|
var getLangFromHast = (node) => {
|
|
529
|
-
const html =
|
|
3054
|
+
const html = select6("html", node);
|
|
530
3055
|
if (html && typeof html.properties.lang === "string") {
|
|
531
3056
|
return html.properties.lang;
|
|
532
3057
|
}
|
|
533
3058
|
if (node.type !== "element") {
|
|
534
3059
|
return;
|
|
535
3060
|
}
|
|
536
|
-
const
|
|
537
|
-
if (
|
|
3061
|
+
const element3 = node;
|
|
3062
|
+
if (element3.tagName !== "html") {
|
|
538
3063
|
return;
|
|
539
3064
|
}
|
|
540
|
-
const langAttr =
|
|
3065
|
+
const langAttr = element3.properties.lang || element3.properties["xml:lang"];
|
|
541
3066
|
if (langAttr) {
|
|
542
3067
|
return langAttr;
|
|
543
3068
|
}
|
|
@@ -555,10 +3080,10 @@ var getUrlFromHast = (node) => {
|
|
|
555
3080
|
return void 0;
|
|
556
3081
|
}
|
|
557
3082
|
const metaTagAttributes = ["og:url", "twitter:url"];
|
|
558
|
-
const metaTags =
|
|
3083
|
+
const metaTags = selectAll3("meta", node);
|
|
559
3084
|
for (const meta of metaTags) {
|
|
560
|
-
const
|
|
561
|
-
if (typeof
|
|
3085
|
+
const property2 = meta.properties.property || meta.properties.name;
|
|
3086
|
+
if (typeof property2 === "string" && metaTagAttributes.includes(property2)) {
|
|
562
3087
|
return typeof meta.properties.content === "string" ? meta.properties.content : void 0;
|
|
563
3088
|
}
|
|
564
3089
|
}
|
|
@@ -569,13 +3094,18 @@ var getUrlFromHast = (node) => {
|
|
|
569
3094
|
var htmlToMdast = (htmlOrHast, options) => {
|
|
570
3095
|
const { extractors, url: defaultUrl, lang: defaultLang } = options || {};
|
|
571
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);
|
|
572
3098
|
const url = defaultUrl || getUrlFromHast(hast);
|
|
573
|
-
const extractedHast = pipeExtractors({ hast, lang, url }, extractors);
|
|
3099
|
+
const extractedHast = pipeExtractors({ hast, lang, url, owned: isOwned }, extractors);
|
|
3100
|
+
normalizeHast(extractedHast, options?.normalize);
|
|
574
3101
|
const mdast = toMdast(extractedHast, {
|
|
575
3102
|
handlers: {
|
|
576
3103
|
math: mathHandler,
|
|
577
3104
|
div: customDivHandler,
|
|
578
3105
|
pre: customCodeHandler,
|
|
3106
|
+
dl: definitionListHandler,
|
|
3107
|
+
sup: supSubHandler("sup"),
|
|
3108
|
+
sub: supSubHandler("sub"),
|
|
579
3109
|
a: customAHandler({ asText: options?.linkAsText }),
|
|
580
3110
|
img: customImgHandler({ hideImage: options?.hideImage }),
|
|
581
3111
|
table: customTableHandler({ asText: options?.tableAsText })
|
|
@@ -628,222 +3158,241 @@ var mdastToMarkdown = (mdast, options) => {
|
|
|
628
3158
|
return markdown;
|
|
629
3159
|
};
|
|
630
3160
|
|
|
631
|
-
// src/
|
|
632
|
-
var
|
|
633
|
-
const
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
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];
|
|
637
3213
|
};
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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());
|
|
652
3230
|
}
|
|
3231
|
+
return [];
|
|
653
3232
|
};
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const formattedNewExtension = newExtension.startsWith(".") ? newExtension : `.${newExtension}`;
|
|
658
|
-
if (fileName.startsWith(".")) {
|
|
659
|
-
const parts = fileName.split(".");
|
|
660
|
-
if (parts.length === 2) {
|
|
661
|
-
return parsedPath.slice(0, -1).concat(`${fileName}${formattedNewExtension}`).join("/");
|
|
662
|
-
}
|
|
663
|
-
parts[parts.length - 1] = newExtension.replace(/^\./, "");
|
|
664
|
-
return parsedPath.slice(0, -1).concat(parts.join(".")).join("/");
|
|
3233
|
+
var readName = (value) => {
|
|
3234
|
+
if (typeof value === "string") {
|
|
3235
|
+
return value;
|
|
665
3236
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
function urlToFilename(url) {
|
|
673
|
-
try {
|
|
674
|
-
const urlObj = new URL(url);
|
|
675
|
-
const domainParts = urlObj.hostname.split(".").reverse().reduce((acc, part, index) => {
|
|
676
|
-
if (index === 0) {
|
|
677
|
-
return acc;
|
|
678
|
-
}
|
|
679
|
-
if (acc.length >= 2) {
|
|
680
|
-
return acc;
|
|
681
|
-
}
|
|
682
|
-
if (part === "www") {
|
|
683
|
-
return acc;
|
|
3237
|
+
if (Array.isArray(value)) {
|
|
3238
|
+
for (const entry of value) {
|
|
3239
|
+
const name = readName(entry);
|
|
3240
|
+
if (name) {
|
|
3241
|
+
return name;
|
|
684
3242
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const domainString = domainParts.reverse().join("-");
|
|
688
|
-
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
689
|
-
const relevantPathParts = pathParts.slice(-2);
|
|
690
|
-
const pathString = relevantPathParts.map((part) => decodeURIComponent(part)).join("-");
|
|
691
|
-
let filename = [domainString, pathString].filter(Boolean).join("-");
|
|
692
|
-
filename = filename.toLowerCase().replace(/[<>:"/\\|?*\x00-\x1F]/g, "").replace(/[\s.]+/g, "-").replace(/^-+|-+$/g, "");
|
|
693
|
-
return filename || "output";
|
|
694
|
-
} catch {
|
|
695
|
-
return "output";
|
|
3243
|
+
}
|
|
3244
|
+
return void 0;
|
|
696
3245
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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;
|
|
700
3251
|
};
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
while (fs.existsSync(nextFilePath)) {
|
|
711
|
-
const match = firstPart.match(/_(\d+)$/);
|
|
712
|
-
if (match) {
|
|
713
|
-
counter = Number.parseInt(match[1], 10) + 1;
|
|
714
|
-
}
|
|
715
|
-
const newName = `${baseName}_${counter}${restName}`;
|
|
716
|
-
nextFilePath = path.join(directory, newName);
|
|
717
|
-
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;
|
|
718
3261
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
cancel(cancelMessage);
|
|
727
|
-
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;
|
|
728
3269
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
if (fs2.existsSync(value) && fs2.statSync(value).isDirectory()) {
|
|
742
|
-
return "No directory can be specified.";
|
|
743
|
-
}
|
|
744
|
-
if (!fs2.existsSync(value) && value.endsWith("/")) {
|
|
745
|
-
return "No directory can be specified.";
|
|
746
|
-
}
|
|
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;
|
|
747
3281
|
}
|
|
748
3282
|
});
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
+
}
|
|
752
3299
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
if (isOutputFileOverwrite) {
|
|
759
|
-
return outputPath;
|
|
3300
|
+
if (isElement(tree)) {
|
|
3301
|
+
const own = stringProperty(tree, "lang");
|
|
3302
|
+
if (own) {
|
|
3303
|
+
return own;
|
|
3304
|
+
}
|
|
760
3305
|
}
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
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;
|
|
774
3318
|
}
|
|
775
3319
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
+
};
|
|
779
3335
|
};
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
var DEFAULT_PATH = "https://example.com";
|
|
787
|
-
var LOADERS = ["fetch", "playwright"];
|
|
788
|
-
var MODES = ["default", "ai"];
|
|
789
|
-
|
|
790
|
-
// src/cli/helpers/inputSourcePath.ts
|
|
791
|
-
var inputSourcePath = async () => {
|
|
792
|
-
const result = await text2({
|
|
793
|
-
message: "Enter the URL or html path to be converted to markdown:",
|
|
794
|
-
placeholder: DEFAULT_PATH,
|
|
795
|
-
initialValue: "",
|
|
796
|
-
validate: (value) => {
|
|
797
|
-
if (value.trim() === "") {
|
|
798
|
-
return "Source is required";
|
|
799
|
-
}
|
|
800
|
-
if (!isUrl(value)) {
|
|
801
|
-
if (!fs3.existsSync(value)) {
|
|
802
|
-
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.";
|
|
803
|
-
}
|
|
804
|
-
if (fs3.statSync(value).isDirectory()) {
|
|
805
|
-
return "You are specifying a local file, but you cannot specify a directory. hint: when specifying a url, start with http or https.";
|
|
806
|
-
}
|
|
807
|
-
}
|
|
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)}`);
|
|
808
3342
|
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
|
|
3343
|
+
}
|
|
3344
|
+
if (lines.length === 0) {
|
|
3345
|
+
return "";
|
|
3346
|
+
}
|
|
3347
|
+
return `---
|
|
3348
|
+
${lines.join("\n")}
|
|
3349
|
+
---
|
|
3350
|
+
|
|
3351
|
+
`;
|
|
812
3352
|
};
|
|
813
3353
|
|
|
814
|
-
// src/
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
|
821
3369
|
});
|
|
822
|
-
|
|
823
|
-
|
|
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 };
|
|
824
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}
|
|
825
3388
|
|
|
826
|
-
|
|
827
|
-
import { select as select6 } from "@clack/prompts";
|
|
828
|
-
var loadersHint = {
|
|
829
|
-
fetch: "Fetch HTML content from the given URL",
|
|
830
|
-
playwright: "Retrieve HTML content after rendering using Playwright; Playwright must be installed in advance.",
|
|
831
|
-
puppeteer: "Retrieve HTML content after rendering using Puppeteer; Puppeteer must be installed in advance."
|
|
832
|
-
};
|
|
833
|
-
var selectLoader = async () => {
|
|
834
|
-
const result = await select6({
|
|
835
|
-
message: "Select loader:",
|
|
836
|
-
initialValue: "fetch",
|
|
837
|
-
options: LOADERS.map((mode) => ({ value: mode, label: mode, hint: loadersHint[mode] || "" }))
|
|
838
|
-
});
|
|
839
|
-
assertContinue(result);
|
|
840
|
-
return result;
|
|
3389
|
+
${body}`;
|
|
841
3390
|
};
|
|
3391
|
+
var normalizeForCompare = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
842
3392
|
|
|
843
3393
|
// src/cli/commands/webforai/loadHtml.ts
|
|
844
|
-
import
|
|
3394
|
+
import fs5 from "node:fs/promises";
|
|
845
3395
|
import { fileURLToPath } from "node:url";
|
|
846
|
-
import { log } from "@clack/prompts";
|
|
847
3396
|
import boxen from "boxen";
|
|
848
3397
|
import pc from "picocolors";
|
|
849
3398
|
import { chromium as chromium2 } from "playwright-core";
|
|
@@ -890,10 +3439,10 @@ var loadHtml2 = async (url, options) => {
|
|
|
890
3439
|
await page.goto(url, { waitUntil: waitUntil ?? "load", timeout });
|
|
891
3440
|
await page.evaluate(() => {
|
|
892
3441
|
const elements = document.querySelectorAll("*");
|
|
893
|
-
for (const
|
|
894
|
-
const rect =
|
|
895
|
-
|
|
896
|
-
|
|
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());
|
|
897
3446
|
}
|
|
898
3447
|
});
|
|
899
3448
|
const html = await page.content();
|
|
@@ -907,37 +3456,42 @@ var loadHtml2 = async (url, options) => {
|
|
|
907
3456
|
};
|
|
908
3457
|
|
|
909
3458
|
// src/cli/commands/webforai/loadHtml.ts
|
|
3459
|
+
var debugLog = (enabled, message) => {
|
|
3460
|
+
if (enabled) {
|
|
3461
|
+
console.error(`[webforai] ${message}`);
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
910
3464
|
var checkPlaywrightAvailable = async () => {
|
|
911
|
-
const
|
|
3465
|
+
const path4 = chromium2.executablePath();
|
|
912
3466
|
try {
|
|
913
|
-
await
|
|
3467
|
+
await fs5.access(path4);
|
|
914
3468
|
return true;
|
|
915
3469
|
} catch {
|
|
916
3470
|
return false;
|
|
917
3471
|
}
|
|
918
3472
|
};
|
|
919
3473
|
var getPlaywrightVersion = async () => {
|
|
920
|
-
const
|
|
921
|
-
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);
|
|
922
3476
|
return pwPackageJson?.version;
|
|
923
3477
|
};
|
|
924
3478
|
var loadHtml3 = async (sourcePath, loader, options) => {
|
|
925
3479
|
if (loader === "local") {
|
|
926
|
-
options.debug
|
|
927
|
-
const content = await
|
|
928
|
-
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)}`);
|
|
929
3483
|
return content;
|
|
930
3484
|
}
|
|
931
3485
|
if (loader === "fetch") {
|
|
932
|
-
options.debug
|
|
3486
|
+
debugLog(options.debug, `Loading HTML from URL: ${sourcePath}`);
|
|
933
3487
|
const content = await loadHtml(sourcePath);
|
|
934
|
-
options.debug
|
|
3488
|
+
debugLog(options.debug, `HTML loaded: ${content.slice(0, 100)}`);
|
|
935
3489
|
return content;
|
|
936
3490
|
}
|
|
937
3491
|
if (loader === "playwright") {
|
|
938
|
-
options.debug
|
|
3492
|
+
debugLog(options.debug, `Loading HTML with playwright: ${sourcePath}`);
|
|
939
3493
|
const isPlaywrightAvailable = await checkPlaywrightAvailable();
|
|
940
|
-
options.debug
|
|
3494
|
+
debugLog(options.debug, `Playwright available: ${isPlaywrightAvailable}`);
|
|
941
3495
|
const pwVersion = await getPlaywrightVersion();
|
|
942
3496
|
if (!isPlaywrightAvailable) {
|
|
943
3497
|
const message = [
|
|
@@ -957,56 +3511,297 @@ var loadHtml3 = async (sourcePath, loader, options) => {
|
|
|
957
3511
|
"",
|
|
958
3512
|
` sudo npx playwright@${pwVersion} install-deps`
|
|
959
3513
|
];
|
|
960
|
-
|
|
3514
|
+
console.error(boxen(message.join("\n"), { padding: 1, borderStyle: "round" }));
|
|
961
3515
|
throw new Error("Playwright is not available");
|
|
962
3516
|
}
|
|
963
3517
|
const content = await loadHtml2(sourcePath);
|
|
964
|
-
options.debug
|
|
3518
|
+
debugLog(options.debug, `HTML loaded: ${content.slice(0, 100)}`);
|
|
965
3519
|
return content;
|
|
966
3520
|
}
|
|
967
3521
|
throw new Error(`Unsupported loader: ${loader}`);
|
|
968
3522
|
};
|
|
969
3523
|
|
|
970
|
-
// src/cli/commands/webforai/
|
|
971
|
-
var
|
|
972
|
-
|
|
973
|
-
|
|
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) => {
|
|
974
3648
|
intro(pc2.bold(pc2.green(`webforai CLI version ${package_default.version}`)));
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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);
|
|
984
3671
|
const s = spinner();
|
|
3672
|
+
s.start(loader === "platform" ? "Converting via the platform..." : "Loading and converting...");
|
|
3673
|
+
let markdown;
|
|
985
3674
|
try {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
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
|
+
}
|
|
989
3684
|
} catch (error) {
|
|
990
|
-
s.stop(pc2.red("
|
|
991
|
-
|
|
992
|
-
process.exit(1);
|
|
993
|
-
}
|
|
994
|
-
options.debug && log2.info(`html: ${html}`);
|
|
995
|
-
const markdown = htmlToMarkdown(html, {
|
|
996
|
-
baseUrl: isUrl(sourcePath) ? sourcePath : void 0,
|
|
997
|
-
...mode === "ai" ? aiModeOptions : readabilityModeOptions
|
|
998
|
-
});
|
|
999
|
-
options.debug && log2.info(`markdown: ${markdown}`);
|
|
1000
|
-
const directory = path2.dirname(outputPath);
|
|
1001
|
-
const isDirectoryExists = await fs5.stat(directory).then((stat) => stat.isDirectory());
|
|
1002
|
-
if (!isDirectoryExists) {
|
|
1003
|
-
await fs5.mkdir(directory, { recursive: true });
|
|
3685
|
+
s.stop(pc2.red("Conversion failed!"));
|
|
3686
|
+
throw error;
|
|
1004
3687
|
}
|
|
1005
|
-
await
|
|
3688
|
+
await writeOutputFile(outputPath, markdown);
|
|
1006
3689
|
outro(pc2.green(`${pc2.bold("Done!")} Markdown saved to ${outputPath}`));
|
|
1007
3690
|
};
|
|
1008
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
|
+
|
|
1009
3759
|
// src/cli/bin.ts
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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);
|