@wrenautomation/falcon 0.0.0-stage → 0.3.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.
Files changed (57) hide show
  1. package/LICENSE.md +91 -0
  2. package/README.md +50 -2
  3. package/dist/app/cli.d.ts +2 -0
  4. package/dist/app/cli.js +77 -0
  5. package/dist/app/cli.js.map +1 -0
  6. package/dist/index.d.ts +10 -0
  7. package/dist/index.js +11 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/study/claims.d.ts +50 -0
  10. package/dist/study/claims.js +123 -0
  11. package/dist/study/claims.js.map +1 -0
  12. package/dist/study/drafts.d.ts +43 -0
  13. package/dist/study/drafts.js +93 -0
  14. package/dist/study/drafts.js.map +1 -0
  15. package/dist/study/evidence.d.ts +31 -0
  16. package/dist/study/evidence.js +123 -0
  17. package/dist/study/evidence.js.map +1 -0
  18. package/dist/study/grounding.d.ts +29 -0
  19. package/dist/study/grounding.js +58 -0
  20. package/dist/study/grounding.js.map +1 -0
  21. package/dist/study/index.d.ts +10 -0
  22. package/dist/study/index.js +11 -0
  23. package/dist/study/index.js.map +1 -0
  24. package/dist/study/report.d.ts +55 -0
  25. package/dist/study/report.js +56 -0
  26. package/dist/study/report.js.map +1 -0
  27. package/dist/text/entities.d.ts +2 -0
  28. package/dist/text/entities.js +4 -0
  29. package/dist/text/entities.js.map +1 -0
  30. package/dist/text/htmltext.d.ts +30 -0
  31. package/dist/text/htmltext.js +208 -0
  32. package/dist/text/htmltext.js.map +1 -0
  33. package/dist/text/index.d.ts +7 -0
  34. package/dist/text/index.js +8 -0
  35. package/dist/text/index.js.map +1 -0
  36. package/dist/text/text.d.ts +8 -0
  37. package/dist/text/text.js +38 -0
  38. package/dist/text/text.js.map +1 -0
  39. package/dist/video/gemini.d.ts +35 -0
  40. package/dist/video/gemini.js +103 -0
  41. package/dist/video/gemini.js.map +1 -0
  42. package/dist/video/index.d.ts +34 -0
  43. package/dist/video/index.js +11 -0
  44. package/dist/video/index.js.map +1 -0
  45. package/dist/video/read.d.ts +52 -0
  46. package/dist/video/read.js +99 -0
  47. package/dist/video/read.js.map +1 -0
  48. package/dist/video/shots.d.ts +33 -0
  49. package/dist/video/shots.js +151 -0
  50. package/dist/video/shots.js.map +1 -0
  51. package/dist/video/tools.d.ts +16 -0
  52. package/dist/video/tools.js +29 -0
  53. package/dist/video/tools.js.map +1 -0
  54. package/dist/video/watch.d.ts +101 -0
  55. package/dist/video/watch.js +420 -0
  56. package/dist/video/watch.js.map +1 -0
  57. package/package.json +66 -4
@@ -0,0 +1,123 @@
1
+ /**
2
+ * What a study reads and what the model is shown. Pure: the run stores the
3
+ * hits and pages, these decide which pages and which parts of them.
4
+ */
5
+ import { foldText } from "./grounding.js";
6
+ /** Pages that read as a login wall or a video player, not text. */
7
+ const UNREADABLE = [
8
+ "linkedin.com",
9
+ "facebook.com",
10
+ "instagram.com",
11
+ "x.com",
12
+ "twitter.com",
13
+ "tiktok.com",
14
+ "youtube.com",
15
+ "youtu.be",
16
+ ];
17
+ const hostOf = (url) => {
18
+ try {
19
+ const u = new URL(url);
20
+ return /^https?:$/.test(u.protocol) ? u.hostname.replace(/^www\./, "") : null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ };
26
+ /** One page, one read: no fragment, no trailing slash. */
27
+ export const pageKey = (url) => url.replace(/#.*$/, "").replace(/\/+$/, "");
28
+ /**
29
+ * The pages to read for one angle: each list's best hit, then each list's
30
+ * second, until `limit`. One list per query (and one for an answer engine's
31
+ * sources), so no single query fills the angle. Social and video pages are
32
+ * skipped; they don't read as text.
33
+ */
34
+ export function pickReads(lists, limit) {
35
+ const out = [];
36
+ const seen = new Set();
37
+ const longest = Math.max(0, ...lists.map((l) => l.length));
38
+ for (let rank = 0; rank < longest && out.length < limit; rank++) {
39
+ for (const list of lists) {
40
+ const hit = list[rank];
41
+ if (!hit)
42
+ continue;
43
+ const host = hostOf(hit.url);
44
+ if (!host || UNREADABLE.some((h) => host === h || host.endsWith(`.${h}`)))
45
+ continue;
46
+ const key = pageKey(hit.url);
47
+ if (seen.has(key))
48
+ continue;
49
+ seen.add(key);
50
+ out.push(key);
51
+ if (out.length >= limit)
52
+ break;
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+ const STOP = new Set("about after also been being between both could does doing each from have having here into more most much only other over same should some such than that their them then there these they this those through under very what when where which while with would your year years".split(" "));
58
+ /** The words worth matching in a question: four letters or more, not filler. */
59
+ export function terms(...texts) {
60
+ return new Set(texts
61
+ .flatMap((t) => foldText(t).split(/[^\p{L}\d]+/u))
62
+ .filter((w) => w.length >= 4 && !STOP.has(w)));
63
+ }
64
+ const CHUNK = 600;
65
+ /** Lines packed into chunks of about CHUNK characters, in page order. */
66
+ function chunks(text) {
67
+ const out = [];
68
+ let cur = "";
69
+ for (const line of text.split(/\n+/)) {
70
+ const l = line.trim();
71
+ if (!l)
72
+ continue;
73
+ if (cur && cur.length + l.length > CHUNK) {
74
+ out.push(cur);
75
+ cur = "";
76
+ }
77
+ cur = cur ? `${cur}\n${l}` : l;
78
+ }
79
+ if (cur)
80
+ out.push(cur);
81
+ return out;
82
+ }
83
+ /**
84
+ * The parts of a page worth showing for these terms, in page order, within
85
+ * `budget` characters. A chunk scores a point per term it has, and one more
86
+ * when it has a number (a study wants figures). A short page is shown whole.
87
+ */
88
+ export function passages(text, want, budget) {
89
+ if (text.length <= budget)
90
+ return text;
91
+ const parts = chunks(text).map((c, i) => {
92
+ const words = new Set(foldText(c).split(/[^\p{L}\d]+/u));
93
+ let score = /\d/.test(c) ? 1 : 0;
94
+ for (const t of want)
95
+ if (words.has(t))
96
+ score++;
97
+ return { i, c, score };
98
+ });
99
+ const picked = [];
100
+ let used = 0;
101
+ for (const p of [...parts].sort((a, b) => b.score - a.score || a.i - b.i)) {
102
+ if (p.score === 0)
103
+ break;
104
+ if (used + p.c.length > budget)
105
+ continue;
106
+ picked.push(p);
107
+ used += p.c.length + 2;
108
+ }
109
+ if (!picked.length)
110
+ return text.slice(0, budget);
111
+ return picked
112
+ .sort((a, b) => a.i - b.i)
113
+ .map((p) => p.c)
114
+ .join("\n\n");
115
+ }
116
+ /** How much of one page the model is shown: its best passages for every angle. */
117
+ export const PAGE_CHARS = 12_000;
118
+ /** The part of one page its claims are drawn from. */
119
+ export const pageEvidence = (page, want) => ({
120
+ url: page.url,
121
+ text: passages(page.text, want, PAGE_CHARS),
122
+ });
123
+ //# sourceMappingURL=evidence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence.js","sourceRoot":"","sources":["../../src/study/evidence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Cite or drop: a model's claim stands only on words we can find on a page we
3
+ * read. Openers and studies share these checks, so "grounded" means one thing.
4
+ */
5
+ /** A page as the model was shown it: its URL and the text it saw (already cut). */
6
+ export interface EvidencePage {
7
+ url: string;
8
+ text: string;
9
+ }
10
+ /**
11
+ * Case, whitespace, curly quotes/dashes and markdown folded, so a faithful copy
12
+ * matches: pages arrive as markdown ("**over 60%**", "[report](https://…)") and
13
+ * models quote the words without the marks.
14
+ */
15
+ export declare function foldText(s: string): string;
16
+ /** The numbers in a text, thousands commas and trailing stops dropped: "1,200." → "1200". */
17
+ export declare const numbers: (s: string) => string[];
18
+ export declare const words: (s: string) => number;
19
+ /**
20
+ * The page a quote is on: the one the model named first, then any page shown
21
+ * (a wrong URL is fixed, not fatal). Null when no page has it.
22
+ */
23
+ export declare function pageQuoted(quote: string, pages: readonly EvidencePage[], namedUrl: string | null): EvidencePage | null;
24
+ /**
25
+ * Every number in `claim` is in `quote`. A year may instead come from the page
26
+ * the quote is on, its URL or text: "Level CFO's 2026 benchmarks" names the
27
+ * report, and the quote rarely repeats it.
28
+ */
29
+ export declare const numbersQuoted: (claim: string, quote: string, page?: EvidencePage) => boolean;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Cite or drop: a model's claim stands only on words we can find on a page we
3
+ * read. Openers and studies share these checks, so "grounded" means one thing.
4
+ */
5
+ /**
6
+ * Case, whitespace, curly quotes/dashes and markdown folded, so a faithful copy
7
+ * matches: pages arrive as markdown ("**over 60%**", "[report](https://…)") and
8
+ * models quote the words without the marks.
9
+ */
10
+ export function foldText(s) {
11
+ return s
12
+ .normalize("NFKC")
13
+ .replace(/!?\[([^\]]*)\]\([^)\s]*\)/g, "$1")
14
+ .replace(/[*_`]+/g, "")
15
+ .replace(/[‘’ʼ]/g, "'")
16
+ .replace(/[“”]/g, '"')
17
+ .replace(/[‐-―]/g, "-")
18
+ .replace(/\s+/g, " ")
19
+ .trim()
20
+ .toLowerCase();
21
+ }
22
+ /** The numbers in a text, thousands commas and trailing stops dropped: "1,200." → "1200". */
23
+ export const numbers = (s) => (s.match(/\d[\d,.]*/g) ?? []).map((n) => n.replace(/[,.]+$/, "").replace(/,/g, ""));
24
+ export const words = (s) => s.trim().split(/\s+/).filter(Boolean).length;
25
+ /**
26
+ * The folded quote is on the folded page word for word, or each of its sentences
27
+ * is: the model often joins two sentences from one page into one quote.
28
+ */
29
+ function quotedFrom(quote, page) {
30
+ if (page.includes(quote))
31
+ return true;
32
+ const sentences = quote
33
+ .split(/(?<=[.;·])\s+/)
34
+ .map((f) => f.replace(/[\s.;·]+$/, ""))
35
+ .filter(Boolean);
36
+ return sentences.length > 1 && sentences.every((f) => page.includes(f));
37
+ }
38
+ /**
39
+ * The page a quote is on: the one the model named first, then any page shown
40
+ * (a wrong URL is fixed, not fatal). Null when no page has it.
41
+ */
42
+ export function pageQuoted(quote, pages, namedUrl) {
43
+ const folded = foldText(quote);
44
+ const named = pages.find((p) => p.url === namedUrl);
45
+ return ([...(named ? [named] : []), ...pages].find((p) => quotedFrom(folded, foldText(p.text))) ?? null);
46
+ }
47
+ const YEAR = /^(19|20)\d\d$/;
48
+ /**
49
+ * Every number in `claim` is in `quote`. A year may instead come from the page
50
+ * the quote is on, its URL or text: "Level CFO's 2026 benchmarks" names the
51
+ * report, and the quote rarely repeats it.
52
+ */
53
+ export const numbersQuoted = (claim, quote, page) => {
54
+ const inQuote = new Set(numbers(quote));
55
+ const onPage = new Set(page ? numbers(`${page.url} ${page.text}`) : []);
56
+ return numbers(claim).every((n) => inQuote.has(n) || (YEAR.test(n) && onPage.has(n)));
57
+ };
58
+ //# sourceMappingURL=grounding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grounding.js","sourceRoot":"","sources":["../../src/study/grounding.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A study's core: the model's prompts and answer shapes, the quote gate that keeps a claim
3
+ * only when its words are on a page it was shown, which pages to read, drafts that cite
4
+ * claims, and the report. Pure: the caller searches, stores and calls the model.
5
+ */
6
+ export * from "./claims.js";
7
+ export * from "./drafts.js";
8
+ export * from "./evidence.js";
9
+ export * from "./grounding.js";
10
+ export * from "./report.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A study's core: the model's prompts and answer shapes, the quote gate that keeps a claim
3
+ * only when its words are on a page it was shown, which pages to read, drafts that cite
4
+ * claims, and the report. Pure: the caller searches, stores and calls the model.
5
+ */
6
+ export * from "./claims.js";
7
+ export * from "./drafts.js";
8
+ export * from "./evidence.js";
9
+ export * from "./grounding.js";
10
+ export * from "./report.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/study/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * A study as markdown: each angle's claims with [n] marks and the reader's calls, the
3
+ * drafts that cite them, then every source once. Pure: the caller reads, this writes.
4
+ */
5
+ import type { DraftItem, DroppedDraft, NumberedClaim, StudyDraftSpec } from "./drafts.js";
6
+ /** A claim as the report sees it: numbered, with the reader's call on it. */
7
+ export interface ReportClaim extends NumberedClaim {
8
+ /** keep, tune or drop; null when nobody has called it. */
9
+ call: string | null;
10
+ comment: string | null;
11
+ }
12
+ export interface ReportAngle {
13
+ angle: string;
14
+ claims: readonly ReportClaim[];
15
+ }
16
+ export interface ReportDraft {
17
+ spec: StudyDraftSpec;
18
+ done: boolean;
19
+ /** Calls or new claims changed its claims since it was written. */
20
+ stale: boolean;
21
+ kept: readonly DraftItem[];
22
+ dropped: readonly DroppedDraft[];
23
+ }
24
+ /** A page someone added by hand, not found by a search. */
25
+ export interface ReportManualPage {
26
+ by: string;
27
+ at: string;
28
+ /** YYYY-MM-DD, when given. */
29
+ published: string | null;
30
+ }
31
+ /** What the report needs of a study; a richer view satisfies it. */
32
+ export interface StudyReportView {
33
+ study: {
34
+ question: string;
35
+ slug: string;
36
+ niche?: string | null;
37
+ };
38
+ angles: readonly ReportAngle[];
39
+ drafts: readonly ReportDraft[];
40
+ /** Page title by source url. */
41
+ titles: ReadonlyMap<string, string>;
42
+ manual?: ReadonlyMap<string, ReportManualPage> | null;
43
+ counts: {
44
+ queries: number;
45
+ pages: number;
46
+ unread: number;
47
+ gated: number;
48
+ waiting: number;
49
+ };
50
+ }
51
+ export interface ReportOptions {
52
+ /** The command that runs the open parts, shown when some are still to run. */
53
+ rerun?: string;
54
+ }
55
+ export declare function studyReport(view: StudyReportView, opts?: ReportOptions): string;
@@ -0,0 +1,56 @@
1
+ const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
2
+ export function studyReport(view, opts = {}) {
3
+ const { study, angles, drafts, titles, manual, counts } = view;
4
+ const rerun = opts.rerun ? `: \`${opts.rerun}\`` : "";
5
+ const claims = angles.flatMap((a) => a.claims);
6
+ const dropped = claims.filter((c) => c.call === "drop").length;
7
+ const open = counts.waiting + drafts.filter((d) => !d.done || d.stale).length;
8
+ const out = [
9
+ `# ${study.question}`,
10
+ "",
11
+ `Study \`${study.slug}\`${study.niche ? ` for ${study.niche}` : ""}. ${plural(counts.queries, "search", "searches")}, ${plural(counts.pages, "page")} read (${counts.unread} unreadable), ${plural(claims.length, "claim")} passed the quote check (${counts.gated} did not), ${dropped} dropped by you.${open ? ` ${plural(open, "part")} still to run${rerun}.` : ""}`,
12
+ ];
13
+ for (const a of angles) {
14
+ out.push("", `## ${a.angle}`, "");
15
+ if (!a.claims.length)
16
+ out.push(counts.waiting || !counts.pages
17
+ ? "_Not run yet._"
18
+ : "_Nothing on the pages read answered this._");
19
+ for (const c of a.claims) {
20
+ const call = c.call ? ` (${c.call}${c.comment ? `: ${c.comment}` : ""})` : "";
21
+ const line = `${c.claim} [${c.n}]${call}`;
22
+ out.push(c.call === "drop" ? `- ~~${line}~~` : `- ${line}`);
23
+ }
24
+ }
25
+ for (const d of drafts) {
26
+ out.push("", `## Draft: ${d.spec.key}`, "");
27
+ if (!d.done)
28
+ out.push("_Not run yet._");
29
+ else if (d.stale)
30
+ out.push(`_Your calls or new claims changed its claims${rerun}._`, "");
31
+ if (d.done && !d.kept.length)
32
+ out.push("_Nothing survived the citation check._");
33
+ for (const item of d.kept) {
34
+ out.push(`### ${item.title || "(untitled)"}`, "", item.body, "", `Cites ${item.cites.map((n) => `[${n}]`).join(" ")}`, "");
35
+ }
36
+ if (d.dropped.length) {
37
+ out.push(`_Dropped: ${d.dropped.map((x) => `${x.title || "(untitled)"} (${x.why.replace(/_/g, " ")})`).join("; ")}._`);
38
+ }
39
+ }
40
+ if (claims.length) {
41
+ out.push("", "## Sources", "");
42
+ for (const c of claims) {
43
+ const title = titles.get(c.source_url);
44
+ const m = manual?.get(c.source_url);
45
+ const held = m
46
+ ? ` (added by hand by ${m.by} on ${m.at.slice(0, 10)}${m.published ? `, published ${m.published}` : ""})`
47
+ : "";
48
+ out.push(`${c.n}. ${title ? `${title}, ` : ""}<${c.source_url}>${held}: "${c.quote}"`);
49
+ }
50
+ }
51
+ return `${out
52
+ .join("\n")
53
+ .replace(/\n{3,}/g, "\n\n")
54
+ .trim()}\n`;
55
+ }
56
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/study/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ /** HTML character references to text (the WHATWG table); a no-break space becomes a space. */
2
+ export declare const decodeHtml: (s: string) => string;
@@ -0,0 +1,4 @@
1
+ import { decodeHTML } from "entities";
2
+ /** HTML character references to text (the WHATWG table); a no-break space becomes a space. */
3
+ export const decodeHtml = (s) => s.includes("&") ? decodeHTML(s).replaceAll(" ", " ") : s;
4
+ //# sourceMappingURL=entities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entities.js","sourceRoot":"","sources":["../../src/text/entities.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,8FAA8F;AAC9F,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAS,EAAU,EAAE,CAC9C,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Tiny HTML reading: visible text, links, title, mailto/tel hrefs in one pass.
3
+ * Deliberately not a DOM: enrichment needs the words and the links, not the tree.
4
+ * htmlparser2's streaming parser feeds the Extractor; it is tolerant of the malformed
5
+ * markup small-business sites serve and closes what HTML implies closed. Never throws:
6
+ * a pathological page degrades to whatever was read so far, never sinks a crawl.
7
+ */
8
+ interface PageLink {
9
+ url: string;
10
+ anchor: string;
11
+ }
12
+ interface PageContent {
13
+ title: string;
14
+ text: string;
15
+ /** Absolute url + anchor text; mailto/tel/javascript/fragment hrefs excluded. */
16
+ links: PageLink[];
17
+ /** Addresses declared in mailto: hrefs, decoded, query-stripped, order kept, duplicates kept. */
18
+ mailtos: string[];
19
+ tels: string[];
20
+ }
21
+ /** HTML character references to text (the WHATWG table). */
22
+ export declare const decodeEntities: (s: string) => string;
23
+ export declare function readPage(html: string, baseUrl?: string): PageContent;
24
+ /**
25
+ * Is this page a JS mount point whose words live client-side? Thin text alone must
26
+ * NOT fire (a tiny server-rendered contact page is real content): a shell is thin
27
+ * text plus substantial, script-dominated markup or a framework mount marker.
28
+ */
29
+ export declare function looksLikeJsShell(html: string, text: string): boolean;
30
+ export {};
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Tiny HTML reading: visible text, links, title, mailto/tel hrefs in one pass.
3
+ * Deliberately not a DOM: enrichment needs the words and the links, not the tree.
4
+ * htmlparser2's streaming parser feeds the Extractor; it is tolerant of the malformed
5
+ * markup small-business sites serve and closes what HTML implies closed. Never throws:
6
+ * a pathological page degrades to whatever was read so far, never sinks a crawl.
7
+ */
8
+ import { Parser } from "htmlparser2";
9
+ import { decodeHtml } from "./entities.js";
10
+ const SKIP_CONTENT = new Set([
11
+ "script",
12
+ "style",
13
+ "noscript",
14
+ "template",
15
+ "svg",
16
+ ]);
17
+ /** Elements that end a run of inline text; a newline keeps extracted text readable. */
18
+ const BLOCK = new Set([
19
+ "p",
20
+ "div",
21
+ "br",
22
+ "li",
23
+ "tr",
24
+ "h1",
25
+ "h2",
26
+ "h3",
27
+ "h4",
28
+ "h5",
29
+ "h6",
30
+ "section",
31
+ "article",
32
+ "header",
33
+ "footer",
34
+ "nav",
35
+ "main",
36
+ "aside",
37
+ "table",
38
+ "td",
39
+ "th",
40
+ "ul",
41
+ "ol",
42
+ "dl",
43
+ "dt",
44
+ "dd",
45
+ "blockquote",
46
+ "pre",
47
+ "address",
48
+ "figure",
49
+ "figcaption",
50
+ "form",
51
+ "hr",
52
+ ]);
53
+ /** HTML character references to text (the WHATWG table). */
54
+ export const decodeEntities = decodeHtml;
55
+ /** Absolute URL for a link, or the raw href when neither it nor the base parses. */
56
+ function joinUrl(base, href) {
57
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(href))
58
+ return href;
59
+ if (!base)
60
+ return href;
61
+ try {
62
+ return new URL(href, base).href;
63
+ }
64
+ catch {
65
+ return href;
66
+ }
67
+ }
68
+ class Extractor {
69
+ baseUrl;
70
+ parts = [];
71
+ links = [];
72
+ mailtos = [];
73
+ tels = [];
74
+ title = "";
75
+ skipDepth = 0;
76
+ inTitle = false;
77
+ href = null;
78
+ anchorParts = [];
79
+ constructor(baseUrl) {
80
+ this.baseUrl = baseUrl;
81
+ }
82
+ startTag(tag, attrs) {
83
+ if (SKIP_CONTENT.has(tag))
84
+ this.skipDepth++;
85
+ else if (tag === "title")
86
+ this.inTitle = true;
87
+ else if (tag === "a") {
88
+ const href = attrs.href;
89
+ if (href?.startsWith("mailto:")) {
90
+ const list = safeDecode(href.slice(7).split("?", 1)[0] ?? "");
91
+ for (const addr of list.split(",")) {
92
+ const clean = addr.trim();
93
+ if (clean)
94
+ this.mailtos.push(clean);
95
+ }
96
+ }
97
+ else if (href?.startsWith("tel:")) {
98
+ const number = href.slice(4).trim();
99
+ if (number)
100
+ this.tels.push(safeDecode(number));
101
+ }
102
+ else if (href && !href.startsWith("javascript:") && !href.startsWith("#")) {
103
+ this.href = joinUrl(this.baseUrl, href);
104
+ this.anchorParts = [];
105
+ }
106
+ }
107
+ if (BLOCK.has(tag))
108
+ this.parts.push("\n");
109
+ }
110
+ endTag(tag) {
111
+ if (SKIP_CONTENT.has(tag) && this.skipDepth)
112
+ this.skipDepth--;
113
+ else if (tag === "title")
114
+ this.inTitle = false;
115
+ else if (tag === "a" && this.href) {
116
+ this.links.push({
117
+ url: this.href,
118
+ anchor: this.anchorParts.join("").split(/\s+/).filter(Boolean).join(" "),
119
+ });
120
+ this.href = null;
121
+ }
122
+ if (BLOCK.has(tag))
123
+ this.parts.push("\n");
124
+ }
125
+ /** Text as the parser hands it: entities already decoded once. */
126
+ data(decoded) {
127
+ const text = decoded.replaceAll("\u00a0", " ");
128
+ // The page's title, never words of the body.
129
+ if (this.inTitle) {
130
+ this.title += text;
131
+ return;
132
+ }
133
+ if (this.skipDepth)
134
+ return;
135
+ this.parts.push(text);
136
+ if (this.href !== null)
137
+ this.anchorParts.push(text);
138
+ }
139
+ }
140
+ function safeDecode(s) {
141
+ try {
142
+ return decodeURIComponent(s);
143
+ }
144
+ catch {
145
+ return s;
146
+ }
147
+ }
148
+ export function readPage(html, baseUrl = "") {
149
+ const out = new Extractor(baseUrl);
150
+ try {
151
+ const parser = new Parser({
152
+ onopentag: (name, attrs) => out.startTag(name, attrs),
153
+ onclosetag: (name) => out.endTag(name),
154
+ ontext: (text) => out.data(text),
155
+ });
156
+ parser.write(html);
157
+ parser.end();
158
+ }
159
+ catch {
160
+ // degrade to whatever was extracted so far
161
+ }
162
+ const text = out.parts
163
+ .join("")
164
+ .split(/\r?\n/)
165
+ .map((line) => line.trim())
166
+ .filter(Boolean)
167
+ .join("\n");
168
+ return {
169
+ title: out.title.split(/\s+/).filter(Boolean).join(" "),
170
+ text,
171
+ links: out.links,
172
+ mailtos: out.mailtos,
173
+ tels: out.tels,
174
+ };
175
+ }
176
+ /** SPA mount-point / framework markers, checked against lowercased markup. */
177
+ const SHELL_MARKERS = [
178
+ 'id="root"',
179
+ "id='root'",
180
+ 'id="app"',
181
+ "id='app'",
182
+ 'id="__next"',
183
+ "__next_data__",
184
+ "data-reactroot",
185
+ 'id="___gatsby"',
186
+ "ng-version=",
187
+ 'id="q-app"',
188
+ "enable javascript",
189
+ "requires javascript",
190
+ ];
191
+ const SHELL_MAX_TEXT = 200;
192
+ const SHELL_MIN_HTML = 2000;
193
+ /**
194
+ * Is this page a JS mount point whose words live client-side? Thin text alone must
195
+ * NOT fire (a tiny server-rendered contact page is real content): a shell is thin
196
+ * text plus substantial, script-dominated markup or a framework mount marker.
197
+ */
198
+ export function looksLikeJsShell(html, text) {
199
+ if (text.trim().length >= SHELL_MAX_TEXT)
200
+ return false;
201
+ if (html.length < SHELL_MIN_HTML)
202
+ return false;
203
+ const lowered = html.toLowerCase();
204
+ if (SHELL_MARKERS.some((marker) => lowered.includes(marker)))
205
+ return true;
206
+ return lowered.split("<script").length - 1 >= 3;
207
+ }
208
+ //# sourceMappingURL=htmltext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"htmltext.js","sourceRoot":"","sources":["../../src/text/htmltext.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Files and markup in, readable text out: HTML to text, or to title, text and links (`readPage`);
3
+ * PDF to text; character references. No database here.
4
+ */
5
+ export * from "./entities.js";
6
+ export * from "./htmltext.js";
7
+ export * from "./text.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Files and markup in, readable text out: HTML to text, or to title, text and links (`readPage`);
3
+ * PDF to text; character references. No database here.
4
+ */
5
+ export * from "./entities.js";
6
+ export * from "./htmltext.js";
7
+ export * from "./text.js";
8
+ //# sourceMappingURL=index.js.map