webforai 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -1
- package/dist/adapters/index.js +58 -0
- package/dist/adapters/registry.js +149 -0
- package/dist/adapters/sites/declarative.js +178 -0
- package/dist/adapters/sites/hackernews.js +94 -0
- package/dist/adapters/sites/youtube.js +165 -0
- package/dist/adapters/types.js +0 -0
- package/dist/bin.js +3199 -399
- package/dist/cjs/adapters/index.js +86 -0
- package/dist/cjs/adapters/registry.js +178 -0
- package/dist/cjs/adapters/sites/declarative.js +202 -0
- package/dist/cjs/adapters/sites/hackernews.js +117 -0
- package/dist/cjs/adapters/sites/youtube.js +191 -0
- package/dist/cjs/adapters/types.js +16 -0
- package/dist/cjs/constants.js +2 -2
- package/dist/cjs/extractors/index.js +8 -0
- package/dist/cjs/extractors/lib/constants.js +186 -0
- package/dist/cjs/extractors/lib/sanitize.js +425 -0
- package/dist/cjs/extractors/lib/score.js +264 -0
- package/dist/cjs/extractors/lib/terminators.js +131 -0
- package/dist/cjs/extractors/lib/ui-chrome.js +119 -0
- package/dist/cjs/extractors/pipeExtractors.js +12 -9
- package/dist/cjs/extractors/presets/auto.js +44 -0
- package/dist/cjs/extractors/presets/minimal-filter.js +27 -48
- package/dist/cjs/extractors/presets/takumi.js +84 -122
- package/dist/cjs/html-to-markdown.js +44 -7
- package/dist/cjs/html-to-mdast.js +9 -1
- package/dist/cjs/index.js +33 -2
- package/dist/cjs/mdast-handlers/custom-a-handler.js +25 -9
- package/dist/cjs/mdast-handlers/custom-img-handler.js +23 -1
- package/dist/cjs/mdast-handlers/custom-table-handler.js +16 -0
- package/dist/cjs/mdast-handlers/definition-list-handler.js +99 -0
- package/dist/cjs/mdast-handlers/math-handler.js +12 -3
- package/dist/cjs/mdast-handlers/sup-sub-handler.js +39 -0
- package/dist/cjs/metadata/index.js +223 -0
- package/dist/cjs/normalize/index.js +168 -0
- package/dist/cjs/platform/client.js +147 -0
- package/dist/cjs/platform/error.js +36 -0
- package/dist/cjs/platform/fetch.js +37 -0
- package/dist/cjs/platform/index.js +40 -0
- package/dist/cjs/platform/types.js +34 -0
- package/dist/cjs/utils/hast-fast.js +284 -0
- package/dist/constants.js +2 -2
- package/dist/extractors/index.js +5 -0
- package/dist/extractors/lib/constants.js +150 -0
- package/dist/extractors/lib/sanitize.js +415 -0
- package/dist/extractors/lib/score.js +242 -0
- package/dist/extractors/lib/terminators.js +107 -0
- package/dist/extractors/lib/ui-chrome.js +96 -0
- package/dist/extractors/pipeExtractors.js +12 -9
- package/dist/extractors/presets/auto.js +20 -0
- package/dist/extractors/presets/minimal-filter.js +35 -47
- package/dist/extractors/presets/takumi.js +91 -122
- package/dist/html-to-markdown.js +42 -6
- package/dist/html-to-mdast.js +9 -1
- package/dist/index.js +34 -3
- package/dist/mdast-handlers/custom-a-handler.js +25 -9
- package/dist/mdast-handlers/custom-img-handler.js +23 -1
- package/dist/mdast-handlers/custom-table-handler.js +16 -0
- package/dist/mdast-handlers/definition-list-handler.js +76 -0
- package/dist/mdast-handlers/math-handler.js +12 -3
- package/dist/mdast-handlers/sup-sub-handler.js +16 -0
- package/dist/metadata/index.js +198 -0
- package/dist/normalize/index.js +149 -0
- package/dist/platform/client.js +125 -0
- package/dist/platform/error.js +13 -0
- package/dist/platform/fetch.js +14 -0
- package/dist/platform/index.js +19 -0
- package/dist/platform/types.js +9 -0
- package/dist/types/adapters/index.d.ts +24 -0
- package/dist/types/adapters/index.js.map +1 -0
- package/dist/types/adapters/registry.d.ts +35 -0
- package/dist/types/adapters/registry.js.map +1 -0
- package/dist/types/adapters/sites/declarative.d.ts +13 -0
- package/dist/types/adapters/sites/declarative.js.map +1 -0
- package/dist/types/adapters/sites/hackernews.d.ts +13 -0
- package/dist/types/adapters/sites/hackernews.js.map +1 -0
- package/dist/types/adapters/sites/youtube.d.ts +31 -0
- package/dist/types/adapters/sites/youtube.js.map +1 -0
- package/dist/types/adapters/types.d.ts +99 -0
- package/dist/types/adapters/types.js.map +1 -0
- package/dist/types/cli/bin.js.map +1 -1
- package/dist/types/cli/commands/skill/content.d.ts +9 -0
- package/dist/types/cli/commands/skill/content.js.map +1 -0
- package/dist/types/cli/commands/skill/index.d.ts +19 -0
- package/dist/types/cli/commands/skill/index.js.map +1 -0
- package/dist/types/cli/commands/webforai/interactive.d.ts +7 -0
- package/dist/types/cli/commands/webforai/interactive.js.map +1 -0
- package/dist/types/cli/commands/webforai/loadHtml.js.map +1 -1
- package/dist/types/cli/commands/webforai/options.d.ts +55 -0
- package/dist/types/cli/commands/webforai/options.js.map +1 -0
- package/dist/types/cli/commands/webforai/run.d.ts +10 -0
- package/dist/types/cli/commands/webforai/run.js.map +1 -0
- package/dist/types/cli/constants.d.ts +14 -4
- package/dist/types/cli/constants.js.map +1 -1
- package/dist/types/cli/helpers/selectExtractMode.js.map +1 -1
- package/dist/types/cli/helpers/selectLoader.js.map +1 -1
- package/dist/types/constants.d.ts +7 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/types/extractors/index.d.ts +2 -0
- package/dist/types/extractors/index.js.map +1 -1
- package/dist/types/extractors/lib/constants.d.ts +81 -0
- package/dist/types/extractors/lib/constants.js.map +1 -0
- package/dist/types/extractors/lib/sanitize.d.ts +66 -0
- package/dist/types/extractors/lib/sanitize.js.map +1 -0
- package/dist/types/extractors/lib/score.d.ts +85 -0
- package/dist/types/extractors/lib/score.js.map +1 -0
- package/dist/types/extractors/lib/terminators.d.ts +26 -0
- package/dist/types/extractors/lib/terminators.js.map +1 -0
- package/dist/types/extractors/lib/ui-chrome.d.ts +18 -0
- package/dist/types/extractors/lib/ui-chrome.js.map +1 -0
- package/dist/types/extractors/pipeExtractors.d.ts +5 -0
- package/dist/types/extractors/pipeExtractors.js.map +1 -1
- package/dist/types/extractors/presets/auto.d.ts +30 -0
- package/dist/types/extractors/presets/auto.js.map +1 -0
- package/dist/types/extractors/presets/minimal-filter.d.ts +10 -2
- package/dist/types/extractors/presets/minimal-filter.js.map +1 -1
- package/dist/types/extractors/presets/takumi.d.ts +19 -3
- package/dist/types/extractors/presets/takumi.js.map +1 -1
- package/dist/types/extractors/types.d.ts +8 -0
- package/dist/types/html-to-markdown.d.ts +38 -0
- package/dist/types/html-to-markdown.js.map +1 -1
- package/dist/types/html-to-mdast.d.ts +11 -0
- package/dist/types/html-to-mdast.js.map +1 -1
- package/dist/types/index.d.ts +6 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/mdast-handlers/custom-a-handler.js.map +1 -1
- package/dist/types/mdast-handlers/custom-img-handler.js.map +1 -1
- package/dist/types/mdast-handlers/custom-table-handler.js.map +1 -1
- package/dist/types/mdast-handlers/definition-list-handler.d.ts +13 -0
- package/dist/types/mdast-handlers/definition-list-handler.js.map +1 -0
- package/dist/types/mdast-handlers/math-handler.js.map +1 -1
- package/dist/types/mdast-handlers/sup-sub-handler.d.ts +10 -0
- package/dist/types/mdast-handlers/sup-sub-handler.js.map +1 -0
- package/dist/types/metadata/index.d.ts +47 -0
- package/dist/types/metadata/index.js.map +1 -0
- package/dist/types/normalize/index.d.ts +53 -0
- package/dist/types/normalize/index.js.map +1 -0
- package/dist/types/platform/client.d.ts +54 -0
- package/dist/types/platform/client.js.map +1 -0
- package/dist/types/platform/error.d.ts +20 -0
- package/dist/types/platform/error.js.map +1 -0
- package/dist/types/platform/fetch.d.ts +34 -0
- package/dist/types/platform/fetch.js.map +1 -0
- package/dist/types/platform/index.d.ts +4 -0
- package/dist/types/platform/index.js.map +1 -0
- package/dist/types/platform/types.d.ts +135 -0
- package/dist/types/platform/types.js.map +1 -0
- package/dist/types/utils/hast-fast.d.ts +101 -0
- package/dist/types/utils/hast-fast.js.map +1 -0
- package/dist/utils/hast-fast.js +246 -0
- package/package.json +10 -3
- package/dist/types/cli/commands/webforai/index.d.ts +0 -6
- package/dist/types/cli/commands/webforai/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -35,7 +35,13 @@
|
|
|
35
35
|
## Overview
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
|
|
38
|
+
# URL or local HTML file → Markdown on stdout (add -o out.md to write a file)
|
|
39
|
+
npx webforai@latest https://example.com/article
|
|
40
|
+
|
|
41
|
+
# machine-readable output, hosted-platform fetching, agent installation
|
|
42
|
+
npx webforai@latest https://example.com --json
|
|
43
|
+
npx webforai@latest https://example.com --engine browser # platform, renders JS
|
|
44
|
+
npx webforai@latest skill --install # Agent Skill via `npx skills add`
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
or
|
|
@@ -52,6 +58,62 @@ const html = await loadHtml(url);
|
|
|
52
58
|
const markdown = htmlToMarkdown(html, { baseUrl: url });
|
|
53
59
|
```
|
|
54
60
|
|
|
61
|
+
## What v3 changes
|
|
62
|
+
|
|
63
|
+
**Better extraction.** The main-content extractor scores candidate containers the way Readability
|
|
64
|
+
does — paragraph density, link density, class weighting, ancestor propagation — and adds signals
|
|
65
|
+
the 2010 heuristics predate: semantic markup (`<article>`, `<main>`, `role="main"`, schema.org
|
|
66
|
+
`articleBody`), the rendered geometry a browser loader can supply, and CJK-aware length
|
|
67
|
+
thresholds. Across a 60-page corpus of real sites, the share of output lines that were bare links
|
|
68
|
+
— the clearest sign navigation leaked in — dropped from 10.6% to 5.0%.
|
|
69
|
+
|
|
70
|
+
**Site adapters.** Pages that generic scoring cannot read are handled directly, matched by
|
|
71
|
+
hostname or by a platform fingerprint so self-hosted instances are covered too: GitHub, Stack
|
|
72
|
+
Overflow, npm, MDN, Zenn, Qiita, Medium, Substack, note, Hatena, WordPress, Wikipedia, Reddit,
|
|
73
|
+
YouTube, Hacker News, Docusaurus, VitePress, MkDocs, Read the Docs and GitBook. An adapter that
|
|
74
|
+
produces too little is ignored in favour of generic extraction, so a site redesign degrades
|
|
75
|
+
rather than breaks.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// Pass adapters: false for generic extraction only.
|
|
79
|
+
import { createAutoExtractor } from "webforai";
|
|
80
|
+
|
|
81
|
+
const markdown = htmlToMarkdown(html, {
|
|
82
|
+
url,
|
|
83
|
+
extractors: [createAutoExtractor({ adapters: false })],
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Page metadata.**
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { htmlToMarkdownWithMetadata } from "webforai";
|
|
91
|
+
|
|
92
|
+
const { markdown, metadata } = htmlToMarkdownWithMetadata(html, { url });
|
|
93
|
+
// metadata: { title, author, published, siteName, canonicalUrl, lang, ... }
|
|
94
|
+
|
|
95
|
+
// Or prepend it as YAML front matter:
|
|
96
|
+
const withFrontmatter = htmlToMarkdown(html, { url, frontmatter: true });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Markup fixes.** Lazily-loaded images (`data-src`, `srcset`) now resolve to a real URL instead
|
|
100
|
+
of `![alt]()`; KaTeX and MathJax expressions are no longer emitted twice; `role="heading"`
|
|
101
|
+
elements become real headings; superscripts and subscripts stay distinguishable; and definition
|
|
102
|
+
lists keep their term/definition pairing.
|
|
103
|
+
|
|
104
|
+
See the [changeset](/.changeset) for the full list, including the breaking changes.
|
|
105
|
+
|
|
106
|
+
## Platform (hosted API)
|
|
107
|
+
|
|
108
|
+
[`apps/platform`](https://github.com/inaridiy/webforai/tree/main/apps/platform) is an OSS,
|
|
109
|
+
self-hostable SaaS built on this library: a metered crawl→Markdown HTTP API on Cloudflare
|
|
110
|
+
(Workers + Workflows + Containers + Browser Run) with Better Auth accounts/API keys and
|
|
111
|
+
Stripe usage-based billing. Use it through the typed `webforai/platform` client
|
|
112
|
+
(`import { createPlatformClient } from "webforai/platform"`) or the CLI
|
|
113
|
+
(`npx webforai <url> --engine browser`); docs live at
|
|
114
|
+
[webforai.dev/platform](https://webforai.dev/platform). See its README and the design ledger
|
|
115
|
+
in `docs/specs/platform`.
|
|
116
|
+
|
|
55
117
|
## Support
|
|
56
118
|
|
|
57
119
|
- [GitHub Sponsors](https://github.com/sponsors/inaridiy)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/adapters/index.ts
|
|
2
|
+
import { stripNonContent } from "../extractors/lib/sanitize.js";
|
|
3
|
+
import { MetricsCollector, cloneHast } from "../utils/hast-fast.js";
|
|
4
|
+
import { buildPageSignature, resolveAdapterByFingerprint, resolveAdapterByHost } from "./registry.js";
|
|
5
|
+
import { DECLARATIVE_ADAPTERS } from "./sites/declarative.js";
|
|
6
|
+
import { hackernewsAdapter } from "./sites/hackernews.js";
|
|
7
|
+
import { youtubeAdapter } from "./sites/youtube.js";
|
|
8
|
+
import {
|
|
9
|
+
buildPageSignature as buildPageSignature2,
|
|
10
|
+
defineSelectorAdapter,
|
|
11
|
+
removeAll,
|
|
12
|
+
resolveAdapter,
|
|
13
|
+
selectFirst
|
|
14
|
+
} from "./registry.js";
|
|
15
|
+
import { DECLARATIVE_ADAPTERS as DECLARATIVE_ADAPTERS2 } from "./sites/declarative.js";
|
|
16
|
+
import { hackernewsAdapter as hackernewsAdapter2 } from "./sites/hackernews.js";
|
|
17
|
+
import { youtubeAdapter as youtubeAdapter2, youtubeCaptionTracks } from "./sites/youtube.js";
|
|
18
|
+
var BUILTIN_ADAPTERS = [youtubeAdapter, hackernewsAdapter, ...DECLARATIVE_ADAPTERS];
|
|
19
|
+
var adapterById = (id) => BUILTIN_ADAPTERS.find((adapter) => adapter.id === id);
|
|
20
|
+
var MIN_ADAPTER_TEXT = 40;
|
|
21
|
+
var runAdapters = (context, adapters = BUILTIN_ADAPTERS) => {
|
|
22
|
+
let matched = resolveAdapterByHost(context, adapters);
|
|
23
|
+
let withSignature = context;
|
|
24
|
+
if (!matched) {
|
|
25
|
+
withSignature = { ...context, signature: context.signature ?? buildPageSignature(context.hast) };
|
|
26
|
+
matched = resolveAdapterByFingerprint(withSignature, adapters);
|
|
27
|
+
}
|
|
28
|
+
const adapter = matched;
|
|
29
|
+
if (!adapter) {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
const target = withSignature.owned ? withSignature : { ...withSignature, hast: cloneHast(withSignature.hast), owned: true };
|
|
33
|
+
let extracted;
|
|
34
|
+
try {
|
|
35
|
+
extracted = adapter.extract(target);
|
|
36
|
+
} catch {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
if (!extracted) {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
stripNonContent(extracted);
|
|
43
|
+
return new MetricsCollector().textLength(extracted) >= MIN_ADAPTER_TEXT ? extracted : void 0;
|
|
44
|
+
};
|
|
45
|
+
export {
|
|
46
|
+
BUILTIN_ADAPTERS,
|
|
47
|
+
DECLARATIVE_ADAPTERS2 as DECLARATIVE_ADAPTERS,
|
|
48
|
+
adapterById,
|
|
49
|
+
buildPageSignature2 as buildPageSignature,
|
|
50
|
+
defineSelectorAdapter,
|
|
51
|
+
hackernewsAdapter2 as hackernewsAdapter,
|
|
52
|
+
removeAll,
|
|
53
|
+
resolveAdapter,
|
|
54
|
+
runAdapters,
|
|
55
|
+
selectFirst,
|
|
56
|
+
youtubeAdapter2 as youtubeAdapter,
|
|
57
|
+
youtubeCaptionTracks
|
|
58
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/adapters/registry.ts
|
|
2
|
+
import { select, selectAll } from "hast-util-select";
|
|
3
|
+
import { MetricsCollector, classList, isElement, pruneInPlace, stringProperty, walk } from "../utils/hast-fast.js";
|
|
4
|
+
var EMPTY_SIGNATURE = {
|
|
5
|
+
classes: /* @__PURE__ */ new Set(),
|
|
6
|
+
ids: /* @__PURE__ */ new Set(),
|
|
7
|
+
tags: /* @__PURE__ */ new Set(),
|
|
8
|
+
generator: "",
|
|
9
|
+
meta: ""
|
|
10
|
+
};
|
|
11
|
+
var buildPageSignature = (root) => {
|
|
12
|
+
const classes = /* @__PURE__ */ new Set();
|
|
13
|
+
const ids = /* @__PURE__ */ new Set();
|
|
14
|
+
const tags = /* @__PURE__ */ new Set();
|
|
15
|
+
const generatorParts = [];
|
|
16
|
+
const metaParts = [];
|
|
17
|
+
walk(root, (node) => {
|
|
18
|
+
if (!isElement(node)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
tags.add(node.tagName);
|
|
22
|
+
for (const name of classList(node)) {
|
|
23
|
+
classes.add(name);
|
|
24
|
+
}
|
|
25
|
+
const id = stringProperty(node, "id");
|
|
26
|
+
if (id) {
|
|
27
|
+
ids.add(id);
|
|
28
|
+
}
|
|
29
|
+
if (node.tagName === "meta") {
|
|
30
|
+
const key = stringProperty(node, "name") ?? stringProperty(node, "property") ?? "";
|
|
31
|
+
const content = stringProperty(node, "content") ?? "";
|
|
32
|
+
metaParts.push(`${key}=${content}`);
|
|
33
|
+
if (key.toLowerCase() === "generator") {
|
|
34
|
+
generatorParts.push(content);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
classes,
|
|
40
|
+
ids,
|
|
41
|
+
tags,
|
|
42
|
+
generator: generatorParts.join(" ").toLowerCase(),
|
|
43
|
+
meta: metaParts.join("\n").toLowerCase()
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
var DEFAULT_MIN_LENGTH = 40;
|
|
47
|
+
var hostOf = (url) => {
|
|
48
|
+
if (!url) {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return new URL(url).hostname;
|
|
53
|
+
} catch {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var defineSelectorAdapter = (spec) => ({
|
|
58
|
+
id: spec.id,
|
|
59
|
+
matches: (context) => {
|
|
60
|
+
const host = hostOf(context.url);
|
|
61
|
+
return Boolean(host && spec.hosts?.test(host));
|
|
62
|
+
},
|
|
63
|
+
matchesFingerprint: (context) => {
|
|
64
|
+
const signature = context.signature ?? EMPTY_SIGNATURE;
|
|
65
|
+
return spec.fingerprint?.(signature) ?? false;
|
|
66
|
+
},
|
|
67
|
+
extract: (context) => {
|
|
68
|
+
for (const selector of spec.content) {
|
|
69
|
+
const content = tryContentSelector(selector, spec, context);
|
|
70
|
+
if (content) {
|
|
71
|
+
return content;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
var tryContentSelector = (selector, spec, context) => {
|
|
78
|
+
const found = selectAll(selector, context.hast);
|
|
79
|
+
if (found.length === 0) {
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
for (const element of found) {
|
|
83
|
+
removeAll(element, spec.remove);
|
|
84
|
+
}
|
|
85
|
+
const total = found.reduce((sum, element) => sum + new MetricsCollector().textLength(element), 0);
|
|
86
|
+
if (total < (spec.minLength ?? DEFAULT_MIN_LENGTH)) {
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
const decorated = spec.decorate?.(found, context);
|
|
90
|
+
if (decorated) {
|
|
91
|
+
return decorated;
|
|
92
|
+
}
|
|
93
|
+
return found.length === 1 ? found[0] : { type: "root", children: found };
|
|
94
|
+
};
|
|
95
|
+
var removeAll = (root, selectors) => {
|
|
96
|
+
if (!selectors || selectors.length === 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const doomed = /* @__PURE__ */ new Set();
|
|
100
|
+
for (const selector of selectors) {
|
|
101
|
+
for (const element of selectAll(selector, root)) {
|
|
102
|
+
doomed.add(element);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (doomed.size > 0) {
|
|
106
|
+
pruneInPlace(root, (node) => !(isElement(node) && doomed.has(node)));
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var selectFirst = (root, selectors) => {
|
|
110
|
+
for (const selector of selectors) {
|
|
111
|
+
const found = select(selector, root);
|
|
112
|
+
if (found) {
|
|
113
|
+
return found;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return void 0;
|
|
117
|
+
};
|
|
118
|
+
var resolveAdapterByHost = (context, adapters) => {
|
|
119
|
+
for (const adapter of adapters) {
|
|
120
|
+
try {
|
|
121
|
+
if (adapter.matches(context)) {
|
|
122
|
+
return adapter;
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return void 0;
|
|
128
|
+
};
|
|
129
|
+
var resolveAdapterByFingerprint = (context, adapters) => {
|
|
130
|
+
for (const adapter of adapters) {
|
|
131
|
+
try {
|
|
132
|
+
if (adapter.matchesFingerprint?.(context)) {
|
|
133
|
+
return adapter;
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return void 0;
|
|
139
|
+
};
|
|
140
|
+
var resolveAdapter = (context, adapters) => resolveAdapterByHost(context, adapters) ?? resolveAdapterByFingerprint(context, adapters);
|
|
141
|
+
export {
|
|
142
|
+
buildPageSignature,
|
|
143
|
+
defineSelectorAdapter,
|
|
144
|
+
removeAll,
|
|
145
|
+
resolveAdapter,
|
|
146
|
+
resolveAdapterByFingerprint,
|
|
147
|
+
resolveAdapterByHost,
|
|
148
|
+
selectFirst
|
|
149
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/adapters/sites/declarative.ts
|
|
2
|
+
import { defineSelectorAdapter } from "../registry.js";
|
|
3
|
+
var COMMON_NOISE = [
|
|
4
|
+
".sr-only",
|
|
5
|
+
"[aria-hidden='true'][class*='icon']",
|
|
6
|
+
"button",
|
|
7
|
+
".social-share",
|
|
8
|
+
".share-buttons",
|
|
9
|
+
".newsletter-signup"
|
|
10
|
+
];
|
|
11
|
+
var SPECS = [
|
|
12
|
+
{
|
|
13
|
+
id: "github",
|
|
14
|
+
hosts: /(^|\.)github\.com$/,
|
|
15
|
+
// Issues and PRs expose a testid; repository landing pages render the README instead.
|
|
16
|
+
content: ["[data-testid='issue-body']", "article.markdown-body", ".markdown-body"],
|
|
17
|
+
remove: [".js-comment-edit-history", ".Details-content--hidden", "clipboard-copy", ...COMMON_NOISE],
|
|
18
|
+
minLength: 30
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "stackoverflow",
|
|
22
|
+
hosts: /(^|\.)(stackoverflow\.com|stackexchange\.com|superuser\.com|serverfault\.com|askubuntu\.com)$/,
|
|
23
|
+
content: ["#mainbar", ".question, .answer"],
|
|
24
|
+
remove: [
|
|
25
|
+
".js-post-menu",
|
|
26
|
+
".post-menu",
|
|
27
|
+
".user-info",
|
|
28
|
+
".votecell",
|
|
29
|
+
".comments-link",
|
|
30
|
+
".js-add-link",
|
|
31
|
+
...COMMON_NOISE
|
|
32
|
+
],
|
|
33
|
+
minLength: 100
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "npm",
|
|
37
|
+
hosts: /(^|\.)npmjs\.com$/,
|
|
38
|
+
content: ["#readme", "#package-tab-readme", "[class*='markdown']"],
|
|
39
|
+
remove: COMMON_NOISE,
|
|
40
|
+
minLength: 50
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "zenn",
|
|
44
|
+
hosts: /(^|\.)zenn\.dev$/,
|
|
45
|
+
content: [".znc"],
|
|
46
|
+
remove: COMMON_NOISE,
|
|
47
|
+
minLength: 50
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "qiita",
|
|
51
|
+
hosts: /(^|\.)qiita\.com$/,
|
|
52
|
+
content: [".it-MdContent", "[class*='mdContent']", "article"],
|
|
53
|
+
remove: [".code-copy", ...COMMON_NOISE],
|
|
54
|
+
minLength: 50
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "medium",
|
|
58
|
+
hosts: /(^|\.)medium\.com$/,
|
|
59
|
+
// Medium also powers custom domains; the fingerprint catches those.
|
|
60
|
+
fingerprint: (signature) => signature.meta.includes("al:android:app_name=medium"),
|
|
61
|
+
content: ["article section", "article", "[data-testid='storyContent']"],
|
|
62
|
+
remove: ["[data-testid='audioPlayButton']", "[role='separator']", ...COMMON_NOISE],
|
|
63
|
+
minLength: 200
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "substack",
|
|
67
|
+
hosts: /(^|\.)substack\.com$/,
|
|
68
|
+
// Substack's custom domains are far more common than *.substack.com.
|
|
69
|
+
fingerprint: (signature) => signature.classes.has("available-content"),
|
|
70
|
+
content: [".available-content", ".body.markup", ".post-content"],
|
|
71
|
+
remove: [".subscription-widget-wrap", ".button-wrapper", ".paywall", ...COMMON_NOISE],
|
|
72
|
+
minLength: 200
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "note",
|
|
76
|
+
hosts: /(^|\.)note\.com$/,
|
|
77
|
+
content: [".note-common-styles__textnote-body", "[class*='textnote-body']"],
|
|
78
|
+
remove: COMMON_NOISE,
|
|
79
|
+
minLength: 50
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "hatena",
|
|
83
|
+
hosts: /(^|\.)(hatenablog\.com|hatenadiary\.(com|jp)|hateblo\.jp|hatenastaff\.com)$/,
|
|
84
|
+
fingerprint: (signature) => signature.classes.has("hatenablog-entry") || signature.classes.has("hatena-body"),
|
|
85
|
+
content: [".entry-content"],
|
|
86
|
+
remove: [".entry-footer", ".social-buttons", ".hatena-asin-detail", ...COMMON_NOISE],
|
|
87
|
+
minLength: 100
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "wordpress",
|
|
91
|
+
// Deliberately fingerprint-only: WordPress powers a large share of the web on its own
|
|
92
|
+
// domains, so keying off a hostname would cover almost none of it.
|
|
93
|
+
fingerprint: (signature) => signature.generator.includes("wordpress") || signature.classes.has("wp-block-post-content") || signature.classes.has("wp-singular"),
|
|
94
|
+
content: [".wp-block-post-content", ".entry-content", ".post-content", "article .content"],
|
|
95
|
+
remove: [".sharedaddy", ".jp-relatedposts", ".wp-block-comments", "#comments", ...COMMON_NOISE],
|
|
96
|
+
minLength: 150
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "wikipedia",
|
|
100
|
+
hosts: /(^|\.)(wikipedia\.org|wikimedia\.org|wikibooks\.org|wiktionary\.org|wikisource\.org)$/,
|
|
101
|
+
fingerprint: (signature) => signature.classes.has("mw-parser-output"),
|
|
102
|
+
content: [".mw-parser-output", "#mw-content-text"],
|
|
103
|
+
remove: [
|
|
104
|
+
".mw-editsection",
|
|
105
|
+
".navbox",
|
|
106
|
+
".navbox-inner",
|
|
107
|
+
".vertical-navbox",
|
|
108
|
+
".sidebar",
|
|
109
|
+
".side-box",
|
|
110
|
+
".metadata",
|
|
111
|
+
".reflist",
|
|
112
|
+
".refbegin",
|
|
113
|
+
".sistersitebox",
|
|
114
|
+
".ambox",
|
|
115
|
+
".hatnote",
|
|
116
|
+
".shortdescription",
|
|
117
|
+
".mw-jump-link",
|
|
118
|
+
".printfooter",
|
|
119
|
+
".catlinks",
|
|
120
|
+
"#toc",
|
|
121
|
+
".toc",
|
|
122
|
+
"table.mbox-small",
|
|
123
|
+
...COMMON_NOISE
|
|
124
|
+
],
|
|
125
|
+
minLength: 200
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "reddit",
|
|
129
|
+
hosts: /(^|\.)reddit\.com$/,
|
|
130
|
+
content: ["shreddit-post [slot='text-body']", "[data-test-id='post-content']", ".Post"],
|
|
131
|
+
remove: ["faceplate-tracker[noun='comment_action_bar']", "shreddit-comment-action-row", ...COMMON_NOISE],
|
|
132
|
+
// Reddit renders most of a thread into shadow roots that never reach the serialised HTML,
|
|
133
|
+
// so a weak match here is worse than generic extraction. Decline unless a substantial
|
|
134
|
+
// post body is actually present in the document.
|
|
135
|
+
minLength: 600
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "docusaurus",
|
|
139
|
+
fingerprint: (signature) => signature.classes.has("theme-doc-markdown") || signature.classes.has("docusaurus-mt-lg"),
|
|
140
|
+
content: [".theme-doc-markdown", "article .markdown"],
|
|
141
|
+
remove: [".theme-doc-toc-mobile", ".pagination-nav", ".theme-doc-version-banner", ...COMMON_NOISE],
|
|
142
|
+
minLength: 150
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "vitepress",
|
|
146
|
+
fingerprint: (signature) => signature.classes.has("vp-doc") || signature.classes.has("VPDoc"),
|
|
147
|
+
content: [".vp-doc > div", ".vp-doc", ".VPDoc .content-container"],
|
|
148
|
+
remove: [".VPDocFooter", ".vp-doc-footer", ".header-anchor", ...COMMON_NOISE],
|
|
149
|
+
minLength: 150
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "mkdocs",
|
|
153
|
+
fingerprint: (signature) => signature.classes.has("md-content__inner"),
|
|
154
|
+
content: [".md-content__inner", "[data-md-component='content']"],
|
|
155
|
+
remove: [".md-source-file", ".md-feedback", ".headerlink", ...COMMON_NOISE],
|
|
156
|
+
minLength: 150
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "readthedocs",
|
|
160
|
+
fingerprint: (signature) => signature.classes.has("rst-content"),
|
|
161
|
+
content: [".rst-content", "[role='main']"],
|
|
162
|
+
remove: [".headerlink", ".rst-footer-buttons", ".wy-breadcrumbs", ...COMMON_NOISE],
|
|
163
|
+
minLength: 150
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "gitbook",
|
|
167
|
+
fingerprint: (signature) => signature.classes.has("gitbook-root"),
|
|
168
|
+
content: ["[data-testid='page.contentEditor']", "main article"],
|
|
169
|
+
remove: COMMON_NOISE,
|
|
170
|
+
minLength: 150
|
|
171
|
+
}
|
|
172
|
+
];
|
|
173
|
+
var DECLARATIVE_ADAPTERS = SPECS.map(defineSelectorAdapter);
|
|
174
|
+
var declarativeAdapterById = (id) => DECLARATIVE_ADAPTERS.find((adapter) => adapter.id === id);
|
|
175
|
+
export {
|
|
176
|
+
DECLARATIVE_ADAPTERS,
|
|
177
|
+
declarativeAdapterById
|
|
178
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/adapters/sites/hackernews.ts
|
|
2
|
+
import { select, selectAll } from "hast-util-select";
|
|
3
|
+
import { numericProperty, stringProperty } from "../../utils/hast-fast.js";
|
|
4
|
+
var MAX_QUOTE_DEPTH = 6;
|
|
5
|
+
var text = (value) => ({ type: "text", value });
|
|
6
|
+
var element = (tagName, children) => ({
|
|
7
|
+
type: "element",
|
|
8
|
+
tagName,
|
|
9
|
+
properties: {},
|
|
10
|
+
children
|
|
11
|
+
});
|
|
12
|
+
var commentDepth = (row) => {
|
|
13
|
+
const cell = select("td.ind", row);
|
|
14
|
+
if (!cell) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
const indent = numericProperty(cell, "indent");
|
|
18
|
+
if (indent !== void 0) {
|
|
19
|
+
return indent;
|
|
20
|
+
}
|
|
21
|
+
const spacer = select("img", cell);
|
|
22
|
+
const width = spacer ? numericProperty(spacer, "width") : void 0;
|
|
23
|
+
return width === void 0 ? 0 : Math.round(width / 40);
|
|
24
|
+
};
|
|
25
|
+
var nest = (content, depth) => {
|
|
26
|
+
let current = content;
|
|
27
|
+
for (let level = 0; level < Math.min(depth, MAX_QUOTE_DEPTH); level++) {
|
|
28
|
+
current = [element("blockquote", current)];
|
|
29
|
+
}
|
|
30
|
+
return current[0];
|
|
31
|
+
};
|
|
32
|
+
var storyNodes = (root) => {
|
|
33
|
+
const nodes = [];
|
|
34
|
+
const titleLink = select(".titleline > a", root);
|
|
35
|
+
if (titleLink) {
|
|
36
|
+
nodes.push(element("h1", [...titleLink.children]));
|
|
37
|
+
const href = stringProperty(titleLink, "href");
|
|
38
|
+
if (href && /^https?:/.test(href)) {
|
|
39
|
+
nodes.push(element("p", [{ ...titleLink, children: [text(href)] }]));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const topText = select(".toptext", root);
|
|
43
|
+
if (topText) {
|
|
44
|
+
nodes.push(element("div", [...topText.children]));
|
|
45
|
+
}
|
|
46
|
+
return nodes;
|
|
47
|
+
};
|
|
48
|
+
var commentNodes = (root) => {
|
|
49
|
+
const nodes = [];
|
|
50
|
+
for (const row of selectAll("tr.comtr, tr.athing.comtr", root)) {
|
|
51
|
+
const body = select(".commtext", row);
|
|
52
|
+
if (!body) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const author = select(".hnuser", row);
|
|
56
|
+
const header = author ? [element("strong", [...author.children])] : [];
|
|
57
|
+
const content = [
|
|
58
|
+
...header.length > 0 ? [element("p", header)] : [],
|
|
59
|
+
element("div", [...body.children])
|
|
60
|
+
];
|
|
61
|
+
nodes.push(nest(content, commentDepth(row)));
|
|
62
|
+
}
|
|
63
|
+
return nodes;
|
|
64
|
+
};
|
|
65
|
+
var hackernewsAdapter = {
|
|
66
|
+
id: "hackernews",
|
|
67
|
+
matches: ({ url }) => {
|
|
68
|
+
if (!url) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return /(^|\.)ycombinator\.com$/.test(new URL(url).hostname);
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
matchesFingerprint: ({ signature }) => Boolean(signature?.ids.has("hnmain") || signature?.classes.has("fatitem")),
|
|
78
|
+
extract: ({ hast }) => {
|
|
79
|
+
const story = storyNodes(hast);
|
|
80
|
+
const comments = commentNodes(hast);
|
|
81
|
+
if (story.length === 0 && comments.length === 0) {
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
const children = [...story];
|
|
85
|
+
if (comments.length > 0) {
|
|
86
|
+
children.push(element("h2", [text("Comments")]));
|
|
87
|
+
children.push(...comments);
|
|
88
|
+
}
|
|
89
|
+
return { type: "root", children };
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
export {
|
|
93
|
+
hackernewsAdapter
|
|
94
|
+
};
|