katman-mcp 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.
Files changed (97) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +160 -0
  3. package/dist/cli.js +71 -0
  4. package/dist/context.js +43 -0
  5. package/dist/crawlers.js +86 -0
  6. package/dist/index.js +25 -0
  7. package/dist/license.js +448 -0
  8. package/dist/meta.js +28 -0
  9. package/dist/net.js +128 -0
  10. package/dist/server.js +17 -0
  11. package/dist/state.js +81 -0
  12. package/dist/tools/account/account.js +64 -0
  13. package/dist/tools/account/cli.js +21 -0
  14. package/dist/tools/account/index.js +48 -0
  15. package/dist/tools/account/sync.js +142 -0
  16. package/dist/tools/account/upsell.js +290 -0
  17. package/dist/tools/crawl/audit/analyze.js +719 -0
  18. package/dist/tools/crawl/audit/bots.js +114 -0
  19. package/dist/tools/crawl/audit/checks.js +126 -0
  20. package/dist/tools/crawl/audit/html.js +382 -0
  21. package/dist/tools/crawl/audit/index.js +364 -0
  22. package/dist/tools/crawl/audit/model.js +1 -0
  23. package/dist/tools/crawl/audit/report.js +199 -0
  24. package/dist/tools/crawl/audit/robots.js +199 -0
  25. package/dist/tools/crawl/audit/sitemap.js +147 -0
  26. package/dist/tools/crawl/cli.js +106 -0
  27. package/dist/tools/crawl/common/advice.js +61 -0
  28. package/dist/tools/crawl/common/findings.js +57 -0
  29. package/dist/tools/crawl/common/routes.js +80 -0
  30. package/dist/tools/crawl/common/text.js +89 -0
  31. package/dist/tools/crawl/common/urls.js +64 -0
  32. package/dist/tools/crawl/index.js +226 -0
  33. package/dist/tools/crawl/indexnow.js +271 -0
  34. package/dist/tools/crawl/keywords.js +178 -0
  35. package/dist/tools/crawl/scan/framework.js +645 -0
  36. package/dist/tools/crawl/scan/index.js +175 -0
  37. package/dist/tools/crawl/scan/project.js +180 -0
  38. package/dist/tools/crawl/scan/routes.js +354 -0
  39. package/dist/tools/crawl/scan/signals.js +414 -0
  40. package/dist/tools/method/cli.js +42 -0
  41. package/dist/tools/method/core/brief.js +302 -0
  42. package/dist/tools/method/core/extract.js +435 -0
  43. package/dist/tools/method/core/generate.js +504 -0
  44. package/dist/tools/method/core/lint.js +556 -0
  45. package/dist/tools/method/core/plan.js +405 -0
  46. package/dist/tools/method/core/prompt.js +150 -0
  47. package/dist/tools/method/core/questions.js +196 -0
  48. package/dist/tools/method/core/report.js +182 -0
  49. package/dist/tools/method/core/research-run.js +148 -0
  50. package/dist/tools/method/core/research.js +448 -0
  51. package/dist/tools/method/core/snippets.js +292 -0
  52. package/dist/tools/method/core/start.js +277 -0
  53. package/dist/tools/method/data/anatomy.js +141 -0
  54. package/dist/tools/method/data/appendices.js +202 -0
  55. package/dist/tools/method/data/cloudflare.js +192 -0
  56. package/dist/tools/method/data/evidence.js +137 -0
  57. package/dist/tools/method/data/geo-jobs.js +128 -0
  58. package/dist/tools/method/data/index.js +19 -0
  59. package/dist/tools/method/data/landing.js +67 -0
  60. package/dist/tools/method/data/lessons.js +127 -0
  61. package/dist/tools/method/data/measurement.js +142 -0
  62. package/dist/tools/method/data/method.js +141 -0
  63. package/dist/tools/method/data/offsite.js +148 -0
  64. package/dist/tools/method/data/pages.js +167 -0
  65. package/dist/tools/method/data/prompts.js +633 -0
  66. package/dist/tools/method/data/publish.js +113 -0
  67. package/dist/tools/method/data/render.js +117 -0
  68. package/dist/tools/method/data/research.js +131 -0
  69. package/dist/tools/method/data/roadmap.js +105 -0
  70. package/dist/tools/method/data/traps.js +74 -0
  71. package/dist/tools/method/data/verify.js +88 -0
  72. package/dist/tools/method/data/viral-list.js +39 -0
  73. package/dist/tools/method/index.js +209 -0
  74. package/dist/tools/method/lib/calls.js +53 -0
  75. package/dist/tools/method/lib/framework.js +525 -0
  76. package/dist/tools/method/lib/l10n.js +17 -0
  77. package/dist/tools/method/lib/routes.js +118 -0
  78. package/dist/tools/method/lib/state.js +454 -0
  79. package/dist/tools/method/lib/text.js +165 -0
  80. package/dist/tools/method/mcp/prompts.js +79 -0
  81. package/dist/tools/method/mcp/resources.js +50 -0
  82. package/dist/tools/visibility/analyze.js +463 -0
  83. package/dist/tools/visibility/cli.js +86 -0
  84. package/dist/tools/visibility/estimate.js +105 -0
  85. package/dist/tools/visibility/index.js +108 -0
  86. package/dist/tools/visibility/judge.js +156 -0
  87. package/dist/tools/visibility/manual.js +275 -0
  88. package/dist/tools/visibility/models.js +277 -0
  89. package/dist/tools/visibility/monitoring.js +165 -0
  90. package/dist/tools/visibility/openrouter.js +449 -0
  91. package/dist/tools/visibility/render.js +295 -0
  92. package/dist/tools/visibility/run.js +684 -0
  93. package/dist/tools/visibility/summary.js +164 -0
  94. package/dist/types.js +3 -0
  95. package/dist/util.js +92 -0
  96. package/package.json +54 -0
  97. package/server.json +34 -0
@@ -0,0 +1,435 @@
1
+ // Rough visible-text extraction for katman_lint_content: Markdown/MDX, HTML, Astro, Vue, Svelte, JSX/TSX and
2
+ // fetched pages. "Rough" on purpose: it finds headings, paragraphs, links, FAQ questions, title/description.
3
+ import { parse, HTMLElement as HTMLElementClass } from "node-html-parser";
4
+ import { countWords, splitSentences } from "../lib/text.js";
5
+ const DATE_WORDS_RE = /(last\s+updated|updated\s*(on|:)?|güncellen\w*|son\s+güncelleme|dateModified|lastmod|modified\s*:|<time\b[^>]*datetime=)/i;
6
+ const FAQ_HEAD_RE = /\b(faq|frequently asked|questions|sss|sık sorulan|sorular)\b/i;
7
+ function clean(s) {
8
+ return s.replace(/\s+/g, " ").trim();
9
+ }
10
+ function questionish(s) {
11
+ const x = s.trim();
12
+ return x.length > 8 && x.length < 200 && /\?$/.test(x);
13
+ }
14
+ function textOf(el) {
15
+ return el ? clean(el.text) : "";
16
+ }
17
+ // ---------------------------------------------------------------------------
18
+ // HTML
19
+ export function extractHtml(html, label, source = "file") {
20
+ const root = parse(html, { comment: false, blockTextElements: { script: true, style: true, noscript: true, pre: true } });
21
+ const metas = root.querySelectorAll("meta");
22
+ const metaVal = (name) => metas.find((m) => (m.getAttribute("name") ?? "").toLowerCase() === name)?.getAttribute("content") ?? undefined;
23
+ const jsonLd = [];
24
+ let jsonLdErrors = 0;
25
+ for (const s of root.querySelectorAll("script")) {
26
+ if (!/ld\+json/i.test(s.getAttribute("type") ?? ""))
27
+ continue;
28
+ try {
29
+ jsonLd.push(JSON.parse(s.rawText));
30
+ }
31
+ catch {
32
+ jsonLdErrors++;
33
+ }
34
+ }
35
+ const title = textOf(root.querySelector("title")) || undefined;
36
+ const lang = root.querySelector("html")?.getAttribute("lang") ?? undefined;
37
+ for (const n of root.querySelectorAll("script, style, noscript, template, svg"))
38
+ n.remove();
39
+ const body = root.querySelector("body") ?? root;
40
+ const allLinks = body.querySelectorAll("a").map((a) => a.getAttribute("href") ?? "").filter(Boolean);
41
+ let main = root.querySelector("main") ?? root.querySelector("article");
42
+ let contentLinks;
43
+ if (main)
44
+ contentLinks = main.querySelectorAll("a").map((a) => a.getAttribute("href") ?? "").filter(Boolean);
45
+ if (!main) {
46
+ for (const n of body.querySelectorAll("header, nav, footer"))
47
+ n.remove();
48
+ main = body;
49
+ contentLinks = main.querySelectorAll("a").map((a) => a.getAttribute("href") ?? "").filter(Boolean);
50
+ }
51
+ const headings = main.querySelectorAll("h1, h2, h3, h4, h5, h6").map((h) => ({ level: Number(h.tagName.slice(1)), text: textOf(h) }));
52
+ const h1 = headings.filter((h) => h.level === 1).map((h) => h.text).filter(Boolean);
53
+ if (!h1.length) {
54
+ const outerH1 = root.querySelectorAll("h1").map((h) => textOf(h)).filter(Boolean);
55
+ h1.push(...outerH1);
56
+ }
57
+ // blocks in document order, remembering where the first H1 is
58
+ const blocks = [];
59
+ let afterH1 = 0;
60
+ let seenH1 = false;
61
+ const walk = (el) => {
62
+ for (const child of el.childNodes) {
63
+ if (!(child instanceof HTMLElementClass))
64
+ continue;
65
+ const c = child;
66
+ const tag = (c.tagName ?? "").toLowerCase();
67
+ if (tag === "h1") {
68
+ if (!seenH1) {
69
+ seenH1 = true;
70
+ afterH1 = blocks.length;
71
+ }
72
+ continue;
73
+ }
74
+ if (["p", "li", "dd", "blockquote", "summary", "dt", "figcaption", "td"].includes(tag)) {
75
+ const t = textOf(c);
76
+ if (t)
77
+ blocks.push(t);
78
+ continue;
79
+ }
80
+ if (/^h[2-6]$/.test(tag))
81
+ continue;
82
+ walk(c);
83
+ }
84
+ };
85
+ walk(main);
86
+ const faqQuestions = new Set();
87
+ let inFaq = false;
88
+ for (const h of headings) {
89
+ if (h.level <= 2)
90
+ inFaq = FAQ_HEAD_RE.test(h.text) || (inFaq && questionish(h.text));
91
+ if (questionish(h.text) && (inFaq || h.level >= 2))
92
+ faqQuestions.add(h.text);
93
+ }
94
+ for (const n of main.querySelectorAll("summary, dt")) {
95
+ const t = textOf(n);
96
+ if (questionish(t))
97
+ faqQuestions.add(t);
98
+ }
99
+ for (const j of jsonLd) {
100
+ const items = Array.isArray(j) ? j : [j];
101
+ for (const it of items) {
102
+ const graph = Array.isArray(it?.["@graph"]) ? it["@graph"] : [it];
103
+ for (const g of graph) {
104
+ if (g && g["@type"] === "FAQPage" && Array.isArray(g.mainEntity)) {
105
+ for (const q of g.mainEntity)
106
+ if (typeof q?.name === "string")
107
+ faqQuestions.add(clean(q.name));
108
+ }
109
+ }
110
+ }
111
+ }
112
+ const text = textOf(main);
113
+ const fullText = textOf(body);
114
+ return {
115
+ source,
116
+ kind: "html",
117
+ label,
118
+ raw: html,
119
+ ...(title ? { title } : {}),
120
+ ...(metaVal("description") !== undefined ? { description: clean(metaVal("description")) } : {}),
121
+ ...(metaVal("robots") ? { robotsMeta: metaVal("robots") } : {}),
122
+ ...(lang ? { lang } : {}),
123
+ h1,
124
+ headings,
125
+ blocks,
126
+ afterH1,
127
+ text,
128
+ links: allLinks,
129
+ contentLinks,
130
+ faqQuestions: [...faqQuestions],
131
+ jsonLd,
132
+ jsonLdErrors,
133
+ hasDate: DATE_WORDS_RE.test(html) || jsonLd.some((j) => JSON.stringify(j).includes("dateModified")),
134
+ wordsJsOff: countWords(fullText),
135
+ notes: [],
136
+ };
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // Markdown / MDX / plain text
140
+ function frontmatter(src) {
141
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(src);
142
+ if (!m)
143
+ return { data: {}, body: src };
144
+ const data = {};
145
+ for (const line of m[1].split(/\r?\n/)) {
146
+ const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line);
147
+ if (kv)
148
+ data[kv[1].toLowerCase()] = kv[2].replace(/^["']|["']$/g, "").trim();
149
+ }
150
+ return { data, body: src.slice(m[0].length) };
151
+ }
152
+ function stripMd(s) {
153
+ return clean(s
154
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, "")
155
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
156
+ .replace(/<[^>]+>/g, " ")
157
+ .replace(/[*_`~]+/g, "")
158
+ .replace(/^>\s?/gm, ""));
159
+ }
160
+ export function extractMarkdown(src, label, source = "file") {
161
+ const { data, body: b0 } = frontmatter(src);
162
+ const body = b0
163
+ .replace(/^\s*(import|export)\s.*$/gm, "")
164
+ .replace(/\{\/\*[\s\S]*?\*\/\}/g, "")
165
+ .replace(/<!--[\s\S]*?-->/g, "")
166
+ .replace(/```[\s\S]*?```/g, "");
167
+ const headings = [];
168
+ const blocks = [];
169
+ let afterH1 = 0;
170
+ let seenH1 = false;
171
+ const links = [...body.matchAll(/\[[^\]]*\]\(([^)\s]+)[^)]*\)/g)].map((m) => m[1]).concat([...body.matchAll(/<a\s[^>]*href=["']([^"']+)["']/gi)].map((m) => m[1]));
172
+ const faqQuestions = new Set();
173
+ let para = [];
174
+ let inFaq = false;
175
+ const flush = () => {
176
+ if (para.length) {
177
+ const t = stripMd(para.join(" "));
178
+ if (t)
179
+ blocks.push(t);
180
+ para = [];
181
+ }
182
+ };
183
+ for (const line of body.split(/\r?\n/)) {
184
+ const h = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
185
+ if (h) {
186
+ flush();
187
+ const text = stripMd(h[2]);
188
+ const level = h[1].length;
189
+ headings.push({ level, text });
190
+ if (level === 1 && !seenH1) {
191
+ seenH1 = true;
192
+ afterH1 = blocks.length;
193
+ }
194
+ if (level <= 2)
195
+ inFaq = FAQ_HEAD_RE.test(text) || (inFaq && questionish(text));
196
+ if (questionish(text))
197
+ faqQuestions.add(text);
198
+ continue;
199
+ }
200
+ const li = /^\s*(?:[-*+]|\d+[.)])\s+(.*)$/.exec(line);
201
+ if (li) {
202
+ flush();
203
+ const t = stripMd(li[1]);
204
+ if (t)
205
+ blocks.push(t);
206
+ if (inFaq && questionish(t))
207
+ faqQuestions.add(t);
208
+ continue;
209
+ }
210
+ const bold = /^\s*\*\*(.+\?)\*\*\s*$/.exec(line);
211
+ if (bold) {
212
+ flush();
213
+ faqQuestions.add(stripMd(bold[1]));
214
+ continue;
215
+ }
216
+ if (!line.trim()) {
217
+ flush();
218
+ continue;
219
+ }
220
+ if (/^\s*\|/.test(line)) {
221
+ flush();
222
+ const cells = line.split("|").map((c) => stripMd(c)).filter((c) => c && !/^-+$/.test(c));
223
+ if (cells.length)
224
+ blocks.push(cells.join(" · "));
225
+ continue;
226
+ }
227
+ para.push(line);
228
+ }
229
+ flush();
230
+ let h1 = headings.filter((x) => x.level === 1).map((x) => x.text);
231
+ const notes = [];
232
+ if (!h1.length && data.title) {
233
+ h1 = [data.title];
234
+ notes.push("No # heading: the frontmatter title is treated as the H1.");
235
+ }
236
+ const text = blocks.join("\n");
237
+ return {
238
+ source,
239
+ kind: source === "text" ? "text" : "markdown",
240
+ label,
241
+ raw: src,
242
+ ...(data.title ? { title: data.title } : {}),
243
+ ...(data.description ? { description: data.description } : {}),
244
+ h1,
245
+ headings,
246
+ blocks,
247
+ afterH1,
248
+ text,
249
+ links,
250
+ faqQuestions: [...faqQuestions],
251
+ jsonLd: [],
252
+ jsonLdErrors: 0,
253
+ hasDate: DATE_WORDS_RE.test(src) || !!(data.updated || data.lastmod || data.datemodified || data.modified || data.updatedate),
254
+ notes,
255
+ };
256
+ }
257
+ // ---------------------------------------------------------------------------
258
+ // Astro / Vue / Svelte single-file components
259
+ function replaceExpressions(s) {
260
+ // {{ vue }} and { svelte / astro } expressions: keep plain string literals, drop the rest
261
+ return s
262
+ .replace(/\{\{[\s\S]*?\}\}/g, " ")
263
+ .replace(/\{\s*(["'`])((?:\\.|(?!\1)[^\\])*)\1\s*\}/g, (_m, _q, str) => (str.includes("${") ? " " : str))
264
+ .replace(/\{[^{}]*\}/g, " ");
265
+ }
266
+ export function extractSfc(src, label, kind) {
267
+ let html = src;
268
+ let title;
269
+ let description;
270
+ if (kind === "astro") {
271
+ const fm = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(html);
272
+ if (fm)
273
+ html = html.slice(fm[0].length);
274
+ }
275
+ else if (kind === "vue") {
276
+ const script = [...src.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)].map((m) => m[1]).join("\n");
277
+ title = /\btitle\s*:\s*(["'`])(.+?)\1/.exec(script)?.[2];
278
+ description = /\bdescription\s*:\s*(["'`])(.+?)\1/.exec(script)?.[2];
279
+ const start = html.indexOf("<template");
280
+ const end = html.lastIndexOf("</template>");
281
+ if (start >= 0 && end > start)
282
+ html = html.slice(html.indexOf(">", start) + 1, end);
283
+ }
284
+ else {
285
+ html = html.replace(/<script\b[\s\S]*?<\/script>/gi, "").replace(/<style\b[\s\S]*?<\/style>/gi, "");
286
+ }
287
+ // layout props: <Layout title="…" description="…">
288
+ title ??= /<[A-Z][\w.]*\b[^>]*\stitle=(["'])(.*?)\1/.exec(html)?.[2];
289
+ description ??= /<[A-Z][\w.]*\b[^>]*\sdescription=(["'])(.*?)\1/.exec(html)?.[2];
290
+ html = replaceExpressions(html.replace(/<svelte:head>/g, "<head>").replace(/<\/svelte:head>/g, "</head>"));
291
+ const x = extractHtml(`<html><body>${html}</body></html>`, label, "file");
292
+ x.raw = src;
293
+ x.kind = "sfc";
294
+ if (!x.title && title)
295
+ x.title = title;
296
+ if (x.description === undefined && description)
297
+ x.description = description;
298
+ const headTitle = /<title>([^<{]+)<\/title>/i.exec(src)?.[1];
299
+ if (!x.title && headTitle)
300
+ x.title = clean(headTitle);
301
+ x.hasDate = x.hasDate || DATE_WORDS_RE.test(src);
302
+ delete x.wordsJsOff;
303
+ return x;
304
+ }
305
+ // ---------------------------------------------------------------------------
306
+ // JSX / TSX
307
+ const CODEY = /(;|=>|===|!==|&&|\|\||\bconst\b|\breturn\b|\bfunction\b|\bimport\b|\bexport\b|\(\)|className|onClick)/;
308
+ function looksLikeCopy(s) {
309
+ const t = s.trim();
310
+ if (t.length < 12 || !/\p{L}/u.test(t))
311
+ return false;
312
+ if (/^(https?:|\/|\.\/|\.\.\/|@|#)/.test(t) || /\.(png|jpe?g|svg|webp|css|js|ts|tsx)$/i.test(t))
313
+ return false;
314
+ const words = t.split(/\s+/);
315
+ if (words.length < 3)
316
+ return false;
317
+ const classy = words.filter((w) => /[-:[\]]/.test(w) || /^\d/.test(w)).length;
318
+ if (classy / words.length > 0.4)
319
+ return false;
320
+ return /^[\p{Lu}"“]/u.test(t) || /[.?!:]$/.test(t);
321
+ }
322
+ function stripJsxInner(s) {
323
+ return clean(replaceExpressions(s).replace(/<[^>]+>/g, " "));
324
+ }
325
+ export function extractJsx(src, label) {
326
+ const code = src
327
+ .replace(/\{\/\*[\s\S]*?\*\/\}/g, " ")
328
+ .replace(/\/\*[\s\S]*?\*\//g, " ")
329
+ .replace(/^\s*\/\/.*$/gm, " ")
330
+ .replace(/^\s*import\s[\s\S]*?from\s+["'][^"']+["'];?\s*$/gm, " ")
331
+ .replace(/^\s*import\s+["'][^"']+["'];?\s*$/gm, " ");
332
+ const title = /\btitle\s*:\s*(["'`])((?:(?!\1).)+)\1/.exec(code)?.[2] ?? (/<title>([^<{]+)<\/title>/i.exec(code)?.[1]?.trim() || undefined);
333
+ const description = /\bdescription\s*:\s*(["'`])((?:(?!\1).)+)\1/.exec(code)?.[2] ??
334
+ /<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i.exec(code)?.[1];
335
+ const headings = [...code.matchAll(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi)].map((m) => ({ level: Number(m[1]), text: stripJsxInner(m[2]), index: m.index ?? 0 }));
336
+ const firstH1 = headings.find((h) => h.level === 1);
337
+ const pieces = [];
338
+ for (const m of code.matchAll(/<(p|li|dd|dt|summary|blockquote|figcaption)\b[^>]*>([\s\S]*?)<\/\1>/gi)) {
339
+ const t = stripJsxInner(m[2]);
340
+ if (t && !CODEY.test(t))
341
+ pieces.push({ index: m.index ?? 0, text: t });
342
+ }
343
+ // copy kept in data (arrays/objects) is common in vibe-coded pages
344
+ for (const m of code.matchAll(/(["'`])((?:\\.|(?!\1)[^\\\n])*)\1/g)) {
345
+ const t = m[2];
346
+ if (m[1] === "`" && t.includes("${"))
347
+ continue;
348
+ if (looksLikeCopy(t) && !pieces.some((p) => p.text.includes(t)))
349
+ pieces.push({ index: m.index ?? 0, text: clean(t) });
350
+ }
351
+ // stray JSX text nodes
352
+ for (const m of code.matchAll(/>([^<>{}]+)</g)) {
353
+ const t = clean(m[1]);
354
+ if (t.split(" ").length >= 4 && !CODEY.test(t) && !pieces.some((p) => p.text.includes(t)))
355
+ pieces.push({ index: m.index ?? 0, text: t });
356
+ }
357
+ pieces.sort((a, b) => a.index - b.index);
358
+ const blocks = pieces.map((p) => p.text);
359
+ const afterH1 = firstH1 ? pieces.filter((p) => p.index < firstH1.index).length : 0;
360
+ const links = [...code.matchAll(/<(?:a|Link|NavLink|NuxtLink)\b[^>]*?\s(?:href|to)\s*=\s*\{?\s*(["'`])([^"'`]+)\1/g)].map((m) => m[2]);
361
+ const faqQuestions = new Set();
362
+ for (const h of headings)
363
+ if (questionish(h.text))
364
+ faqQuestions.add(h.text);
365
+ for (const m of code.matchAll(/\b(?:q|question|name|title)\s*:\s*(["'`])((?:(?!\1).)+\?)\1/g))
366
+ faqQuestions.add(clean(m[2]));
367
+ for (const b of blocks)
368
+ if (questionish(b))
369
+ faqQuestions.add(b);
370
+ return {
371
+ source: "file",
372
+ kind: "jsx",
373
+ label,
374
+ raw: src,
375
+ ...(title ? { title } : {}),
376
+ ...(description ? { description } : {}),
377
+ h1: headings.filter((h) => h.level === 1).map((h) => h.text),
378
+ headings: headings.map(({ level, text }) => ({ level, text })),
379
+ blocks,
380
+ afterH1,
381
+ text: blocks.join("\n"),
382
+ links,
383
+ faqQuestions: [...faqQuestions],
384
+ jsonLd: [],
385
+ jsonLdErrors: 0,
386
+ hasDate: DATE_WORDS_RE.test(code),
387
+ notes: ["JSX/TSX text is extracted roughly (headings, paragraphs, copy in string literals). For an exact check, lint the built page with url."],
388
+ };
389
+ }
390
+ export function extractFile(src, label, ext) {
391
+ switch (ext) {
392
+ case ".md":
393
+ case ".mdx":
394
+ case ".markdown":
395
+ case ".txt":
396
+ return extractMarkdown(src, label);
397
+ case ".html":
398
+ case ".htm":
399
+ return extractHtml(src, label);
400
+ case ".astro":
401
+ return extractSfc(src, label, "astro");
402
+ case ".vue":
403
+ return extractSfc(src, label, "vue");
404
+ case ".svelte":
405
+ return extractSfc(src, label, "svelte");
406
+ case ".tsx":
407
+ case ".jsx":
408
+ case ".ts":
409
+ case ".js":
410
+ return extractJsx(src, label);
411
+ default:
412
+ return /<html|<body|<p[\s>]/i.test(src) ? extractHtml(src, label) : extractMarkdown(src, label);
413
+ }
414
+ }
415
+ /** The first 1–3 sentences after the H1. */
416
+ export function firstSentences(x, max = 3) {
417
+ const out = [];
418
+ const blocks = x.blocks.slice(x.afterH1).length ? x.blocks.slice(x.afterH1) : x.blocks;
419
+ for (const b of blocks) {
420
+ if (countWords(b) < 4 && !out.length)
421
+ continue; // skip tiny eyebrow/labels before the lead
422
+ for (const s of splitSentences(b)) {
423
+ out.push(s);
424
+ if (out.length >= max)
425
+ return out;
426
+ }
427
+ if (out.length)
428
+ break; // the first paragraph only
429
+ }
430
+ return out;
431
+ }
432
+ export function leadText(x, chars = 200) {
433
+ const blocks = x.blocks.slice(x.afterH1).length ? x.blocks.slice(x.afterH1) : x.blocks;
434
+ return blocks.join(" ").slice(0, chars);
435
+ }