@tforgach/axi-fetch 0.1.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/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/cache.d.ts +8 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +47 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +112 -0
- package/dist/cli.js.map +1 -0
- package/dist/extractors/article.d.ts +12 -0
- package/dist/extractors/article.d.ts.map +1 -0
- package/dist/extractors/article.js +40 -0
- package/dist/extractors/article.js.map +1 -0
- package/dist/extractors/generic.d.ts +13 -0
- package/dist/extractors/generic.d.ts.map +1 -0
- package/dist/extractors/generic.js +46 -0
- package/dist/extractors/generic.js.map +1 -0
- package/dist/extractors/shared.d.ts +26 -0
- package/dist/extractors/shared.d.ts.map +1 -0
- package/dist/extractors/shared.js +164 -0
- package/dist/extractors/shared.js.map +1 -0
- package/dist/fetcher.d.ts +26 -0
- package/dist/fetcher.d.ts.map +1 -0
- package/dist/fetcher.js +170 -0
- package/dist/fetcher.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +14 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +54 -0
- package/dist/output.js.map +1 -0
- package/dist/typeDetector.d.ts +14 -0
- package/dist/typeDetector.d.ts.map +1 -0
- package/dist/typeDetector.js +98 -0
- package/dist/typeDetector.js.map +1 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Travis Forgach
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @tforgach/axi-fetch
|
|
2
|
+
|
|
3
|
+
Token-efficient web fetching for AI agents. `axi-fetch` fetches a URL, extracts
|
|
4
|
+
the meaningful content, and returns a compact **TOON** (Token-Oriented Object
|
|
5
|
+
Notation) response following **AXI** (Agent eXperience Interface) principles —
|
|
6
|
+
instead of raw HTML or even clean markdown.
|
|
7
|
+
|
|
8
|
+
On real content pages it delivers a large token reduction vs a readable-markdown
|
|
9
|
+
reader tool (median ~80% in-repo benchmark), by combining aggressive extraction,
|
|
10
|
+
structured code/table lifting, sensible truncation, and TOON encoding.
|
|
11
|
+
|
|
12
|
+
Built as an AXI CLI on top of
|
|
13
|
+
[`axi-sdk-js`](https://www.npmjs.com/package/axi-sdk-js) (TOON output, structured
|
|
14
|
+
errors, exit codes, self-update).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install -g @tforgach/axi-fetch # CLI
|
|
20
|
+
# or as a library:
|
|
21
|
+
npm install @tforgach/axi-fetch
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node 20+.
|
|
25
|
+
|
|
26
|
+
## CLI
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
axi-fetch <url> [flags]
|
|
30
|
+
|
|
31
|
+
--full Return full content (skip truncation)
|
|
32
|
+
--no-links Omit outbound links
|
|
33
|
+
--no-cache Bypass the on-disk response cache
|
|
34
|
+
--timeout <ms> Network timeout (default 10000)
|
|
35
|
+
--max <chars> Truncate content to N chars (default 1500)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
axi-fetch https://en.wikipedia.org/wiki/Token_bucket
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
url: "https://en.wikipedia.org/wiki/Token_bucket"
|
|
46
|
+
title: Token bucket
|
|
47
|
+
type: article
|
|
48
|
+
confidence: 0.8
|
|
49
|
+
contentLength: 7800
|
|
50
|
+
truncated: true
|
|
51
|
+
content: The token bucket is an algorithm used in packet-switched networks...
|
|
52
|
+
sections[2]{heading,level}:
|
|
53
|
+
Comparison to leaky bucket,2
|
|
54
|
+
Hierarchical token bucket,2
|
|
55
|
+
help[1]: Re-run with `--full` to get the complete content
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Failures print a structured error and use a non-zero exit code (2 for bad
|
|
59
|
+
input/flags, 1 otherwise).
|
|
60
|
+
|
|
61
|
+
## Library
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { axiFetch, extractFromHtml } from "@tforgach/axi-fetch";
|
|
65
|
+
|
|
66
|
+
const { axiResponse, toonOutput } = await axiFetch("https://example.com/article");
|
|
67
|
+
console.log(toonOutput); // TOON string (agent-facing)
|
|
68
|
+
console.log(axiResponse.type); // "article" | "documentation" | "generic"
|
|
69
|
+
|
|
70
|
+
// Or run the pipeline on HTML you already have (no network):
|
|
71
|
+
const result = extractFromHtml(html, "https://example.com/article");
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
URL → fetch (native, redirects + meta-refresh + charset) → detect type (rules)
|
|
78
|
+
→ extract (Readability / cheerio) → lift sections/code/tables/links
|
|
79
|
+
→ truncate + next-steps → TOON encode
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FetchedPage } from "./fetcher.js";
|
|
2
|
+
/** Default freshness window: repeated fetches within 15 min skip the network. */
|
|
3
|
+
export declare const DEFAULT_CACHE_TTL: number;
|
|
4
|
+
/** Return a cached page if present and newer than `ttl`; otherwise null. */
|
|
5
|
+
export declare function readCache(url: string, ttl?: number): Promise<FetchedPage | null>;
|
|
6
|
+
/** Persist a fetched page. Best-effort — write failures are swallowed. */
|
|
7
|
+
export declare function writeCache(url: string, page: FetchedPage): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,QAAc,CAAC;AAkB7C,4EAA4E;AAC5E,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,MAA0B,GAC9B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAc7B;AAED,0EAA0E;AAC1E,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9E"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
/** Default freshness window: repeated fetches within 15 min skip the network. */
|
|
6
|
+
export const DEFAULT_CACHE_TTL = 15 * 60_000;
|
|
7
|
+
/** Cache directory, resolved lazily so AXI_FETCH_CACHE_DIR / XDG can override it. */
|
|
8
|
+
function cacheDir() {
|
|
9
|
+
if (process.env.AXI_FETCH_CACHE_DIR)
|
|
10
|
+
return process.env.AXI_FETCH_CACHE_DIR;
|
|
11
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
12
|
+
return join(base, "axi-fetch");
|
|
13
|
+
}
|
|
14
|
+
function keyPath(url) {
|
|
15
|
+
const hash = createHash("sha256").update(url).digest("hex").slice(0, 32);
|
|
16
|
+
return join(cacheDir(), `${hash}.json`);
|
|
17
|
+
}
|
|
18
|
+
/** Return a cached page if present and newer than `ttl`; otherwise null. */
|
|
19
|
+
export async function readCache(url, ttl = DEFAULT_CACHE_TTL) {
|
|
20
|
+
try {
|
|
21
|
+
const entry = JSON.parse(await readFile(keyPath(url), "utf8"));
|
|
22
|
+
if (Date.now() - entry.cachedAt > ttl)
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
html: entry.html,
|
|
26
|
+
finalUrl: entry.finalUrl,
|
|
27
|
+
status: entry.status,
|
|
28
|
+
contentType: entry.contentType,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Missing/corrupt cache is a normal miss.
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Persist a fetched page. Best-effort — write failures are swallowed. */
|
|
37
|
+
export async function writeCache(url, page) {
|
|
38
|
+
try {
|
|
39
|
+
await mkdir(cacheDir(), { recursive: true });
|
|
40
|
+
const entry = { ...page, cachedAt: Date.now() };
|
|
41
|
+
await writeFile(keyPath(url), JSON.stringify(entry));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Caching is an optimization, never a hard dependency.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,iFAAiF;AACjF,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,GAAG,MAAM,CAAC;AAM7C,qFAAqF;AACrF,SAAS,QAAQ;IACf,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;IAC5E,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzE,OAAO,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;AAC1C,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAW,EACX,MAAc,iBAAiB;IAE/B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAe,CAAC;QAC7E,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,QAAQ,GAAG,GAAG;YAAE,OAAO,IAAI,CAAC;QACnD,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,WAAW,EAAE,KAAK,CAAC,WAAW;SAC/B,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,0CAA0C;QAC1C,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAW,EAAE,IAAiB;IAC7D,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAe,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QAC5D,MAAM,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,uDAAuD;IACzD,CAAC;AACH,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { AxiError, runAxiCli } from "axi-sdk-js";
|
|
3
|
+
import { axiFetch } from "./index.js";
|
|
4
|
+
import { toStructured } from "./output.js";
|
|
5
|
+
const VERSION = "0.1.0";
|
|
6
|
+
const DESCRIPTION = "Fetch a URL and get a token-efficient, agent-ready TOON response";
|
|
7
|
+
// Commands the SDK dispatches directly. Anything else in first position that
|
|
8
|
+
// isn't a flag is treated as a URL for the implicit `fetch` command.
|
|
9
|
+
const KNOWN_COMMANDS = new Set(["fetch", "update", "help"]);
|
|
10
|
+
const TOP_LEVEL_HELP = [
|
|
11
|
+
`axi-fetch — ${DESCRIPTION}`,
|
|
12
|
+
"",
|
|
13
|
+
"Usage:",
|
|
14
|
+
" axi-fetch <url> [flags]",
|
|
15
|
+
" axi-fetch fetch <url> [flags]",
|
|
16
|
+
"",
|
|
17
|
+
"Flags:",
|
|
18
|
+
" --full Return full content (skip truncation)",
|
|
19
|
+
" --no-links Omit outbound links",
|
|
20
|
+
" --no-cache Bypass the on-disk response cache",
|
|
21
|
+
" --timeout <ms> Network timeout in milliseconds (default 10000)",
|
|
22
|
+
" --max <chars> Truncate content to N chars (default 1500)",
|
|
23
|
+
"",
|
|
24
|
+
"Examples:",
|
|
25
|
+
" axi-fetch https://example.com/article",
|
|
26
|
+
" axi-fetch example.com --full --no-links",
|
|
27
|
+
].join("\n");
|
|
28
|
+
const FETCH_HELP = [
|
|
29
|
+
"axi-fetch <url> — fetch and extract a page as TOON",
|
|
30
|
+
"",
|
|
31
|
+
"Flags: --full, --no-links, --timeout <ms>, --max <chars>",
|
|
32
|
+
].join("\n");
|
|
33
|
+
function readNumber(flag, value) {
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
if (value === undefined || Number.isNaN(parsed) || parsed <= 0) {
|
|
36
|
+
throw new AxiError(`Flag ${flag} requires a positive number`, "VALIDATION_ERROR");
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function parseFetchArgs(args) {
|
|
41
|
+
let url;
|
|
42
|
+
const options = {};
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
const arg = args[i];
|
|
45
|
+
if (!arg.startsWith("-")) {
|
|
46
|
+
if (url !== undefined) {
|
|
47
|
+
throw new AxiError(`Unexpected argument: ${arg}`, "VALIDATION_ERROR");
|
|
48
|
+
}
|
|
49
|
+
url = arg;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const eq = arg.indexOf("=");
|
|
53
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq);
|
|
54
|
+
const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
55
|
+
const takeValue = () => inlineValue ?? args[++i];
|
|
56
|
+
switch (flag) {
|
|
57
|
+
case "--full":
|
|
58
|
+
options.full = true;
|
|
59
|
+
break;
|
|
60
|
+
case "--no-links":
|
|
61
|
+
options.includeLinks = false;
|
|
62
|
+
break;
|
|
63
|
+
case "--no-cache":
|
|
64
|
+
options.cache = false;
|
|
65
|
+
break;
|
|
66
|
+
case "--timeout":
|
|
67
|
+
options.timeout = readNumber(flag, takeValue());
|
|
68
|
+
break;
|
|
69
|
+
case "--max":
|
|
70
|
+
options.maxContentLength = readNumber(flag, takeValue());
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
throw new AxiError(`Unknown flag: ${flag}`, "VALIDATION_ERROR", [
|
|
74
|
+
"Run `axi-fetch --help` to see supported flags",
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!url) {
|
|
79
|
+
throw new AxiError("Missing URL argument", "VALIDATION_ERROR", [
|
|
80
|
+
"Usage: axi-fetch <url> [--full] [--no-links] [--timeout <ms>] [--max <chars>]",
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
return { url, options };
|
|
84
|
+
}
|
|
85
|
+
async function fetchCommand(args) {
|
|
86
|
+
const { url, options } = parseFetchArgs(args);
|
|
87
|
+
const { axiResponse } = await axiFetch(url, options);
|
|
88
|
+
return toStructured(axiResponse);
|
|
89
|
+
}
|
|
90
|
+
/** Treat a leading positional (a URL) as an implicit `fetch` command. */
|
|
91
|
+
function normalizeArgv(argv) {
|
|
92
|
+
const first = argv[0];
|
|
93
|
+
if (first && !first.startsWith("-") && !KNOWN_COMMANDS.has(first)) {
|
|
94
|
+
return ["fetch", ...argv];
|
|
95
|
+
}
|
|
96
|
+
return argv;
|
|
97
|
+
}
|
|
98
|
+
await runAxiCli({
|
|
99
|
+
description: DESCRIPTION,
|
|
100
|
+
version: VERSION,
|
|
101
|
+
argv: normalizeArgv(process.argv.slice(2)),
|
|
102
|
+
topLevelHelp: TOP_LEVEL_HELP,
|
|
103
|
+
commands: { fetch: fetchCommand },
|
|
104
|
+
home: () => ({
|
|
105
|
+
description: DESCRIPTION,
|
|
106
|
+
usage: "axi-fetch <url> [--full] [--no-links] [--timeout <ms>] [--max <chars>]",
|
|
107
|
+
example: "axi-fetch https://example.com/article",
|
|
108
|
+
help: ["Run `axi-fetch --help` for the full reference"],
|
|
109
|
+
}),
|
|
110
|
+
getCommandHelp: (command) => (command === "fetch" ? FETCH_HELP : null),
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Content } from "../types.js";
|
|
2
|
+
export interface ArticleExtraction {
|
|
3
|
+
title: string;
|
|
4
|
+
content: Content;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Article extraction via Mozilla Readability (the same engine as Firefox Reader
|
|
8
|
+
* View). Readability needs a real DOM, so we back it with jsdom; the readable
|
|
9
|
+
* HTML fragment is then parsed with cheerio for the outline and links.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractArticle(html: string, url: string, includeLinks: boolean): ArticleExtraction;
|
|
12
|
+
//# sourceMappingURL=article.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"article.d.ts","sourceRoot":"","sources":["../../src/extractors/article.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAW3C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,OAAO,GACpB,iBAAiB,CA+BnB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Readability } from "@mozilla/readability";
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
import { JSDOM } from "jsdom";
|
|
4
|
+
import { extractCodeBlocks, extractLinks, extractProse, extractSections, extractTables, normalizeText, } from "./shared.js";
|
|
5
|
+
import { extractGeneric } from "./generic.js";
|
|
6
|
+
/**
|
|
7
|
+
* Article extraction via Mozilla Readability (the same engine as Firefox Reader
|
|
8
|
+
* View). Readability needs a real DOM, so we back it with jsdom; the readable
|
|
9
|
+
* HTML fragment is then parsed with cheerio for the outline and links.
|
|
10
|
+
*/
|
|
11
|
+
export function extractArticle(html, url, includeLinks) {
|
|
12
|
+
const dom = new JSDOM(html, { url });
|
|
13
|
+
// keepClasses so language hints on code blocks survive into the readable HTML.
|
|
14
|
+
const reader = new Readability(dom.window.document, { keepClasses: true });
|
|
15
|
+
const parsed = reader.parse();
|
|
16
|
+
// Readability bails on pages without a clear article body; fall back cleanly.
|
|
17
|
+
if (!parsed || !parsed.textContent?.trim()) {
|
|
18
|
+
return extractGeneric(html, url, includeLinks);
|
|
19
|
+
}
|
|
20
|
+
const $ = cheerio.load(parsed.content ?? "");
|
|
21
|
+
// Pull structured blocks (outline, code, tables) BEFORE extractProse strips
|
|
22
|
+
// them from the prose. extractProse then reads block-by-block, removing
|
|
23
|
+
// tables/code/citations (also cleaning up infobox/footnote links).
|
|
24
|
+
const sections = extractSections($);
|
|
25
|
+
const codeBlocks = extractCodeBlocks($);
|
|
26
|
+
const tables = extractTables($);
|
|
27
|
+
const main = extractProse($);
|
|
28
|
+
return {
|
|
29
|
+
title: normalizeText(parsed.title || dom.window.document.title || url),
|
|
30
|
+
content: {
|
|
31
|
+
main,
|
|
32
|
+
truncated: false,
|
|
33
|
+
sections,
|
|
34
|
+
codeBlocks,
|
|
35
|
+
tables,
|
|
36
|
+
links: includeLinks ? extractLinks($, url) : [],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=article.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"article.js","sourceRoot":"","sources":["../../src/extractors/article.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAO9C;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,GAAW,EACX,YAAqB;IAErB,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IACrC,+EAA+E;IAC/E,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;IAE9B,8EAA8E;IAC9E,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,EAAE,EAAE,CAAC;QAC3C,OAAO,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IAC7C,4EAA4E;IAC5E,wEAAwE;IACxE,mEAAmE;IACnE,MAAM,QAAQ,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IACpC,MAAM,UAAU,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IAE7B,OAAO;QACL,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,GAAG,CAAC;QACtE,OAAO,EAAE;YACP,IAAI;YACJ,SAAS,EAAE,KAAK;YAChB,QAAQ;YACR,UAAU;YACV,MAAM;YACN,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE;SAChD;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Content } from "../types.js";
|
|
2
|
+
export interface GenericExtraction {
|
|
3
|
+
title: string;
|
|
4
|
+
content: Content;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Aggressive readability-style fallback. Strips page chrome (nav/header/footer),
|
|
8
|
+
* then runs the same structured extraction as the article path: code blocks and
|
|
9
|
+
* tables come out as structured data, and prose is read block-by-block (no more
|
|
10
|
+
* word-gluing from raw `.text()`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractGeneric(html: string, url: string, includeLinks: boolean): GenericExtraction;
|
|
13
|
+
//# sourceMappingURL=generic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generic.d.ts","sourceRoot":"","sources":["../../src/extractors/generic.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAU3C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAgBD;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,OAAO,GACpB,iBAAiB,CA0BnB"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as cheerio from "cheerio";
|
|
2
|
+
import { extractCodeBlocks, extractLinks, extractProse, extractSections, extractTables, normalizeText, } from "./shared.js";
|
|
3
|
+
const NOISE_SELECTORS = [
|
|
4
|
+
"script",
|
|
5
|
+
"style",
|
|
6
|
+
"noscript",
|
|
7
|
+
"nav",
|
|
8
|
+
"header",
|
|
9
|
+
"footer",
|
|
10
|
+
"aside",
|
|
11
|
+
"form",
|
|
12
|
+
"template",
|
|
13
|
+
"svg",
|
|
14
|
+
"[aria-hidden='true']",
|
|
15
|
+
].join(", ");
|
|
16
|
+
/**
|
|
17
|
+
* Aggressive readability-style fallback. Strips page chrome (nav/header/footer),
|
|
18
|
+
* then runs the same structured extraction as the article path: code blocks and
|
|
19
|
+
* tables come out as structured data, and prose is read block-by-block (no more
|
|
20
|
+
* word-gluing from raw `.text()`).
|
|
21
|
+
*/
|
|
22
|
+
export function extractGeneric(html, url, includeLinks) {
|
|
23
|
+
const $ = cheerio.load(html);
|
|
24
|
+
// Read the title before de-noising; cheerio decodes HTML entities for us.
|
|
25
|
+
const title = normalizeText($("title").first().text()) ||
|
|
26
|
+
normalizeText($("h1").first().text()) ||
|
|
27
|
+
url;
|
|
28
|
+
$(NOISE_SELECTORS).remove();
|
|
29
|
+
// Pull structured blocks before extractProse strips them from the prose.
|
|
30
|
+
const sections = extractSections($);
|
|
31
|
+
const codeBlocks = extractCodeBlocks($);
|
|
32
|
+
const tables = extractTables($);
|
|
33
|
+
const main = extractProse($);
|
|
34
|
+
return {
|
|
35
|
+
title,
|
|
36
|
+
content: {
|
|
37
|
+
main,
|
|
38
|
+
truncated: false,
|
|
39
|
+
sections,
|
|
40
|
+
codeBlocks,
|
|
41
|
+
tables,
|
|
42
|
+
links: includeLinks ? extractLinks($, url) : [],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=generic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generic.js","sourceRoot":"","sources":["../../src/extractors/generic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAEnC,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,aAAa,CAAC;AAOrB,MAAM,eAAe,GAAG;IACtB,QAAQ;IACR,OAAO;IACP,UAAU;IACV,KAAK;IACL,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,MAAM;IACN,UAAU;IACV,KAAK;IACL,sBAAsB;CACvB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,GAAW,EACX,YAAqB;IAErB,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,0EAA0E;IAC1E,MAAM,KAAK,GACT,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC;QACxC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC;QACrC,GAAG,CAAC;IACN,CAAC,CAAC,eAAe,CAAC,CAAC,MAAM,EAAE,CAAC;IAE5B,yEAAyE;IACzE,MAAM,QAAQ,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IACpC,MAAM,UAAU,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IAE7B,OAAO;QACL,KAAK;QACL,OAAO,EAAE;YACP,IAAI;YACJ,SAAS,EAAE,KAAK;YAChB,QAAQ;YACR,UAAU;YACV,MAAM;YACN,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE;SAChD;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CheerioAPI } from "cheerio";
|
|
2
|
+
import type { CodeBlock, Link, Section, Table } from "../types.js";
|
|
3
|
+
/** Collapse runs of whitespace and trim; readable text for agents. */
|
|
4
|
+
export declare function normalizeText(text: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Extract readable prose from a loaded content fragment, one block at a time so
|
|
7
|
+
* block boundaries become newlines (avoids "wordAnother" gluing from raw
|
|
8
|
+
* textContent). Nested blocks are de-duplicated by skipping any block that lives
|
|
9
|
+
* inside another prose block, since the ancestor already covers its text.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractProse($: CheerioAPI): string;
|
|
12
|
+
/** Build a heading outline (h1-h6) from a cheerio document/fragment. */
|
|
13
|
+
export declare function extractSections($: CheerioAPI, root?: string): Section[];
|
|
14
|
+
/**
|
|
15
|
+
* Collect up to MAX_LINKS meaningful outbound links, classified internal vs
|
|
16
|
+
* external relative to `baseUrl` (AXI principle 2: keep the list small).
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractLinks($: CheerioAPI, baseUrl: string, root?: string): Link[];
|
|
19
|
+
/**
|
|
20
|
+
* Pull `<pre>` code blocks out as structured data. Whitespace is preserved (no
|
|
21
|
+
* normalizeText) so indentation survives; language is inferred from class hints.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractCodeBlocks($: CheerioAPI): CodeBlock[];
|
|
24
|
+
/** Pull real data tables out as `{ headers, rows }` (a strong TOON fit). */
|
|
25
|
+
export declare function extractTables($: CheerioAPI): Table[];
|
|
26
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/extractors/shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAUnE,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAaD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAUlD;AAOD,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,SAAS,GAAG,OAAO,EAAE,CAavE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,CAAC,EAAE,UAAU,EACb,OAAO,EAAE,MAAM,EACf,IAAI,SAAS,GACZ,IAAI,EAAE,CAsCR;AAWD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,SAAS,EAAE,CAgB5D;AAKD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,GAAG,KAAK,EAAE,CA+BpD"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const MAX_LINKS = 5;
|
|
2
|
+
const MAX_SECTIONS = 25;
|
|
3
|
+
const MAX_CODE_BLOCKS = 15;
|
|
4
|
+
const MAX_CODE_CHARS = 1500;
|
|
5
|
+
const MAX_TABLES = 5;
|
|
6
|
+
const MAX_TABLE_ROWS = 30;
|
|
7
|
+
const MAX_TABLE_COLS = 10;
|
|
8
|
+
/** Collapse runs of whitespace and trim; readable text for agents. */
|
|
9
|
+
export function normalizeText(text) {
|
|
10
|
+
return text.replace(/\s+/g, " ").trim();
|
|
11
|
+
}
|
|
12
|
+
// Noise stripped before reading prose: citation markers, edit links, tables and
|
|
13
|
+
// code blocks (extracted separately as structured data), and raw styles.
|
|
14
|
+
const PROSE_NOISE = "sup.reference, .mw-editsection, .reference, table, pre, style";
|
|
15
|
+
// Block-level elements that carry article prose, read in document order.
|
|
16
|
+
const PROSE_BLOCKS = "p, li, blockquote, h2, h3, h4, h5, h6, dd";
|
|
17
|
+
/** Detect footnote/citation anchor text like "[1]" or "[a]". */
|
|
18
|
+
const FOOTNOTE_TEXT = /^\[[0-9a-z]+\]$/i;
|
|
19
|
+
/**
|
|
20
|
+
* Extract readable prose from a loaded content fragment, one block at a time so
|
|
21
|
+
* block boundaries become newlines (avoids "wordAnother" gluing from raw
|
|
22
|
+
* textContent). Nested blocks are de-duplicated by skipping any block that lives
|
|
23
|
+
* inside another prose block, since the ancestor already covers its text.
|
|
24
|
+
*/
|
|
25
|
+
export function extractProse($) {
|
|
26
|
+
$(PROSE_NOISE).remove();
|
|
27
|
+
const parts = [];
|
|
28
|
+
$(PROSE_BLOCKS).each((_, el) => {
|
|
29
|
+
if ($(el).parents(PROSE_BLOCKS).length > 0)
|
|
30
|
+
return;
|
|
31
|
+
const text = stripCitationMarkers(normalizeText($(el).text()));
|
|
32
|
+
if (text)
|
|
33
|
+
parts.push(text);
|
|
34
|
+
});
|
|
35
|
+
return parts.join("\n");
|
|
36
|
+
}
|
|
37
|
+
/** Remove inline citation/footnote markers like "[1]" or "[b]" left in prose. */
|
|
38
|
+
function stripCitationMarkers(text) {
|
|
39
|
+
return text.replace(/\[(?:\d+|[a-z])\]/gi, "").replace(/\s+/g, " ").trim();
|
|
40
|
+
}
|
|
41
|
+
/** Build a heading outline (h1-h6) from a cheerio document/fragment. */
|
|
42
|
+
export function extractSections($, root = "body") {
|
|
43
|
+
const sections = [];
|
|
44
|
+
$(`${root} h1, ${root} h2, ${root} h3, ${root} h4, ${root} h5, ${root} h6`).each((_, el) => {
|
|
45
|
+
if (sections.length >= MAX_SECTIONS)
|
|
46
|
+
return;
|
|
47
|
+
const heading = normalizeText($(el).text());
|
|
48
|
+
if (!heading)
|
|
49
|
+
return;
|
|
50
|
+
const tagName = $(el).prop("tagName") ?? "";
|
|
51
|
+
const level = Number(tagName.slice(1)) || 1;
|
|
52
|
+
sections.push({ heading, level });
|
|
53
|
+
});
|
|
54
|
+
return sections;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Collect up to MAX_LINKS meaningful outbound links, classified internal vs
|
|
58
|
+
* external relative to `baseUrl` (AXI principle 2: keep the list small).
|
|
59
|
+
*/
|
|
60
|
+
export function extractLinks($, baseUrl, root = "body") {
|
|
61
|
+
const links = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
let base;
|
|
64
|
+
try {
|
|
65
|
+
base = new URL(baseUrl);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
base = undefined;
|
|
69
|
+
}
|
|
70
|
+
$(`${root} a[href]`).each((_, el) => {
|
|
71
|
+
if (links.length >= MAX_LINKS)
|
|
72
|
+
return;
|
|
73
|
+
const href = $(el).attr("href");
|
|
74
|
+
const text = normalizeText($(el).text());
|
|
75
|
+
if (!href || !text)
|
|
76
|
+
return;
|
|
77
|
+
// Drop footnote/citation markers like "[1]" or "[a]".
|
|
78
|
+
if (FOOTNOTE_TEXT.test(text))
|
|
79
|
+
return;
|
|
80
|
+
let resolved;
|
|
81
|
+
try {
|
|
82
|
+
resolved = new URL(href, base);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:")
|
|
88
|
+
return;
|
|
89
|
+
// Drop in-page anchors (e.g. #cite_note) — same page, not an outbound link.
|
|
90
|
+
if (base && resolved.hash && resolved.origin + resolved.pathname === base.origin + base.pathname) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (seen.has(resolved.href))
|
|
94
|
+
return;
|
|
95
|
+
seen.add(resolved.href);
|
|
96
|
+
const kind = base && resolved.hostname === base.hostname ? "internal" : "external";
|
|
97
|
+
links.push({ text, url: resolved.href, kind });
|
|
98
|
+
});
|
|
99
|
+
return links;
|
|
100
|
+
}
|
|
101
|
+
// Language hint from a class token like `language-python`, `lang-js`,
|
|
102
|
+
// `highlight-source-ts`, or `brush: python`.
|
|
103
|
+
const LANG_CLASS = /(?:language|lang|highlight-source|brush)[-:\s]+([a-z0-9+#]+)/i;
|
|
104
|
+
/** Trim leading/trailing blank lines while preserving indentation. */
|
|
105
|
+
function trimCode(code) {
|
|
106
|
+
return code.replace(/^\s*\n/, "").replace(/\s+$/, "");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Pull `<pre>` code blocks out as structured data. Whitespace is preserved (no
|
|
110
|
+
* normalizeText) so indentation survives; language is inferred from class hints.
|
|
111
|
+
*/
|
|
112
|
+
export function extractCodeBlocks($) {
|
|
113
|
+
const blocks = [];
|
|
114
|
+
$("pre").each((_, el) => {
|
|
115
|
+
if (blocks.length >= MAX_CODE_BLOCKS)
|
|
116
|
+
return;
|
|
117
|
+
const $el = $(el);
|
|
118
|
+
const code = trimCode($el.text());
|
|
119
|
+
if (!code.trim())
|
|
120
|
+
return;
|
|
121
|
+
const classes = `${$el.attr("class") ?? ""} ${$el.find("code").first().attr("class") ?? ""}`;
|
|
122
|
+
const language = LANG_CLASS.exec(classes)?.[1]?.toLowerCase();
|
|
123
|
+
const clipped = code.length > MAX_CODE_CHARS ? `${code.slice(0, MAX_CODE_CHARS)}…` : code;
|
|
124
|
+
blocks.push(language ? { language, code: clipped } : { code: clipped });
|
|
125
|
+
});
|
|
126
|
+
return blocks;
|
|
127
|
+
}
|
|
128
|
+
// Layout/navigation tables that aren't real tabular content.
|
|
129
|
+
const TABLE_NOISE = ".infobox, .navbox, .metadata, .sidebar, .vertical-navbox, .ambox, [role='presentation']";
|
|
130
|
+
/** Pull real data tables out as `{ headers, rows }` (a strong TOON fit). */
|
|
131
|
+
export function extractTables($) {
|
|
132
|
+
const tables = [];
|
|
133
|
+
$("table").each((_, el) => {
|
|
134
|
+
if (tables.length >= MAX_TABLES)
|
|
135
|
+
return;
|
|
136
|
+
const $t = $(el);
|
|
137
|
+
if ($t.is(TABLE_NOISE) || $t.parents(TABLE_NOISE).length > 0)
|
|
138
|
+
return;
|
|
139
|
+
const cellText = (i, cell) => normalizeText($(cell).text());
|
|
140
|
+
let headers = $t.find("thead th").map(cellText).get();
|
|
141
|
+
if (headers.length === 0) {
|
|
142
|
+
headers = $t.find("tr").first().find("th").map(cellText).get();
|
|
143
|
+
}
|
|
144
|
+
const rowScope = $t.find("tbody tr").length ? $t.find("tbody tr") : $t.find("tr");
|
|
145
|
+
const rows = [];
|
|
146
|
+
rowScope.each((_, tr) => {
|
|
147
|
+
if (rows.length >= MAX_TABLE_ROWS)
|
|
148
|
+
return;
|
|
149
|
+
const cells = $(tr).find("td");
|
|
150
|
+
if (cells.length === 0)
|
|
151
|
+
return; // header-only row
|
|
152
|
+
const row = cells.slice(0, MAX_TABLE_COLS).map(cellText).get();
|
|
153
|
+
if (row.some((c) => c.length > 0))
|
|
154
|
+
rows.push(row);
|
|
155
|
+
});
|
|
156
|
+
// Keep only tables that are actually tabular (>=2 rows, >=2 columns).
|
|
157
|
+
const width = Math.max(headers.length, rows[0]?.length ?? 0);
|
|
158
|
+
if (rows.length >= 2 && width >= 2) {
|
|
159
|
+
tables.push({ headers: headers.slice(0, MAX_TABLE_COLS), rows });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return tables;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/extractors/shared.ts"],"names":[],"mappings":"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"}
|