@lacspace/llms-txt 1.2.1 → 1.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.
package/README.md CHANGED
@@ -120,6 +120,48 @@ export function GET() {
120
120
  }
121
121
  ```
122
122
 
123
+ ## Advanced (new)
124
+
125
+ All additive and backward-compatible — `llmsTxt`, `llmsFullTxt`, `parseLlmsTxt`, `llmsTxtFromSitemap` and the `*Response` helpers keep their existing behavior.
126
+
127
+ ### Build from routes
128
+
129
+ `llmsTxtFromRoutes(routes, meta)` turns a flat list of `{ title, url, notes?, section? }` into an `llms.txt`, grouping by `section` (first-seen order preserved).
130
+
131
+ ```ts
132
+ import { llmsTxtFromRoutes } from "@lacspace/llms-txt";
133
+
134
+ llmsTxtFromRoutes(
135
+ [
136
+ { title: "Home", url: "https://acme.com/", section: "Start" },
137
+ { title: "API", url: "https://acme.com/api", notes: "reference", section: "Docs" },
138
+ { title: "CLI", url: "https://acme.com/cli", section: "Docs" },
139
+ ],
140
+ { title: "Acme", summary: "Acme docs", defaultSection: "Docs" },
141
+ );
142
+ ```
143
+
144
+ ### Sitemap: XML string, auto-sections, dedupe
145
+
146
+ `llmsTxtFromSitemap` now accepts a **raw sitemap XML string** as well as an array, de-duplicates repeated URLs, and can derive sections from the first path segment with `sectionFromPath: true`. The original array signature is unchanged.
147
+
148
+ ```ts
149
+ llmsTxtFromSitemap(sitemapXmlString, { title: "Acme", sectionFromPath: true });
150
+ // "/docs/intro" → "## Docs", "/blog/hello" → "## Blog"
151
+ ```
152
+
153
+ ### Sort links within sections
154
+
155
+ `llmsTxt(doc, { sort })` — and the `sort` option on `llmsTxtFromRoutes` / `llmsTxtFromSitemap` — orders the links inside each section. Sections keep their array order.
156
+
157
+ ```ts
158
+ llmsTxt(doc, { sort: "title" }); // ascending by title
159
+ llmsTxt(doc, { sort: "url-desc" }); // descending by url
160
+ llmsTxt(doc, { sort: (a, b) => /* custom */ 0 });
161
+ ```
162
+
163
+ `parseLlmsTxt` round-trips what `llmsTxt` produces (title, summary, details, sections and links).
164
+
123
165
  ## Licensing
124
166
 
125
167
  This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — MIT-equivalent freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
package/dist/index.cjs CHANGED
@@ -1,13 +1,21 @@
1
1
  'use strict';
2
2
 
3
3
  // src/index.ts
4
- function llmsTxt(doc) {
4
+ function sortLinks(links, sort) {
5
+ if (!sort) return links;
6
+ const copy = links.slice();
7
+ if (typeof sort === "function") return copy.sort(sort);
8
+ const [field, dir] = sort.split("-");
9
+ const factor = dir === "desc" ? -1 : 1;
10
+ return copy.sort((a, b) => factor * a[field].localeCompare(b[field]));
11
+ }
12
+ function llmsTxt(doc, opts = {}) {
5
13
  const out = [`# ${doc.title}`];
6
14
  if (doc.summary) out.push("", `> ${doc.summary}`);
7
15
  if (doc.details) out.push("", doc.details.trim());
8
16
  for (const section of doc.sections) {
9
17
  out.push("", `## ${section.title}`, "");
10
- for (const l of section.links) {
18
+ for (const l of sortLinks(section.links, opts.sort)) {
11
19
  out.push(`- [${l.title}](${l.url})${l.notes ? `: ${l.notes}` : ""}`);
12
20
  }
13
21
  }
@@ -58,24 +66,83 @@ function titleFromUrl(url) {
58
66
  }
59
67
  const seg = path.split("/").filter(Boolean).pop();
60
68
  if (!seg) return "Home";
61
- return decodeURIComponent(seg).replace(/\.[a-z]+$/i, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
69
+ let decoded;
70
+ try {
71
+ decoded = decodeURIComponent(seg);
72
+ } catch {
73
+ decoded = seg;
74
+ }
75
+ return decoded.replace(/\.[a-z]+$/i, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
76
+ }
77
+ function sectionFromUrl(url) {
78
+ let path;
79
+ try {
80
+ path = new URL(url).pathname;
81
+ } catch {
82
+ path = url;
83
+ }
84
+ const seg = path.split("/").filter(Boolean)[0];
85
+ if (!seg) return "Home";
86
+ let decoded;
87
+ try {
88
+ decoded = decodeURIComponent(seg);
89
+ } catch {
90
+ decoded = seg;
91
+ }
92
+ return decoded.replace(/\.[a-z]+$/i, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
93
+ }
94
+ function parseSitemapXml(xml) {
95
+ const out = [];
96
+ const re = /<loc>\s*([\s\S]*?)\s*<\/loc>/gi;
97
+ let m;
98
+ while (m = re.exec(xml)) {
99
+ const loc = m[1].trim().replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
100
+ if (loc) out.push({ loc });
101
+ }
102
+ return out;
62
103
  }
63
104
  function llmsTxtFromSitemap(entries, meta) {
105
+ const list = typeof entries === "string" ? parseSitemapXml(entries) : entries;
64
106
  const bySection = /* @__PURE__ */ new Map();
65
- for (const e of entries) {
107
+ const order = [];
108
+ const seen = /* @__PURE__ */ new Set();
109
+ for (const e of list) {
66
110
  const url = e.url ?? e.loc;
67
- if (!url) continue;
68
- const section = e.section ?? meta.defaultSection ?? "Pages";
69
- if (!bySection.has(section)) bySection.set(section, []);
111
+ if (!url || seen.has(url)) continue;
112
+ seen.add(url);
113
+ const section = e.section ?? (meta.sectionFromPath ? sectionFromUrl(url) : meta.defaultSection ?? "Pages");
114
+ if (!bySection.has(section)) {
115
+ bySection.set(section, []);
116
+ order.push(section);
117
+ }
70
118
  bySection.get(section).push({ title: e.title ?? titleFromUrl(url), url });
71
119
  }
72
120
  const doc = {
73
121
  title: meta.title,
74
122
  summary: meta.summary,
75
123
  details: meta.details,
76
- sections: [...bySection].map(([title, links]) => ({ title, links }))
124
+ sections: order.map((title) => ({ title, links: bySection.get(title) }))
125
+ };
126
+ return llmsTxt(doc, { sort: meta.sort });
127
+ }
128
+ function llmsTxtFromRoutes(routes, meta) {
129
+ const bySection = /* @__PURE__ */ new Map();
130
+ const order = [];
131
+ for (const r of routes) {
132
+ const section = r.section ?? meta.defaultSection ?? "Docs";
133
+ if (!bySection.has(section)) {
134
+ bySection.set(section, []);
135
+ order.push(section);
136
+ }
137
+ bySection.get(section).push({ title: r.title, url: r.url, notes: r.notes });
138
+ }
139
+ const doc = {
140
+ title: meta.title,
141
+ summary: meta.summary,
142
+ details: meta.details,
143
+ sections: order.map((title) => ({ title, links: bySection.get(title) }))
77
144
  };
78
- return llmsTxt(doc);
145
+ return llmsTxt(doc, { sort: meta.sort });
79
146
  }
80
147
  function llmsTxtResponse(doc, init = {}) {
81
148
  return new Response(llmsTxt(doc), {
@@ -93,6 +160,7 @@ function llmsFullTxtResponse(doc, init = {}) {
93
160
  exports.llmsFullTxt = llmsFullTxt;
94
161
  exports.llmsFullTxtResponse = llmsFullTxtResponse;
95
162
  exports.llmsTxt = llmsTxt;
163
+ exports.llmsTxtFromRoutes = llmsTxtFromRoutes;
96
164
  exports.llmsTxtFromSitemap = llmsTxtFromSitemap;
97
165
  exports.llmsTxtResponse = llmsTxtResponse;
98
166
  exports.parseLlmsTxt = parseLlmsTxt;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/llms-txt\n * Generate and parse llms.txt and llms-full.txt (the llmstxt.org standard).\n *\n * llms.txt is a Markdown file at your site root that gives LLMs a curated map of\n * your most useful content; llms-full.txt inlines the full text so a model can\n * read everything in one request.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nexport interface LlmsLink {\n title: string;\n url: string;\n /** Short note shown after the link. */\n notes?: string;\n}\n\nexport interface LlmsSection {\n title: string;\n links: LlmsLink[];\n}\n\nexport interface LlmsDoc {\n /** The site / project name (rendered as the H1). */\n title: string;\n /** One-line summary (rendered as a blockquote). */\n summary?: string;\n /** Free-form Markdown shown before the sections. */\n details?: string;\n sections: LlmsSection[];\n}\n\n/**\n * Render an `llms.txt` document.\n * @example\n * llmsTxt({\n * title: \"Lacspace\",\n * summary: \"Open-source TypeScript packages and products.\",\n * sections: [{ title: \"Docs\", links: [{ title: \"Packages\", url: \"https://lacspace.com/packages\" }] }],\n * });\n */\nexport function llmsTxt(doc: LlmsDoc): string {\n const out: string[] = [`# ${doc.title}`];\n if (doc.summary) out.push(\"\", `> ${doc.summary}`);\n if (doc.details) out.push(\"\", doc.details.trim());\n for (const section of doc.sections) {\n out.push(\"\", `## ${section.title}`, \"\");\n for (const l of section.links) {\n out.push(`- [${l.title}](${l.url})${l.notes ? `: ${l.notes}` : \"\"}`);\n }\n }\n return out.join(\"\\n\") + \"\\n\";\n}\n\nexport interface LlmsFullSection {\n title: string;\n /** Full Markdown content for this section. */\n content: string;\n /** Optional source URL, added as a heading link. */\n url?: string;\n}\n\nexport interface LlmsFullDoc {\n title: string;\n summary?: string;\n sections: LlmsFullSection[];\n}\n\n/** Render an `llms-full.txt` document with the full content inlined. */\nexport function llmsFullTxt(doc: LlmsFullDoc): string {\n const out: string[] = [`# ${doc.title}`];\n if (doc.summary) out.push(\"\", `> ${doc.summary}`);\n for (const section of doc.sections) {\n out.push(\"\", \"---\", \"\", `## ${section.title}`);\n if (section.url) out.push(\"\", `Source: ${section.url}`);\n out.push(\"\", section.content.trim());\n }\n return out.join(\"\\n\") + \"\\n\";\n}\n\n/** Parse an `llms.txt` string back into a structured document. */\nexport function parseLlmsTxt(txt: string): LlmsDoc {\n const lines = txt.split(/\\r?\\n/);\n const doc: LlmsDoc = { title: \"\", sections: [] };\n let current: LlmsSection | null = null;\n const detailBuf: string[] = [];\n const linkRe = /^-\\s*\\[([^\\]]+)\\]\\(([^)]+)\\)\\s*(?::\\s*(.*))?$/;\n\n for (const raw of lines) {\n const line = raw.trimEnd();\n if (line.startsWith(\"# \")) {\n doc.title = line.slice(2).trim();\n } else if (line.startsWith(\"> \")) {\n doc.summary = (doc.summary ? doc.summary + \" \" : \"\") + line.slice(2).trim();\n } else if (line.startsWith(\"## \")) {\n current = { title: line.slice(3).trim(), links: [] };\n doc.sections.push(current);\n } else if (current && linkRe.test(line)) {\n const m = line.match(linkRe)!;\n current.links.push({ title: m[1]!, url: m[2]!, notes: m[3]?.trim() || undefined });\n } else if (!current && line && !line.startsWith(\"#\")) {\n detailBuf.push(line);\n }\n }\n const details = detailBuf.join(\"\\n\").trim();\n if (details) doc.details = details;\n return doc;\n}\n\n/* ------------------------------ from sitemap ------------------------------ */\n\n/** A sitemap-ish entry — accepts `url` or `loc`, plus optional title/section. */\nexport interface SitemapEntryLike {\n url?: string;\n loc?: string;\n title?: string;\n section?: string;\n}\n\nfunction titleFromUrl(url: string): string {\n let path: string;\n try {\n path = new URL(url).pathname;\n } catch {\n path = url;\n }\n const seg = path.split(\"/\").filter(Boolean).pop();\n if (!seg) return \"Home\";\n return decodeURIComponent(seg).replace(/\\.[a-z]+$/i, \"\").replace(/[-_]+/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * Build an `llms.txt` from a list of sitemap entries — group by `section`,\n * derive titles from the URL when not given. Pairs with `@lacspace/sitemap`.\n */\nexport function llmsTxtFromSitemap(\n entries: SitemapEntryLike[],\n meta: { title: string; summary?: string; details?: string; defaultSection?: string },\n): string {\n const bySection = new Map<string, LlmsLink[]>();\n for (const e of entries) {\n const url = e.url ?? e.loc;\n if (!url) continue;\n const section = e.section ?? meta.defaultSection ?? \"Pages\";\n if (!bySection.has(section)) bySection.set(section, []);\n bySection.get(section)!.push({ title: e.title ?? titleFromUrl(url), url });\n }\n const doc: LlmsDoc = {\n title: meta.title,\n summary: meta.summary,\n details: meta.details,\n sections: [...bySection].map(([title, links]) => ({ title, links })),\n };\n return llmsTxt(doc);\n}\n\n/* ------------------------------ adapters ------------------------------ */\n\n/** `llms.txt` as a Fetch/edge `Response` (text/plain) for app/llms.txt/route.ts. */\nexport function llmsTxtResponse(doc: LlmsDoc, init: ResponseInit = {}): Response {\n return new Response(llmsTxt(doc), {\n ...init,\n headers: { \"content-type\": \"text/plain; charset=utf-8\", ...(init.headers ?? {}) },\n });\n}\n\n/** `llms-full.txt` as a Fetch/edge `Response` (text/plain). */\nexport function llmsFullTxtResponse(doc: LlmsFullDoc, init: ResponseInit = {}): Response {\n return new Response(llmsFullTxt(doc), {\n ...init,\n headers: { \"content-type\": \"text/plain; charset=utf-8\", ...(init.headers ?? {}) },\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/llms-txt\n * Generate and parse llms.txt and llms-full.txt (the llmstxt.org standard).\n *\n * llms.txt is a Markdown file at your site root that gives LLMs a curated map of\n * your most useful content; llms-full.txt inlines the full text so a model can\n * read everything in one request.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nexport interface LlmsLink {\n title: string;\n url: string;\n /** Short note shown after the link. */\n notes?: string;\n}\n\nexport interface LlmsSection {\n title: string;\n links: LlmsLink[];\n}\n\nexport interface LlmsDoc {\n /** The site / project name (rendered as the H1). */\n title: string;\n /** One-line summary (rendered as a blockquote). */\n summary?: string;\n /** Free-form Markdown shown before the sections. */\n details?: string;\n sections: LlmsSection[];\n}\n\n/**\n * How to order the links inside each section.\n * - `\"title\"` / `\"url\"` — ascending by that field\n * - `\"title-desc\"` / `\"url-desc\"` — descending\n * - a comparator — full control\n * Sections keep their array order; only the links within a section are sorted.\n */\nexport type LinkSort =\n | \"title\"\n | \"url\"\n | \"title-desc\"\n | \"url-desc\"\n | ((a: LlmsLink, b: LlmsLink) => number);\n\n/** Options accepted by the rendering helpers. */\nexport interface LlmsTxtOptions {\n /** Order links within each section. Omit to keep the given order. */\n sort?: LinkSort;\n}\n\nfunction sortLinks(links: LlmsLink[], sort?: LinkSort): LlmsLink[] {\n if (!sort) return links;\n const copy = links.slice();\n if (typeof sort === \"function\") return copy.sort(sort);\n const [field, dir] = sort.split(\"-\") as [\"title\" | \"url\", \"desc\" | undefined];\n const factor = dir === \"desc\" ? -1 : 1;\n return copy.sort((a, b) => factor * a[field].localeCompare(b[field]));\n}\n\n/**\n * Render an `llms.txt` document.\n * @param opts optional rendering options (e.g. `{ sort: \"title\" }`).\n * @example\n * llmsTxt({\n * title: \"Lacspace\",\n * summary: \"Open-source TypeScript packages and products.\",\n * sections: [{ title: \"Docs\", links: [{ title: \"Packages\", url: \"https://lacspace.com/packages\" }] }],\n * });\n */\nexport function llmsTxt(doc: LlmsDoc, opts: LlmsTxtOptions = {}): string {\n const out: string[] = [`# ${doc.title}`];\n if (doc.summary) out.push(\"\", `> ${doc.summary}`);\n if (doc.details) out.push(\"\", doc.details.trim());\n for (const section of doc.sections) {\n out.push(\"\", `## ${section.title}`, \"\");\n for (const l of sortLinks(section.links, opts.sort)) {\n out.push(`- [${l.title}](${l.url})${l.notes ? `: ${l.notes}` : \"\"}`);\n }\n }\n return out.join(\"\\n\") + \"\\n\";\n}\n\nexport interface LlmsFullSection {\n title: string;\n /** Full Markdown content for this section. */\n content: string;\n /** Optional source URL, added as a heading link. */\n url?: string;\n}\n\nexport interface LlmsFullDoc {\n title: string;\n summary?: string;\n sections: LlmsFullSection[];\n}\n\n/** Render an `llms-full.txt` document with the full content inlined. */\nexport function llmsFullTxt(doc: LlmsFullDoc): string {\n const out: string[] = [`# ${doc.title}`];\n if (doc.summary) out.push(\"\", `> ${doc.summary}`);\n for (const section of doc.sections) {\n out.push(\"\", \"---\", \"\", `## ${section.title}`);\n if (section.url) out.push(\"\", `Source: ${section.url}`);\n out.push(\"\", section.content.trim());\n }\n return out.join(\"\\n\") + \"\\n\";\n}\n\n/** Parse an `llms.txt` string back into a structured document. */\nexport function parseLlmsTxt(txt: string): LlmsDoc {\n const lines = txt.split(/\\r?\\n/);\n const doc: LlmsDoc = { title: \"\", sections: [] };\n let current: LlmsSection | null = null;\n const detailBuf: string[] = [];\n const linkRe = /^-\\s*\\[([^\\]]+)\\]\\(([^)]+)\\)\\s*(?::\\s*(.*))?$/;\n\n for (const raw of lines) {\n const line = raw.trimEnd();\n if (line.startsWith(\"# \")) {\n doc.title = line.slice(2).trim();\n } else if (line.startsWith(\"> \")) {\n doc.summary = (doc.summary ? doc.summary + \" \" : \"\") + line.slice(2).trim();\n } else if (line.startsWith(\"## \")) {\n current = { title: line.slice(3).trim(), links: [] };\n doc.sections.push(current);\n } else if (current && linkRe.test(line)) {\n const m = line.match(linkRe)!;\n current.links.push({ title: m[1]!, url: m[2]!, notes: m[3]?.trim() || undefined });\n } else if (!current && line && !line.startsWith(\"#\")) {\n detailBuf.push(line);\n }\n }\n const details = detailBuf.join(\"\\n\").trim();\n if (details) doc.details = details;\n return doc;\n}\n\n/* ------------------------------ from sitemap ------------------------------ */\n\n/** A sitemap-ish entry — accepts `url` or `loc`, plus optional title/section. */\nexport interface SitemapEntryLike {\n url?: string;\n loc?: string;\n title?: string;\n section?: string;\n}\n\nfunction titleFromUrl(url: string): string {\n let path: string;\n try {\n path = new URL(url).pathname;\n } catch {\n path = url;\n }\n const seg = path.split(\"/\").filter(Boolean).pop();\n if (!seg) return \"Home\";\n let decoded: string;\n try {\n decoded = decodeURIComponent(seg);\n } catch {\n // Malformed percent-encoding — fall back to the raw segment instead of throwing.\n decoded = seg;\n }\n return decoded.replace(/\\.[a-z]+$/i, \"\").replace(/[-_]+/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/** First path segment of a URL, title-cased, for auto-sectioning (\"/docs/x\" → \"Docs\"). */\nfunction sectionFromUrl(url: string): string {\n let path: string;\n try {\n path = new URL(url).pathname;\n } catch {\n path = url;\n }\n const seg = path.split(\"/\").filter(Boolean)[0];\n if (!seg) return \"Home\";\n let decoded: string;\n try {\n decoded = decodeURIComponent(seg);\n } catch {\n decoded = seg;\n }\n return decoded.replace(/\\.[a-z]+$/i, \"\").replace(/[-_]+/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/** Pull `<loc>` values out of a sitemap (or sitemap-index) XML string. */\nfunction parseSitemapXml(xml: string): SitemapEntryLike[] {\n const out: SitemapEntryLike[] = [];\n const re = /<loc>\\s*([\\s\\S]*?)\\s*<\\/loc>/gi;\n let m: RegExpExecArray | null;\n while ((m = re.exec(xml))) {\n const loc = m[1]!\n .trim()\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&apos;/g, \"'\");\n if (loc) out.push({ loc });\n }\n return out;\n}\n\n/** Meta / options for {@link llmsTxtFromSitemap}. */\nexport interface SitemapToLlmsMeta extends LlmsTxtOptions {\n title: string;\n summary?: string;\n details?: string;\n /** Fallback section name when an entry has none. Default `\"Pages\"`. */\n defaultSection?: string;\n /**\n * When an entry has no explicit `section`, derive one from the first path\n * segment (\"/docs/x\" → \"Docs\") instead of using {@link defaultSection}.\n */\n sectionFromPath?: boolean;\n}\n\n/**\n * Build an `llms.txt` from sitemap entries — accepts either an **array** of\n * `{ loc | url, title?, section? }` entries or a raw **sitemap XML string**.\n * Groups by `section` (or, with `sectionFromPath`, by the first path segment),\n * derives titles from the URL when not given, and de-duplicates repeated URLs.\n * Pairs with `@lacspace/sitemap`. The original array signature is unchanged.\n */\nexport function llmsTxtFromSitemap(\n entries: SitemapEntryLike[] | string,\n meta: SitemapToLlmsMeta,\n): string {\n const list = typeof entries === \"string\" ? parseSitemapXml(entries) : entries;\n const bySection = new Map<string, LlmsLink[]>();\n const order: string[] = [];\n const seen = new Set<string>();\n for (const e of list) {\n const url = e.url ?? e.loc;\n if (!url || seen.has(url)) continue;\n seen.add(url);\n const section =\n e.section ?? (meta.sectionFromPath ? sectionFromUrl(url) : meta.defaultSection ?? \"Pages\");\n if (!bySection.has(section)) {\n bySection.set(section, []);\n order.push(section);\n }\n bySection.get(section)!.push({ title: e.title ?? titleFromUrl(url), url });\n }\n const doc: LlmsDoc = {\n title: meta.title,\n summary: meta.summary,\n details: meta.details,\n sections: order.map((title) => ({ title, links: bySection.get(title)! })),\n };\n return llmsTxt(doc, { sort: meta.sort });\n}\n\n/* ------------------------------ from routes ------------------------------ */\n\n/** A single route/page entry for {@link llmsTxtFromRoutes}. */\nexport interface LlmsRoute {\n title: string;\n url: string;\n notes?: string;\n /** Section heading to group under. Defaults to {@link RoutesToLlmsMeta.defaultSection}. */\n section?: string;\n}\n\n/** Meta / options for {@link llmsTxtFromRoutes}. */\nexport interface RoutesToLlmsMeta extends LlmsTxtOptions {\n title: string;\n summary?: string;\n details?: string;\n /** Section name for routes with no `section`. Default `\"Docs\"`. */\n defaultSection?: string;\n}\n\n/**\n * Build an `llms.txt` from a flat list of route entries, grouping by `section`\n * (first-seen order preserved). Titles and notes are used verbatim.\n * @example\n * llmsTxtFromRoutes(\n * [\n * { title: \"Home\", url: \"https://acme.com/\", section: \"Start\" },\n * { title: \"API\", url: \"https://acme.com/api\", notes: \"reference\", section: \"Docs\" },\n * ],\n * { title: \"Acme\", summary: \"Acme docs\" },\n * );\n */\nexport function llmsTxtFromRoutes(routes: LlmsRoute[], meta: RoutesToLlmsMeta): string {\n const bySection = new Map<string, LlmsLink[]>();\n const order: string[] = [];\n for (const r of routes) {\n const section = r.section ?? meta.defaultSection ?? \"Docs\";\n if (!bySection.has(section)) {\n bySection.set(section, []);\n order.push(section);\n }\n bySection.get(section)!.push({ title: r.title, url: r.url, notes: r.notes });\n }\n const doc: LlmsDoc = {\n title: meta.title,\n summary: meta.summary,\n details: meta.details,\n sections: order.map((title) => ({ title, links: bySection.get(title)! })),\n };\n return llmsTxt(doc, { sort: meta.sort });\n}\n\n/* ------------------------------ adapters ------------------------------ */\n\n/** `llms.txt` as a Fetch/edge `Response` (text/plain) for app/llms.txt/route.ts. */\nexport function llmsTxtResponse(doc: LlmsDoc, init: ResponseInit = {}): Response {\n return new Response(llmsTxt(doc), {\n ...init,\n headers: { \"content-type\": \"text/plain; charset=utf-8\", ...(init.headers ?? {}) },\n });\n}\n\n/** `llms-full.txt` as a Fetch/edge `Response` (text/plain). */\nexport function llmsFullTxtResponse(doc: LlmsFullDoc, init: ResponseInit = {}): Response {\n return new Response(llmsFullTxt(doc), {\n ...init,\n headers: { \"content-type\": \"text/plain; charset=utf-8\", ...(init.headers ?? {}) },\n });\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -27,8 +27,22 @@ interface LlmsDoc {
27
27
  details?: string;
28
28
  sections: LlmsSection[];
29
29
  }
30
+ /**
31
+ * How to order the links inside each section.
32
+ * - `"title"` / `"url"` — ascending by that field
33
+ * - `"title-desc"` / `"url-desc"` — descending
34
+ * - a comparator — full control
35
+ * Sections keep their array order; only the links within a section are sorted.
36
+ */
37
+ type LinkSort = "title" | "url" | "title-desc" | "url-desc" | ((a: LlmsLink, b: LlmsLink) => number);
38
+ /** Options accepted by the rendering helpers. */
39
+ interface LlmsTxtOptions {
40
+ /** Order links within each section. Omit to keep the given order. */
41
+ sort?: LinkSort;
42
+ }
30
43
  /**
31
44
  * Render an `llms.txt` document.
45
+ * @param opts optional rendering options (e.g. `{ sort: "title" }`).
32
46
  * @example
33
47
  * llmsTxt({
34
48
  * title: "Lacspace",
@@ -36,7 +50,7 @@ interface LlmsDoc {
36
50
  * sections: [{ title: "Docs", links: [{ title: "Packages", url: "https://lacspace.com/packages" }] }],
37
51
  * });
38
52
  */
39
- declare function llmsTxt(doc: LlmsDoc): string;
53
+ declare function llmsTxt(doc: LlmsDoc, opts?: LlmsTxtOptions): string;
40
54
  interface LlmsFullSection {
41
55
  title: string;
42
56
  /** Full Markdown content for this section. */
@@ -60,19 +74,59 @@ interface SitemapEntryLike {
60
74
  title?: string;
61
75
  section?: string;
62
76
  }
77
+ /** Meta / options for {@link llmsTxtFromSitemap}. */
78
+ interface SitemapToLlmsMeta extends LlmsTxtOptions {
79
+ title: string;
80
+ summary?: string;
81
+ details?: string;
82
+ /** Fallback section name when an entry has none. Default `"Pages"`. */
83
+ defaultSection?: string;
84
+ /**
85
+ * When an entry has no explicit `section`, derive one from the first path
86
+ * segment ("/docs/x" → "Docs") instead of using {@link defaultSection}.
87
+ */
88
+ sectionFromPath?: boolean;
89
+ }
63
90
  /**
64
- * Build an `llms.txt` from a list of sitemap entries — group by `section`,
65
- * derive titles from the URL when not given. Pairs with `@lacspace/sitemap`.
91
+ * Build an `llms.txt` from sitemap entries — accepts either an **array** of
92
+ * `{ loc | url, title?, section? }` entries or a raw **sitemap XML string**.
93
+ * Groups by `section` (or, with `sectionFromPath`, by the first path segment),
94
+ * derives titles from the URL when not given, and de-duplicates repeated URLs.
95
+ * Pairs with `@lacspace/sitemap`. The original array signature is unchanged.
66
96
  */
67
- declare function llmsTxtFromSitemap(entries: SitemapEntryLike[], meta: {
97
+ declare function llmsTxtFromSitemap(entries: SitemapEntryLike[] | string, meta: SitemapToLlmsMeta): string;
98
+ /** A single route/page entry for {@link llmsTxtFromRoutes}. */
99
+ interface LlmsRoute {
100
+ title: string;
101
+ url: string;
102
+ notes?: string;
103
+ /** Section heading to group under. Defaults to {@link RoutesToLlmsMeta.defaultSection}. */
104
+ section?: string;
105
+ }
106
+ /** Meta / options for {@link llmsTxtFromRoutes}. */
107
+ interface RoutesToLlmsMeta extends LlmsTxtOptions {
68
108
  title: string;
69
109
  summary?: string;
70
110
  details?: string;
111
+ /** Section name for routes with no `section`. Default `"Docs"`. */
71
112
  defaultSection?: string;
72
- }): string;
113
+ }
114
+ /**
115
+ * Build an `llms.txt` from a flat list of route entries, grouping by `section`
116
+ * (first-seen order preserved). Titles and notes are used verbatim.
117
+ * @example
118
+ * llmsTxtFromRoutes(
119
+ * [
120
+ * { title: "Home", url: "https://acme.com/", section: "Start" },
121
+ * { title: "API", url: "https://acme.com/api", notes: "reference", section: "Docs" },
122
+ * ],
123
+ * { title: "Acme", summary: "Acme docs" },
124
+ * );
125
+ */
126
+ declare function llmsTxtFromRoutes(routes: LlmsRoute[], meta: RoutesToLlmsMeta): string;
73
127
  /** `llms.txt` as a Fetch/edge `Response` (text/plain) for app/llms.txt/route.ts. */
74
128
  declare function llmsTxtResponse(doc: LlmsDoc, init?: ResponseInit): Response;
75
129
  /** `llms-full.txt` as a Fetch/edge `Response` (text/plain). */
76
130
  declare function llmsFullTxtResponse(doc: LlmsFullDoc, init?: ResponseInit): Response;
77
131
 
78
- export { type LlmsDoc, type LlmsFullDoc, type LlmsFullSection, type LlmsLink, type LlmsSection, type SitemapEntryLike, llmsFullTxt, llmsFullTxtResponse, llmsTxt, llmsTxtFromSitemap, llmsTxtResponse, parseLlmsTxt };
132
+ export { type LinkSort, type LlmsDoc, type LlmsFullDoc, type LlmsFullSection, type LlmsLink, type LlmsRoute, type LlmsSection, type LlmsTxtOptions, type RoutesToLlmsMeta, type SitemapEntryLike, type SitemapToLlmsMeta, llmsFullTxt, llmsFullTxtResponse, llmsTxt, llmsTxtFromRoutes, llmsTxtFromSitemap, llmsTxtResponse, parseLlmsTxt };
package/dist/index.d.ts CHANGED
@@ -27,8 +27,22 @@ interface LlmsDoc {
27
27
  details?: string;
28
28
  sections: LlmsSection[];
29
29
  }
30
+ /**
31
+ * How to order the links inside each section.
32
+ * - `"title"` / `"url"` — ascending by that field
33
+ * - `"title-desc"` / `"url-desc"` — descending
34
+ * - a comparator — full control
35
+ * Sections keep their array order; only the links within a section are sorted.
36
+ */
37
+ type LinkSort = "title" | "url" | "title-desc" | "url-desc" | ((a: LlmsLink, b: LlmsLink) => number);
38
+ /** Options accepted by the rendering helpers. */
39
+ interface LlmsTxtOptions {
40
+ /** Order links within each section. Omit to keep the given order. */
41
+ sort?: LinkSort;
42
+ }
30
43
  /**
31
44
  * Render an `llms.txt` document.
45
+ * @param opts optional rendering options (e.g. `{ sort: "title" }`).
32
46
  * @example
33
47
  * llmsTxt({
34
48
  * title: "Lacspace",
@@ -36,7 +50,7 @@ interface LlmsDoc {
36
50
  * sections: [{ title: "Docs", links: [{ title: "Packages", url: "https://lacspace.com/packages" }] }],
37
51
  * });
38
52
  */
39
- declare function llmsTxt(doc: LlmsDoc): string;
53
+ declare function llmsTxt(doc: LlmsDoc, opts?: LlmsTxtOptions): string;
40
54
  interface LlmsFullSection {
41
55
  title: string;
42
56
  /** Full Markdown content for this section. */
@@ -60,19 +74,59 @@ interface SitemapEntryLike {
60
74
  title?: string;
61
75
  section?: string;
62
76
  }
77
+ /** Meta / options for {@link llmsTxtFromSitemap}. */
78
+ interface SitemapToLlmsMeta extends LlmsTxtOptions {
79
+ title: string;
80
+ summary?: string;
81
+ details?: string;
82
+ /** Fallback section name when an entry has none. Default `"Pages"`. */
83
+ defaultSection?: string;
84
+ /**
85
+ * When an entry has no explicit `section`, derive one from the first path
86
+ * segment ("/docs/x" → "Docs") instead of using {@link defaultSection}.
87
+ */
88
+ sectionFromPath?: boolean;
89
+ }
63
90
  /**
64
- * Build an `llms.txt` from a list of sitemap entries — group by `section`,
65
- * derive titles from the URL when not given. Pairs with `@lacspace/sitemap`.
91
+ * Build an `llms.txt` from sitemap entries — accepts either an **array** of
92
+ * `{ loc | url, title?, section? }` entries or a raw **sitemap XML string**.
93
+ * Groups by `section` (or, with `sectionFromPath`, by the first path segment),
94
+ * derives titles from the URL when not given, and de-duplicates repeated URLs.
95
+ * Pairs with `@lacspace/sitemap`. The original array signature is unchanged.
66
96
  */
67
- declare function llmsTxtFromSitemap(entries: SitemapEntryLike[], meta: {
97
+ declare function llmsTxtFromSitemap(entries: SitemapEntryLike[] | string, meta: SitemapToLlmsMeta): string;
98
+ /** A single route/page entry for {@link llmsTxtFromRoutes}. */
99
+ interface LlmsRoute {
100
+ title: string;
101
+ url: string;
102
+ notes?: string;
103
+ /** Section heading to group under. Defaults to {@link RoutesToLlmsMeta.defaultSection}. */
104
+ section?: string;
105
+ }
106
+ /** Meta / options for {@link llmsTxtFromRoutes}. */
107
+ interface RoutesToLlmsMeta extends LlmsTxtOptions {
68
108
  title: string;
69
109
  summary?: string;
70
110
  details?: string;
111
+ /** Section name for routes with no `section`. Default `"Docs"`. */
71
112
  defaultSection?: string;
72
- }): string;
113
+ }
114
+ /**
115
+ * Build an `llms.txt` from a flat list of route entries, grouping by `section`
116
+ * (first-seen order preserved). Titles and notes are used verbatim.
117
+ * @example
118
+ * llmsTxtFromRoutes(
119
+ * [
120
+ * { title: "Home", url: "https://acme.com/", section: "Start" },
121
+ * { title: "API", url: "https://acme.com/api", notes: "reference", section: "Docs" },
122
+ * ],
123
+ * { title: "Acme", summary: "Acme docs" },
124
+ * );
125
+ */
126
+ declare function llmsTxtFromRoutes(routes: LlmsRoute[], meta: RoutesToLlmsMeta): string;
73
127
  /** `llms.txt` as a Fetch/edge `Response` (text/plain) for app/llms.txt/route.ts. */
74
128
  declare function llmsTxtResponse(doc: LlmsDoc, init?: ResponseInit): Response;
75
129
  /** `llms-full.txt` as a Fetch/edge `Response` (text/plain). */
76
130
  declare function llmsFullTxtResponse(doc: LlmsFullDoc, init?: ResponseInit): Response;
77
131
 
78
- export { type LlmsDoc, type LlmsFullDoc, type LlmsFullSection, type LlmsLink, type LlmsSection, type SitemapEntryLike, llmsFullTxt, llmsFullTxtResponse, llmsTxt, llmsTxtFromSitemap, llmsTxtResponse, parseLlmsTxt };
132
+ export { type LinkSort, type LlmsDoc, type LlmsFullDoc, type LlmsFullSection, type LlmsLink, type LlmsRoute, type LlmsSection, type LlmsTxtOptions, type RoutesToLlmsMeta, type SitemapEntryLike, type SitemapToLlmsMeta, llmsFullTxt, llmsFullTxtResponse, llmsTxt, llmsTxtFromRoutes, llmsTxtFromSitemap, llmsTxtResponse, parseLlmsTxt };
package/dist/index.js CHANGED
@@ -1,11 +1,19 @@
1
1
  // src/index.ts
2
- function llmsTxt(doc) {
2
+ function sortLinks(links, sort) {
3
+ if (!sort) return links;
4
+ const copy = links.slice();
5
+ if (typeof sort === "function") return copy.sort(sort);
6
+ const [field, dir] = sort.split("-");
7
+ const factor = dir === "desc" ? -1 : 1;
8
+ return copy.sort((a, b) => factor * a[field].localeCompare(b[field]));
9
+ }
10
+ function llmsTxt(doc, opts = {}) {
3
11
  const out = [`# ${doc.title}`];
4
12
  if (doc.summary) out.push("", `> ${doc.summary}`);
5
13
  if (doc.details) out.push("", doc.details.trim());
6
14
  for (const section of doc.sections) {
7
15
  out.push("", `## ${section.title}`, "");
8
- for (const l of section.links) {
16
+ for (const l of sortLinks(section.links, opts.sort)) {
9
17
  out.push(`- [${l.title}](${l.url})${l.notes ? `: ${l.notes}` : ""}`);
10
18
  }
11
19
  }
@@ -56,24 +64,83 @@ function titleFromUrl(url) {
56
64
  }
57
65
  const seg = path.split("/").filter(Boolean).pop();
58
66
  if (!seg) return "Home";
59
- return decodeURIComponent(seg).replace(/\.[a-z]+$/i, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
67
+ let decoded;
68
+ try {
69
+ decoded = decodeURIComponent(seg);
70
+ } catch {
71
+ decoded = seg;
72
+ }
73
+ return decoded.replace(/\.[a-z]+$/i, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
74
+ }
75
+ function sectionFromUrl(url) {
76
+ let path;
77
+ try {
78
+ path = new URL(url).pathname;
79
+ } catch {
80
+ path = url;
81
+ }
82
+ const seg = path.split("/").filter(Boolean)[0];
83
+ if (!seg) return "Home";
84
+ let decoded;
85
+ try {
86
+ decoded = decodeURIComponent(seg);
87
+ } catch {
88
+ decoded = seg;
89
+ }
90
+ return decoded.replace(/\.[a-z]+$/i, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
91
+ }
92
+ function parseSitemapXml(xml) {
93
+ const out = [];
94
+ const re = /<loc>\s*([\s\S]*?)\s*<\/loc>/gi;
95
+ let m;
96
+ while (m = re.exec(xml)) {
97
+ const loc = m[1].trim().replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
98
+ if (loc) out.push({ loc });
99
+ }
100
+ return out;
60
101
  }
61
102
  function llmsTxtFromSitemap(entries, meta) {
103
+ const list = typeof entries === "string" ? parseSitemapXml(entries) : entries;
62
104
  const bySection = /* @__PURE__ */ new Map();
63
- for (const e of entries) {
105
+ const order = [];
106
+ const seen = /* @__PURE__ */ new Set();
107
+ for (const e of list) {
64
108
  const url = e.url ?? e.loc;
65
- if (!url) continue;
66
- const section = e.section ?? meta.defaultSection ?? "Pages";
67
- if (!bySection.has(section)) bySection.set(section, []);
109
+ if (!url || seen.has(url)) continue;
110
+ seen.add(url);
111
+ const section = e.section ?? (meta.sectionFromPath ? sectionFromUrl(url) : meta.defaultSection ?? "Pages");
112
+ if (!bySection.has(section)) {
113
+ bySection.set(section, []);
114
+ order.push(section);
115
+ }
68
116
  bySection.get(section).push({ title: e.title ?? titleFromUrl(url), url });
69
117
  }
70
118
  const doc = {
71
119
  title: meta.title,
72
120
  summary: meta.summary,
73
121
  details: meta.details,
74
- sections: [...bySection].map(([title, links]) => ({ title, links }))
122
+ sections: order.map((title) => ({ title, links: bySection.get(title) }))
123
+ };
124
+ return llmsTxt(doc, { sort: meta.sort });
125
+ }
126
+ function llmsTxtFromRoutes(routes, meta) {
127
+ const bySection = /* @__PURE__ */ new Map();
128
+ const order = [];
129
+ for (const r of routes) {
130
+ const section = r.section ?? meta.defaultSection ?? "Docs";
131
+ if (!bySection.has(section)) {
132
+ bySection.set(section, []);
133
+ order.push(section);
134
+ }
135
+ bySection.get(section).push({ title: r.title, url: r.url, notes: r.notes });
136
+ }
137
+ const doc = {
138
+ title: meta.title,
139
+ summary: meta.summary,
140
+ details: meta.details,
141
+ sections: order.map((title) => ({ title, links: bySection.get(title) }))
75
142
  };
76
- return llmsTxt(doc);
143
+ return llmsTxt(doc, { sort: meta.sort });
77
144
  }
78
145
  function llmsTxtResponse(doc, init = {}) {
79
146
  return new Response(llmsTxt(doc), {
@@ -88,6 +155,6 @@ function llmsFullTxtResponse(doc, init = {}) {
88
155
  });
89
156
  }
90
157
 
91
- export { llmsFullTxt, llmsFullTxtResponse, llmsTxt, llmsTxtFromSitemap, llmsTxtResponse, parseLlmsTxt };
158
+ export { llmsFullTxt, llmsFullTxtResponse, llmsTxt, llmsTxtFromRoutes, llmsTxtFromSitemap, llmsTxtResponse, parseLlmsTxt };
92
159
  //# sourceMappingURL=index.js.map
93
160
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/llms-txt\n * Generate and parse llms.txt and llms-full.txt (the llmstxt.org standard).\n *\n * llms.txt is a Markdown file at your site root that gives LLMs a curated map of\n * your most useful content; llms-full.txt inlines the full text so a model can\n * read everything in one request.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nexport interface LlmsLink {\n title: string;\n url: string;\n /** Short note shown after the link. */\n notes?: string;\n}\n\nexport interface LlmsSection {\n title: string;\n links: LlmsLink[];\n}\n\nexport interface LlmsDoc {\n /** The site / project name (rendered as the H1). */\n title: string;\n /** One-line summary (rendered as a blockquote). */\n summary?: string;\n /** Free-form Markdown shown before the sections. */\n details?: string;\n sections: LlmsSection[];\n}\n\n/**\n * Render an `llms.txt` document.\n * @example\n * llmsTxt({\n * title: \"Lacspace\",\n * summary: \"Open-source TypeScript packages and products.\",\n * sections: [{ title: \"Docs\", links: [{ title: \"Packages\", url: \"https://lacspace.com/packages\" }] }],\n * });\n */\nexport function llmsTxt(doc: LlmsDoc): string {\n const out: string[] = [`# ${doc.title}`];\n if (doc.summary) out.push(\"\", `> ${doc.summary}`);\n if (doc.details) out.push(\"\", doc.details.trim());\n for (const section of doc.sections) {\n out.push(\"\", `## ${section.title}`, \"\");\n for (const l of section.links) {\n out.push(`- [${l.title}](${l.url})${l.notes ? `: ${l.notes}` : \"\"}`);\n }\n }\n return out.join(\"\\n\") + \"\\n\";\n}\n\nexport interface LlmsFullSection {\n title: string;\n /** Full Markdown content for this section. */\n content: string;\n /** Optional source URL, added as a heading link. */\n url?: string;\n}\n\nexport interface LlmsFullDoc {\n title: string;\n summary?: string;\n sections: LlmsFullSection[];\n}\n\n/** Render an `llms-full.txt` document with the full content inlined. */\nexport function llmsFullTxt(doc: LlmsFullDoc): string {\n const out: string[] = [`# ${doc.title}`];\n if (doc.summary) out.push(\"\", `> ${doc.summary}`);\n for (const section of doc.sections) {\n out.push(\"\", \"---\", \"\", `## ${section.title}`);\n if (section.url) out.push(\"\", `Source: ${section.url}`);\n out.push(\"\", section.content.trim());\n }\n return out.join(\"\\n\") + \"\\n\";\n}\n\n/** Parse an `llms.txt` string back into a structured document. */\nexport function parseLlmsTxt(txt: string): LlmsDoc {\n const lines = txt.split(/\\r?\\n/);\n const doc: LlmsDoc = { title: \"\", sections: [] };\n let current: LlmsSection | null = null;\n const detailBuf: string[] = [];\n const linkRe = /^-\\s*\\[([^\\]]+)\\]\\(([^)]+)\\)\\s*(?::\\s*(.*))?$/;\n\n for (const raw of lines) {\n const line = raw.trimEnd();\n if (line.startsWith(\"# \")) {\n doc.title = line.slice(2).trim();\n } else if (line.startsWith(\"> \")) {\n doc.summary = (doc.summary ? doc.summary + \" \" : \"\") + line.slice(2).trim();\n } else if (line.startsWith(\"## \")) {\n current = { title: line.slice(3).trim(), links: [] };\n doc.sections.push(current);\n } else if (current && linkRe.test(line)) {\n const m = line.match(linkRe)!;\n current.links.push({ title: m[1]!, url: m[2]!, notes: m[3]?.trim() || undefined });\n } else if (!current && line && !line.startsWith(\"#\")) {\n detailBuf.push(line);\n }\n }\n const details = detailBuf.join(\"\\n\").trim();\n if (details) doc.details = details;\n return doc;\n}\n\n/* ------------------------------ from sitemap ------------------------------ */\n\n/** A sitemap-ish entry — accepts `url` or `loc`, plus optional title/section. */\nexport interface SitemapEntryLike {\n url?: string;\n loc?: string;\n title?: string;\n section?: string;\n}\n\nfunction titleFromUrl(url: string): string {\n let path: string;\n try {\n path = new URL(url).pathname;\n } catch {\n path = url;\n }\n const seg = path.split(\"/\").filter(Boolean).pop();\n if (!seg) return \"Home\";\n return decodeURIComponent(seg).replace(/\\.[a-z]+$/i, \"\").replace(/[-_]+/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * Build an `llms.txt` from a list of sitemap entries — group by `section`,\n * derive titles from the URL when not given. Pairs with `@lacspace/sitemap`.\n */\nexport function llmsTxtFromSitemap(\n entries: SitemapEntryLike[],\n meta: { title: string; summary?: string; details?: string; defaultSection?: string },\n): string {\n const bySection = new Map<string, LlmsLink[]>();\n for (const e of entries) {\n const url = e.url ?? e.loc;\n if (!url) continue;\n const section = e.section ?? meta.defaultSection ?? \"Pages\";\n if (!bySection.has(section)) bySection.set(section, []);\n bySection.get(section)!.push({ title: e.title ?? titleFromUrl(url), url });\n }\n const doc: LlmsDoc = {\n title: meta.title,\n summary: meta.summary,\n details: meta.details,\n sections: [...bySection].map(([title, links]) => ({ title, links })),\n };\n return llmsTxt(doc);\n}\n\n/* ------------------------------ adapters ------------------------------ */\n\n/** `llms.txt` as a Fetch/edge `Response` (text/plain) for app/llms.txt/route.ts. */\nexport function llmsTxtResponse(doc: LlmsDoc, init: ResponseInit = {}): Response {\n return new Response(llmsTxt(doc), {\n ...init,\n headers: { \"content-type\": \"text/plain; charset=utf-8\", ...(init.headers ?? {}) },\n });\n}\n\n/** `llms-full.txt` as a Fetch/edge `Response` (text/plain). */\nexport function llmsFullTxtResponse(doc: LlmsFullDoc, init: ResponseInit = {}): Response {\n return new Response(llmsFullTxt(doc), {\n ...init,\n headers: { \"content-type\": \"text/plain; charset=utf-8\", ...(init.headers ?? {}) },\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/llms-txt\n * Generate and parse llms.txt and llms-full.txt (the llmstxt.org standard).\n *\n * llms.txt is a Markdown file at your site root that gives LLMs a curated map of\n * your most useful content; llms-full.txt inlines the full text so a model can\n * read everything in one request.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nexport interface LlmsLink {\n title: string;\n url: string;\n /** Short note shown after the link. */\n notes?: string;\n}\n\nexport interface LlmsSection {\n title: string;\n links: LlmsLink[];\n}\n\nexport interface LlmsDoc {\n /** The site / project name (rendered as the H1). */\n title: string;\n /** One-line summary (rendered as a blockquote). */\n summary?: string;\n /** Free-form Markdown shown before the sections. */\n details?: string;\n sections: LlmsSection[];\n}\n\n/**\n * How to order the links inside each section.\n * - `\"title\"` / `\"url\"` — ascending by that field\n * - `\"title-desc\"` / `\"url-desc\"` — descending\n * - a comparator — full control\n * Sections keep their array order; only the links within a section are sorted.\n */\nexport type LinkSort =\n | \"title\"\n | \"url\"\n | \"title-desc\"\n | \"url-desc\"\n | ((a: LlmsLink, b: LlmsLink) => number);\n\n/** Options accepted by the rendering helpers. */\nexport interface LlmsTxtOptions {\n /** Order links within each section. Omit to keep the given order. */\n sort?: LinkSort;\n}\n\nfunction sortLinks(links: LlmsLink[], sort?: LinkSort): LlmsLink[] {\n if (!sort) return links;\n const copy = links.slice();\n if (typeof sort === \"function\") return copy.sort(sort);\n const [field, dir] = sort.split(\"-\") as [\"title\" | \"url\", \"desc\" | undefined];\n const factor = dir === \"desc\" ? -1 : 1;\n return copy.sort((a, b) => factor * a[field].localeCompare(b[field]));\n}\n\n/**\n * Render an `llms.txt` document.\n * @param opts optional rendering options (e.g. `{ sort: \"title\" }`).\n * @example\n * llmsTxt({\n * title: \"Lacspace\",\n * summary: \"Open-source TypeScript packages and products.\",\n * sections: [{ title: \"Docs\", links: [{ title: \"Packages\", url: \"https://lacspace.com/packages\" }] }],\n * });\n */\nexport function llmsTxt(doc: LlmsDoc, opts: LlmsTxtOptions = {}): string {\n const out: string[] = [`# ${doc.title}`];\n if (doc.summary) out.push(\"\", `> ${doc.summary}`);\n if (doc.details) out.push(\"\", doc.details.trim());\n for (const section of doc.sections) {\n out.push(\"\", `## ${section.title}`, \"\");\n for (const l of sortLinks(section.links, opts.sort)) {\n out.push(`- [${l.title}](${l.url})${l.notes ? `: ${l.notes}` : \"\"}`);\n }\n }\n return out.join(\"\\n\") + \"\\n\";\n}\n\nexport interface LlmsFullSection {\n title: string;\n /** Full Markdown content for this section. */\n content: string;\n /** Optional source URL, added as a heading link. */\n url?: string;\n}\n\nexport interface LlmsFullDoc {\n title: string;\n summary?: string;\n sections: LlmsFullSection[];\n}\n\n/** Render an `llms-full.txt` document with the full content inlined. */\nexport function llmsFullTxt(doc: LlmsFullDoc): string {\n const out: string[] = [`# ${doc.title}`];\n if (doc.summary) out.push(\"\", `> ${doc.summary}`);\n for (const section of doc.sections) {\n out.push(\"\", \"---\", \"\", `## ${section.title}`);\n if (section.url) out.push(\"\", `Source: ${section.url}`);\n out.push(\"\", section.content.trim());\n }\n return out.join(\"\\n\") + \"\\n\";\n}\n\n/** Parse an `llms.txt` string back into a structured document. */\nexport function parseLlmsTxt(txt: string): LlmsDoc {\n const lines = txt.split(/\\r?\\n/);\n const doc: LlmsDoc = { title: \"\", sections: [] };\n let current: LlmsSection | null = null;\n const detailBuf: string[] = [];\n const linkRe = /^-\\s*\\[([^\\]]+)\\]\\(([^)]+)\\)\\s*(?::\\s*(.*))?$/;\n\n for (const raw of lines) {\n const line = raw.trimEnd();\n if (line.startsWith(\"# \")) {\n doc.title = line.slice(2).trim();\n } else if (line.startsWith(\"> \")) {\n doc.summary = (doc.summary ? doc.summary + \" \" : \"\") + line.slice(2).trim();\n } else if (line.startsWith(\"## \")) {\n current = { title: line.slice(3).trim(), links: [] };\n doc.sections.push(current);\n } else if (current && linkRe.test(line)) {\n const m = line.match(linkRe)!;\n current.links.push({ title: m[1]!, url: m[2]!, notes: m[3]?.trim() || undefined });\n } else if (!current && line && !line.startsWith(\"#\")) {\n detailBuf.push(line);\n }\n }\n const details = detailBuf.join(\"\\n\").trim();\n if (details) doc.details = details;\n return doc;\n}\n\n/* ------------------------------ from sitemap ------------------------------ */\n\n/** A sitemap-ish entry — accepts `url` or `loc`, plus optional title/section. */\nexport interface SitemapEntryLike {\n url?: string;\n loc?: string;\n title?: string;\n section?: string;\n}\n\nfunction titleFromUrl(url: string): string {\n let path: string;\n try {\n path = new URL(url).pathname;\n } catch {\n path = url;\n }\n const seg = path.split(\"/\").filter(Boolean).pop();\n if (!seg) return \"Home\";\n let decoded: string;\n try {\n decoded = decodeURIComponent(seg);\n } catch {\n // Malformed percent-encoding — fall back to the raw segment instead of throwing.\n decoded = seg;\n }\n return decoded.replace(/\\.[a-z]+$/i, \"\").replace(/[-_]+/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/** First path segment of a URL, title-cased, for auto-sectioning (\"/docs/x\" → \"Docs\"). */\nfunction sectionFromUrl(url: string): string {\n let path: string;\n try {\n path = new URL(url).pathname;\n } catch {\n path = url;\n }\n const seg = path.split(\"/\").filter(Boolean)[0];\n if (!seg) return \"Home\";\n let decoded: string;\n try {\n decoded = decodeURIComponent(seg);\n } catch {\n decoded = seg;\n }\n return decoded.replace(/\\.[a-z]+$/i, \"\").replace(/[-_]+/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/** Pull `<loc>` values out of a sitemap (or sitemap-index) XML string. */\nfunction parseSitemapXml(xml: string): SitemapEntryLike[] {\n const out: SitemapEntryLike[] = [];\n const re = /<loc>\\s*([\\s\\S]*?)\\s*<\\/loc>/gi;\n let m: RegExpExecArray | null;\n while ((m = re.exec(xml))) {\n const loc = m[1]!\n .trim()\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&apos;/g, \"'\");\n if (loc) out.push({ loc });\n }\n return out;\n}\n\n/** Meta / options for {@link llmsTxtFromSitemap}. */\nexport interface SitemapToLlmsMeta extends LlmsTxtOptions {\n title: string;\n summary?: string;\n details?: string;\n /** Fallback section name when an entry has none. Default `\"Pages\"`. */\n defaultSection?: string;\n /**\n * When an entry has no explicit `section`, derive one from the first path\n * segment (\"/docs/x\" → \"Docs\") instead of using {@link defaultSection}.\n */\n sectionFromPath?: boolean;\n}\n\n/**\n * Build an `llms.txt` from sitemap entries — accepts either an **array** of\n * `{ loc | url, title?, section? }` entries or a raw **sitemap XML string**.\n * Groups by `section` (or, with `sectionFromPath`, by the first path segment),\n * derives titles from the URL when not given, and de-duplicates repeated URLs.\n * Pairs with `@lacspace/sitemap`. The original array signature is unchanged.\n */\nexport function llmsTxtFromSitemap(\n entries: SitemapEntryLike[] | string,\n meta: SitemapToLlmsMeta,\n): string {\n const list = typeof entries === \"string\" ? parseSitemapXml(entries) : entries;\n const bySection = new Map<string, LlmsLink[]>();\n const order: string[] = [];\n const seen = new Set<string>();\n for (const e of list) {\n const url = e.url ?? e.loc;\n if (!url || seen.has(url)) continue;\n seen.add(url);\n const section =\n e.section ?? (meta.sectionFromPath ? sectionFromUrl(url) : meta.defaultSection ?? \"Pages\");\n if (!bySection.has(section)) {\n bySection.set(section, []);\n order.push(section);\n }\n bySection.get(section)!.push({ title: e.title ?? titleFromUrl(url), url });\n }\n const doc: LlmsDoc = {\n title: meta.title,\n summary: meta.summary,\n details: meta.details,\n sections: order.map((title) => ({ title, links: bySection.get(title)! })),\n };\n return llmsTxt(doc, { sort: meta.sort });\n}\n\n/* ------------------------------ from routes ------------------------------ */\n\n/** A single route/page entry for {@link llmsTxtFromRoutes}. */\nexport interface LlmsRoute {\n title: string;\n url: string;\n notes?: string;\n /** Section heading to group under. Defaults to {@link RoutesToLlmsMeta.defaultSection}. */\n section?: string;\n}\n\n/** Meta / options for {@link llmsTxtFromRoutes}. */\nexport interface RoutesToLlmsMeta extends LlmsTxtOptions {\n title: string;\n summary?: string;\n details?: string;\n /** Section name for routes with no `section`. Default `\"Docs\"`. */\n defaultSection?: string;\n}\n\n/**\n * Build an `llms.txt` from a flat list of route entries, grouping by `section`\n * (first-seen order preserved). Titles and notes are used verbatim.\n * @example\n * llmsTxtFromRoutes(\n * [\n * { title: \"Home\", url: \"https://acme.com/\", section: \"Start\" },\n * { title: \"API\", url: \"https://acme.com/api\", notes: \"reference\", section: \"Docs\" },\n * ],\n * { title: \"Acme\", summary: \"Acme docs\" },\n * );\n */\nexport function llmsTxtFromRoutes(routes: LlmsRoute[], meta: RoutesToLlmsMeta): string {\n const bySection = new Map<string, LlmsLink[]>();\n const order: string[] = [];\n for (const r of routes) {\n const section = r.section ?? meta.defaultSection ?? \"Docs\";\n if (!bySection.has(section)) {\n bySection.set(section, []);\n order.push(section);\n }\n bySection.get(section)!.push({ title: r.title, url: r.url, notes: r.notes });\n }\n const doc: LlmsDoc = {\n title: meta.title,\n summary: meta.summary,\n details: meta.details,\n sections: order.map((title) => ({ title, links: bySection.get(title)! })),\n };\n return llmsTxt(doc, { sort: meta.sort });\n}\n\n/* ------------------------------ adapters ------------------------------ */\n\n/** `llms.txt` as a Fetch/edge `Response` (text/plain) for app/llms.txt/route.ts. */\nexport function llmsTxtResponse(doc: LlmsDoc, init: ResponseInit = {}): Response {\n return new Response(llmsTxt(doc), {\n ...init,\n headers: { \"content-type\": \"text/plain; charset=utf-8\", ...(init.headers ?? {}) },\n });\n}\n\n/** `llms-full.txt` as a Fetch/edge `Response` (text/plain). */\nexport function llmsFullTxtResponse(doc: LlmsFullDoc, init: ResponseInit = {}): Response {\n return new Response(llmsFullTxt(doc), {\n ...init,\n headers: { \"content-type\": \"text/plain; charset=utf-8\", ...(init.headers ?? {}) },\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lacspace/llms-txt",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Generate and parse llms.txt and llms-full.txt (the llmstxt.org standard) — a Markdown map of your site for LLMs. Zero-dependency, isomorphic.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",