@andesine/pages 0.0.0-stage → 0.4.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/CHANGELOG.md +99 -0
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/answers.d.ts +6 -0
- package/dist/astro/content.astro +118 -0
- package/dist/astro/middleware.js +39 -0
- package/dist/astro/middleware.js.map +1 -0
- package/dist/astro/routes/answers.js +129 -0
- package/dist/astro/routes/answers.js.map +1 -0
- package/dist/astro/routes/entry-pages.js +22 -0
- package/dist/astro/routes/entry-pages.js.map +1 -0
- package/dist/astro/routes/llms-full.js +12 -0
- package/dist/astro/routes/llms-full.js.map +1 -0
- package/dist/astro/routes/llms.js +12 -0
- package/dist/astro/routes/llms.js.map +1 -0
- package/dist/astro/routes/markdown-alias.js +14 -0
- package/dist/astro/routes/markdown-alias.js.map +1 -0
- package/dist/astro/routes/markdown.js +12 -0
- package/dist/astro/routes/markdown.js.map +1 -0
- package/dist/astro/routes/not-found.astro +12 -0
- package/dist/astro/routes/robots.js +9 -0
- package/dist/astro/routes/robots.js.map +1 -0
- package/dist/astro/routes/search.js +42 -0
- package/dist/astro/routes/search.js.map +1 -0
- package/dist/astro/routes/sitemap.js +12 -0
- package/dist/astro/routes/sitemap.js.map +1 -0
- package/dist/astro/routes/skill.js +13 -0
- package/dist/astro/routes/skill.js.map +1 -0
- package/dist/astro/routes/skills.js +24 -0
- package/dist/astro/routes/skills.js.map +1 -0
- package/dist/astro/routes/social.js +91 -0
- package/dist/astro/routes/social.js.map +1 -0
- package/dist/astro/routes/static.astro +53 -0
- package/dist/astro/routes/well-known-llms.js +2 -0
- package/dist/astro.d.ts +34 -0
- package/dist/astro.js +60 -0
- package/dist/astro.js.map +1 -0
- package/dist/chunks/assets-D6WJmZ05.js +21 -0
- package/dist/chunks/assets-D6WJmZ05.js.map +1 -0
- package/dist/chunks/catalog-DU4BUrpw.js +28 -0
- package/dist/chunks/catalog-DU4BUrpw.js.map +1 -0
- package/dist/chunks/config-CuH5JSI4.js +184 -0
- package/dist/chunks/config-CuH5JSI4.js.map +1 -0
- package/dist/chunks/files-BxqiQzFj.js +62 -0
- package/dist/chunks/files-BxqiQzFj.js.map +1 -0
- package/dist/chunks/hash-D4piIIQ6.js +11 -0
- package/dist/chunks/hash-D4piIIQ6.js.map +1 -0
- package/dist/chunks/index-D8wL2B5s.d.ts +571 -0
- package/dist/chunks/index-vsu3hISM.d.ts +62 -0
- package/dist/chunks/limits-Ppc7BQPG.js +45 -0
- package/dist/chunks/limits-Ppc7BQPG.js.map +1 -0
- package/dist/chunks/markdown-DKJSQxR2.js +42 -0
- package/dist/chunks/markdown-DKJSQxR2.js.map +1 -0
- package/dist/chunks/mdx-components-CUUDTapN.d.ts +9 -0
- package/dist/chunks/normalize-DG5nxOLm.js +361 -0
- package/dist/chunks/normalize-DG5nxOLm.js.map +1 -0
- package/dist/chunks/paths-75QgA3Qs.js +16 -0
- package/dist/chunks/paths-75QgA3Qs.js.map +1 -0
- package/dist/chunks/prepare-lnuw_ycM.js +219 -0
- package/dist/chunks/prepare-lnuw_ycM.js.map +1 -0
- package/dist/chunks/read-DCv1lmcM.js +57 -0
- package/dist/chunks/read-DCv1lmcM.js.map +1 -0
- package/dist/chunks/request-DOuBHGQs.js +92 -0
- package/dist/chunks/request-DOuBHGQs.js.map +1 -0
- package/dist/chunks/response-CtzaKMWH.js +15 -0
- package/dist/chunks/response-CtzaKMWH.js.map +1 -0
- package/dist/chunks/site-DY-uZ0PP.js +419 -0
- package/dist/chunks/site-DY-uZ0PP.js.map +1 -0
- package/dist/chunks/skill-CBTGUHQH.js +85 -0
- package/dist/chunks/skill-CBTGUHQH.js.map +1 -0
- package/dist/chunks/sources-Cm6NdIen.js +236 -0
- package/dist/chunks/sources-Cm6NdIen.js.map +1 -0
- package/dist/chunks/stored-CabomV-C.js +354 -0
- package/dist/chunks/stored-CabomV-C.js.map +1 -0
- package/dist/chunks/stored-CcEfvCQH.js +475 -0
- package/dist/chunks/stored-CcEfvCQH.js.map +1 -0
- package/dist/chunks/types-DBzuXfSe.d.ts +53 -0
- package/dist/chunks/view-C-9ZM1zK.js +745 -0
- package/dist/chunks/view-C-9ZM1zK.js.map +1 -0
- package/dist/chunks/view-Dlf_VRI7.d.ts +151 -0
- package/dist/client.d.ts +73 -0
- package/dist/client.js +295 -0
- package/dist/client.js.map +1 -0
- package/dist/collections.d.ts +7 -0
- package/dist/collections.js +215 -0
- package/dist/collections.js.map +1 -0
- package/dist/entry-pages.d.ts +10 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +16 -0
- package/dist/integration.js +435 -0
- package/dist/integration.js.map +1 -0
- package/dist/llms-full.d.ts +5 -0
- package/dist/llms.d.ts +5 -0
- package/dist/markdown-alias.d.ts +7 -0
- package/dist/markdown.d.ts +7 -0
- package/dist/middleware.d.ts +8 -0
- package/dist/robots.d.ts +5 -0
- package/dist/runtime.d.ts +81 -0
- package/dist/runtime.js +56 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scripts/restore-navigation.js +1 -0
- package/dist/search.d.ts +6 -0
- package/dist/sitemap.d.ts +5 -0
- package/dist/skill.d.ts +5 -0
- package/dist/skills.d.ts +10 -0
- package/dist/social.d.ts +7 -0
- package/dist/well-known-llms.d.ts +2 -0
- package/package.json +103 -3
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { toHTMLAST } from "@andesine/converters/html";
|
|
3
|
+
import { fromMarkdown } from "@andesine/converters/markdown";
|
|
4
|
+
import { bundledLanguages, codeToHast } from "shiki";
|
|
5
|
+
import { getIconCSS, getIconData } from "@iconify/utils";
|
|
6
|
+
//#region src/content/elements.ts
|
|
7
|
+
const noProps = z.object({}).strict();
|
|
8
|
+
const cardSchema = z.object({
|
|
9
|
+
title: z.string().min(1),
|
|
10
|
+
description: z.string().optional(),
|
|
11
|
+
icon: z.string().optional(),
|
|
12
|
+
href: z.string().optional()
|
|
13
|
+
}).strict();
|
|
14
|
+
/** Custom elements in Andesine content. Names and props match the MDX components. */
|
|
15
|
+
const elementSchemas = {
|
|
16
|
+
Callout: z.object({
|
|
17
|
+
type: z.enum([
|
|
18
|
+
"note",
|
|
19
|
+
"tip",
|
|
20
|
+
"warning",
|
|
21
|
+
"danger"
|
|
22
|
+
]).optional(),
|
|
23
|
+
title: z.string().optional()
|
|
24
|
+
}).strict(),
|
|
25
|
+
Card: cardSchema,
|
|
26
|
+
CardGrid: noProps,
|
|
27
|
+
CodeGroup: noProps,
|
|
28
|
+
Disclosure: z.object({
|
|
29
|
+
title: z.string().min(1),
|
|
30
|
+
open: z.boolean().optional()
|
|
31
|
+
}).strict(),
|
|
32
|
+
Figure: z.object({ caption: z.string().min(1) }).strict(),
|
|
33
|
+
FileTree: noProps,
|
|
34
|
+
Operation: z.object({
|
|
35
|
+
source: z.string().min(1),
|
|
36
|
+
id: z.string().min(1),
|
|
37
|
+
anchor: z.string().optional(),
|
|
38
|
+
description: z.boolean().optional(),
|
|
39
|
+
authentication: z.boolean().optional(),
|
|
40
|
+
parameters: z.boolean().optional(),
|
|
41
|
+
requestBody: z.boolean().optional(),
|
|
42
|
+
responses: z.boolean().optional(),
|
|
43
|
+
examples: z.boolean().optional()
|
|
44
|
+
}).strict(),
|
|
45
|
+
Steps: noProps,
|
|
46
|
+
Tab: z.object({ label: z.string().min(1) }).strict(),
|
|
47
|
+
Tabs: z.object({ syncKey: z.string().optional() }).strict(),
|
|
48
|
+
Visibility: z.object({ for: z.enum(["agents", "humans"]) }).strict()
|
|
49
|
+
};
|
|
50
|
+
/** Allowed child tags or elements, when an element restricts its children. */
|
|
51
|
+
const childRules = {
|
|
52
|
+
CodeGroup: "pre",
|
|
53
|
+
FileTree: "ul",
|
|
54
|
+
Steps: "ol",
|
|
55
|
+
Tabs: "Tab"
|
|
56
|
+
};
|
|
57
|
+
const getElementName = (node) => {
|
|
58
|
+
const name = node.properties.dataAndesineElement;
|
|
59
|
+
return typeof name === "string" ? name : void 0;
|
|
60
|
+
};
|
|
61
|
+
/** Reads and validates a custom element from the converters' HTML output. */
|
|
62
|
+
const readElement = (node) => {
|
|
63
|
+
const name = getElementName(node);
|
|
64
|
+
if (!name) return void 0;
|
|
65
|
+
if (!(name in elementSchemas)) throw new Error(`Unknown element <${name}>. Use ${Object.keys(elementSchemas).join(", ")}.`);
|
|
66
|
+
const result = elementSchemas[name].safeParse(JSON.parse(String(node.properties.dataAndesineProps ?? "{}")));
|
|
67
|
+
if (!result.success) throw new Error(`Invalid props on <${name}>:\n${z.prettifyError(result.error)}`);
|
|
68
|
+
const rule = childRules[name];
|
|
69
|
+
const invalidChild = node.children.filter((child) => child.type === "element").find((child) => (getElementName(child) ?? child.tagName) !== rule);
|
|
70
|
+
if (rule && invalidChild) throw new Error(`<${name}> can only contain ${rule} content.`);
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
props: result.data
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/content/hast.ts
|
|
78
|
+
const isElement = (node) => node.type === "element";
|
|
79
|
+
const getText = (node) => {
|
|
80
|
+
if (node.type === "text") return node.value;
|
|
81
|
+
return "children" in node ? node.children.map(getText).join("") : "";
|
|
82
|
+
};
|
|
83
|
+
/** Reads a `<ul>` of files and folders. Items that end with `/`, or have nested lists, are folders. */
|
|
84
|
+
const toFileTreeItems = (list) => {
|
|
85
|
+
return list.children.filter(isElement).map((item) => {
|
|
86
|
+
const nested = item.children.find((child) => isElement(child) && child.tagName === "ul");
|
|
87
|
+
const name = item.children.filter((child) => child !== nested).map(getText).join("").trim();
|
|
88
|
+
const folder = name.endsWith("/") || Boolean(nested);
|
|
89
|
+
return {
|
|
90
|
+
name: name.replace(/\/$/, ""),
|
|
91
|
+
type: folder ? "folder" : "file",
|
|
92
|
+
children: nested ? toFileTreeItems(nested) : []
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/content/icons.ts
|
|
98
|
+
/**
|
|
99
|
+
* Creates CSS for icon classes, such as `i-lucide:book`, that come from content data. UnoCSS
|
|
100
|
+
* cannot see these classes in the source. The selectors have no specificity, so size classes
|
|
101
|
+
* on the same element still apply.
|
|
102
|
+
*/
|
|
103
|
+
const createIconCSS = async (icons) => {
|
|
104
|
+
const { lookupCollection } = await import("@iconify/json");
|
|
105
|
+
return (await Promise.all([...new Set(icons)].map(async (icon) => {
|
|
106
|
+
const [, prefix, name] = /^i-([\w-]+):([\w-]+)$/.exec(icon) ?? [];
|
|
107
|
+
const collection = prefix ? await lookupCollection(prefix).catch(() => void 0) : void 0;
|
|
108
|
+
const data = collection && name ? getIconData(collection, name) : void 0;
|
|
109
|
+
if (!data) throw new Error(`Unknown icon ${icon}. Use an Iconify class, e.g. i-lucide:book.`);
|
|
110
|
+
return getIconCSS(data, {
|
|
111
|
+
iconSelector: `:where(.${icon.replace(/[^\w-]/g, (character) => `\\${character}`)})`,
|
|
112
|
+
mode: "mask",
|
|
113
|
+
format: "compressed"
|
|
114
|
+
});
|
|
115
|
+
}))).join("");
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/content/prepare.ts
|
|
119
|
+
/** Highlights code with the site's Shiki theme, as a `pre` element. */
|
|
120
|
+
const highlightCode = async (text, language, options) => {
|
|
121
|
+
const lang = language in bundledLanguages ? language : "text";
|
|
122
|
+
const hasThemes = Object.keys(options.themes ?? {}).length > 0;
|
|
123
|
+
const highlighted = (await codeToHast(text, {
|
|
124
|
+
lang,
|
|
125
|
+
...hasThemes ? {
|
|
126
|
+
themes: options.themes,
|
|
127
|
+
defaultColor: options.defaultColor
|
|
128
|
+
} : { theme: options.theme ?? "github-light" }
|
|
129
|
+
})).children.find((child) => child.type === "element");
|
|
130
|
+
highlighted.properties.dataLanguage = language;
|
|
131
|
+
return highlighted;
|
|
132
|
+
};
|
|
133
|
+
/** Highlights a converter code block. */
|
|
134
|
+
const highlight = (pre, options) => {
|
|
135
|
+
const code = pre.children.find((child) => child.type === "element");
|
|
136
|
+
return highlightCode(getText(code ?? pre), String(code?.properties.dataLanguage ?? "text"), options);
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Converts an Andesine document into a HTML syntax tree for rendering. Heading IDs follow the
|
|
140
|
+
* Andesine anchor rules, so links from search and answers match.
|
|
141
|
+
*/
|
|
142
|
+
const prepareContent = async (document, options) => {
|
|
143
|
+
const { tree } = await toHTMLAST(document, {
|
|
144
|
+
title: "omit",
|
|
145
|
+
properties: "omit",
|
|
146
|
+
imageURL: options.imageURL
|
|
147
|
+
});
|
|
148
|
+
const root = tree;
|
|
149
|
+
const headings = [];
|
|
150
|
+
const icons = [];
|
|
151
|
+
const visit = async (node) => {
|
|
152
|
+
const children = [];
|
|
153
|
+
for (const child of node.children) children.push(...await visitChild(child));
|
|
154
|
+
node.children = children;
|
|
155
|
+
};
|
|
156
|
+
const visitChild = async (child) => {
|
|
157
|
+
if (child.type !== "element") return [child];
|
|
158
|
+
const element = readElement(child);
|
|
159
|
+
if (element?.name === "Visibility") {
|
|
160
|
+
if (element.props.for === "agents") return [];
|
|
161
|
+
await visit(child);
|
|
162
|
+
return child.children;
|
|
163
|
+
}
|
|
164
|
+
const depth = /^h([1-6])$/.exec(child.tagName)?.[1];
|
|
165
|
+
const href = child.properties.href;
|
|
166
|
+
const isCard = element?.name === "Card";
|
|
167
|
+
if (child.tagName === "pre") return [await highlight(child, options.highlight)];
|
|
168
|
+
if (depth && typeof child.properties.id === "string") headings.push({
|
|
169
|
+
id: child.properties.id,
|
|
170
|
+
text: getText(child),
|
|
171
|
+
depth: Number(depth)
|
|
172
|
+
});
|
|
173
|
+
if (child.tagName === "a" && typeof href === "string") child.properties.href = options.linkURL(href);
|
|
174
|
+
if (typeof element?.props.icon === "string") icons.push(element.props.icon);
|
|
175
|
+
if (isCard && typeof element.props.href === "string") {
|
|
176
|
+
const props = {
|
|
177
|
+
...element.props,
|
|
178
|
+
href: options.linkURL(element.props.href)
|
|
179
|
+
};
|
|
180
|
+
child.properties.dataAndesineProps = JSON.stringify(props);
|
|
181
|
+
}
|
|
182
|
+
await visit(child);
|
|
183
|
+
return [child];
|
|
184
|
+
};
|
|
185
|
+
await visit(root);
|
|
186
|
+
return {
|
|
187
|
+
nodes: root,
|
|
188
|
+
headings,
|
|
189
|
+
iconCSS: await createIconCSS(icons)
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
/** Prepares Markdown, such as OpenAPI descriptions, like Andesine content. Images keep their URLs. */
|
|
193
|
+
const prepareMarkdown = async (markdown, highlight) => {
|
|
194
|
+
const document = await fromMarkdown(markdown, {
|
|
195
|
+
document: "fragment",
|
|
196
|
+
imageAssetID: (image) => image.url,
|
|
197
|
+
decode: { nodes: {
|
|
198
|
+
html: () => null,
|
|
199
|
+
footnoteReference: (node) => {
|
|
200
|
+
return {
|
|
201
|
+
type: "text",
|
|
202
|
+
text: `[${"label" in node ? node.label : ""}]`
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
footnoteDefinition: (node, context) => {
|
|
206
|
+
return "children" in node ? context.children(node.children) : null;
|
|
207
|
+
}
|
|
208
|
+
} }
|
|
209
|
+
});
|
|
210
|
+
return prepareContent(document, {
|
|
211
|
+
imageURL: (image) => String(image.attrs?.assetID),
|
|
212
|
+
linkURL: (href) => href,
|
|
213
|
+
highlight
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
//#endregion
|
|
217
|
+
export { toFileTreeItems as a, isElement as i, prepareContent as n, readElement as o, prepareMarkdown as r, highlightCode as t };
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=prepare-lnuw_ycM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prepare-lnuw_ycM.js","names":[],"sources":["../../src/content/elements.ts","../../src/content/hast.ts","../../src/content/icons.ts","../../src/content/prepare.ts"],"sourcesContent":["import type { Element } from \"hast\";\nimport { z } from \"zod\";\n\ninterface ContentElement {\n name: ElementName;\n props: Record<string, unknown>;\n}\n\ntype ElementName = keyof typeof elementSchemas;\n\nconst noProps = z.object({}).strict();\nconst cardSchema = z\n .object({\n title: z.string().min(1),\n description: z.string().optional(),\n icon: z.string().optional(),\n href: z.string().optional()\n })\n .strict();\n/** Custom elements in Andesine content. Names and props match the MDX components. */\nconst elementSchemas = {\n Callout: z\n .object({\n type: z.enum([\"note\", \"tip\", \"warning\", \"danger\"]).optional(),\n title: z.string().optional()\n })\n .strict(),\n Card: cardSchema,\n CardGrid: noProps,\n CodeGroup: noProps,\n Disclosure: z.object({ title: z.string().min(1), open: z.boolean().optional() }).strict(),\n Figure: z.object({ caption: z.string().min(1) }).strict(),\n FileTree: noProps,\n // An API operation from an OpenAPI source; `id` is its operation ID.\n Operation: z\n .object({\n source: z.string().min(1),\n id: z.string().min(1),\n anchor: z.string().optional(),\n description: z.boolean().optional(),\n authentication: z.boolean().optional(),\n parameters: z.boolean().optional(),\n requestBody: z.boolean().optional(),\n responses: z.boolean().optional(),\n examples: z.boolean().optional()\n })\n .strict(),\n Steps: noProps,\n Tab: z.object({ label: z.string().min(1) }).strict(),\n Tabs: z.object({ syncKey: z.string().optional() }).strict(),\n // Content for one audience: the web page, or the Markdown outputs for agents.\n Visibility: z.object({ for: z.enum([\"agents\", \"humans\"]) }).strict()\n};\n/** Allowed child tags or elements, when an element restricts its children. */\nconst childRules: Partial<Record<ElementName, string>> = {\n CodeGroup: \"pre\",\n FileTree: \"ul\",\n Steps: \"ol\",\n Tabs: \"Tab\"\n};\n\nconst getElementName = (node: Element): string | undefined => {\n const name = node.properties.dataAndesineElement;\n\n return typeof name === \"string\" ? name : undefined;\n};\n/** Reads and validates a custom element from the converters' HTML output. */\nconst readElement = (node: Element): ContentElement | undefined => {\n const name = getElementName(node);\n\n if (!name) return undefined;\n\n if (!(name in elementSchemas)) {\n throw new Error(`Unknown element <${name}>. Use ${Object.keys(elementSchemas).join(\", \")}.`);\n }\n\n const result = elementSchemas[name as ElementName].safeParse(\n JSON.parse(String(node.properties.dataAndesineProps ?? \"{}\"))\n );\n\n if (!result.success) {\n throw new Error(`Invalid props on <${name}>:\\n${z.prettifyError(result.error)}`);\n }\n\n const rule = childRules[name as ElementName];\n const children = node.children.filter((child) => child.type === \"element\");\n const invalidChild = children.find((child) => (getElementName(child) ?? child.tagName) !== rule);\n\n if (rule && invalidChild) throw new Error(`<${name}> can only contain ${rule} content.`);\n\n return { name: name as ElementName, props: result.data };\n};\n\nexport { readElement };\nexport type { ContentElement, ElementName };\n","import type { Element, ElementContent, Root, RootContent } from \"hast\";\nimport type { FileTreeItem } from \"./mdx-components\";\n\nconst isElement = (node: RootContent | ElementContent): node is Element => node.type === \"element\";\nconst getText = (node: Root | RootContent): string => {\n if (node.type === \"text\") return node.value;\n\n return \"children\" in node ? node.children.map(getText).join(\"\") : \"\";\n};\n/** Reads a `<ul>` of files and folders. Items that end with `/`, or have nested lists, are folders. */\nconst toFileTreeItems = (list: Element): FileTreeItem[] => {\n return list.children.filter(isElement).map((item) => {\n const nested = item.children.find(\n (child): child is Element => isElement(child) && child.tagName === \"ul\"\n );\n const name = item.children\n .filter((child) => child !== nested)\n .map(getText)\n .join(\"\")\n .trim();\n const folder = name.endsWith(\"/\") || Boolean(nested);\n\n return {\n name: name.replace(/\\/$/, \"\"),\n type: folder ? \"folder\" : \"file\",\n children: nested ? toFileTreeItems(nested) : []\n };\n });\n};\n\nexport { isElement, getText, toFileTreeItems };\n","import { getIconCSS, getIconData } from \"@iconify/utils\";\n\n/**\n * Creates CSS for icon classes, such as `i-lucide:book`, that come from content data. UnoCSS\n * cannot see these classes in the source. The selectors have no specificity, so size classes\n * on the same element still apply.\n */\nconst createIconCSS = async (icons: Iterable<string>): Promise<string> => {\n // Loaded on use: the package reads icon sets from disk, which Cloudflare Workers do not have.\n const { lookupCollection } = await import(\"@iconify/json\");\n const rules = await Promise.all(\n [...new Set(icons)].map(async (icon) => {\n const [, prefix, name] = /^i-([\\w-]+):([\\w-]+)$/.exec(icon) ?? [];\n const collection = prefix ? await lookupCollection(prefix).catch(() => undefined) : undefined;\n const data = collection && name ? getIconData(collection, name) : undefined;\n\n if (!data) throw new Error(`Unknown icon ${icon}. Use an Iconify class, e.g. i-lucide:book.`);\n\n return getIconCSS(data, {\n iconSelector: `:where(.${icon.replace(/[^\\w-]/g, (character) => `\\\\${character}`)})`,\n mode: \"mask\",\n format: \"compressed\"\n });\n })\n );\n\n return rules.join(\"\");\n};\n\nexport { createIconCSS };\n","import type { ContentNode } from \"@andesine/converters\";\nimport { toHTMLAST, type HTMLOptions } from \"@andesine/converters/html\";\nimport { fromMarkdown } from \"@andesine/converters/markdown\";\nimport type { Element, ElementContent, Root } from \"hast\";\nimport { bundledLanguages, codeToHast, type BundledLanguage } from \"shiki\";\nimport type { HeadingContext } from \"../context\";\nimport { readElement } from \"./elements\";\nimport { getText } from \"./hast\";\nimport { createIconCSS } from \"./icons\";\n\ninterface HighlightOptions {\n theme?: unknown;\n themes?: Record<string, unknown>;\n defaultColor?: string | false;\n}\n\ninterface PrepareOptions {\n imageURL: NonNullable<HTMLOptions[\"imageURL\"]>;\n /** Maps content links, such as `/Docs/Setup`, to site URLs. */\n linkURL(href: string): string;\n highlight: HighlightOptions;\n}\n\ninterface PreparedContent {\n nodes: Root;\n headings: HeadingContext[];\n /** CSS for icon classes in element props. */\n iconCSS: string;\n}\n\n/** Highlights code with the site's Shiki theme, as a `pre` element. */\nconst highlightCode = async (\n text: string,\n language: string,\n options: HighlightOptions\n): Promise<Element> => {\n const lang = language in bundledLanguages ? (language as BundledLanguage) : \"text\";\n // Astro's default config has an empty `themes` object.\n const hasThemes = Object.keys(options.themes ?? {}).length > 0;\n const root = await codeToHast(text, {\n lang,\n ...(hasThemes\n ? { themes: options.themes, defaultColor: options.defaultColor }\n : { theme: options.theme ?? \"github-light\" })\n } as Parameters<typeof codeToHast>[1]);\n const highlighted = root.children.find((child): child is Element => child.type === \"element\")!;\n\n highlighted.properties.dataLanguage = language;\n\n return highlighted;\n};\n/** Highlights a converter code block. */\nconst highlight = (pre: Element, options: HighlightOptions): Promise<Element> => {\n const code = pre.children.find((child): child is Element => child.type === \"element\");\n\n return highlightCode(\n getText(code ?? pre),\n String(code?.properties.dataLanguage ?? \"text\"),\n options\n );\n};\n\n/**\n * Converts an Andesine document into a HTML syntax tree for rendering. Heading IDs follow the\n * Andesine anchor rules, so links from search and answers match.\n */\nconst prepareContent = async (\n document: ContentNode,\n options: PrepareOptions\n): Promise<PreparedContent> => {\n const { tree } = await toHTMLAST(document, {\n title: \"omit\",\n properties: \"omit\",\n imageURL: options.imageURL\n });\n const root = tree as Root;\n const headings: HeadingContext[] = [];\n const icons: string[] = [];\n // Children are visited in order, so headings keep their document order.\n const visit = async (node: Root | Element): Promise<void> => {\n const children: ElementContent[] = [];\n\n for (const child of node.children) children.push(...(await visitChild(child)));\n\n node.children = children;\n };\n const visitChild = async (child: Root[\"children\"][number]): Promise<ElementContent[]> => {\n if (child.type !== \"element\") return [child as ElementContent];\n\n const element = readElement(child);\n\n // The web page shows only human content; agent content is in the Markdown outputs.\n if (element?.name === \"Visibility\") {\n if (element.props.for === \"agents\") return [];\n\n await visit(child);\n\n return child.children;\n }\n\n const depth = /^h([1-6])$/.exec(child.tagName)?.[1];\n const href = child.properties.href;\n const isCard = element?.name === \"Card\";\n\n if (child.tagName === \"pre\") return [await highlight(child, options.highlight)];\n\n if (depth && typeof child.properties.id === \"string\") {\n headings.push({ id: child.properties.id, text: getText(child), depth: Number(depth) });\n }\n\n if (child.tagName === \"a\" && typeof href === \"string\") {\n child.properties.href = options.linkURL(href);\n }\n\n if (typeof element?.props.icon === \"string\") icons.push(element.props.icon);\n\n if (isCard && typeof element.props.href === \"string\") {\n const props = { ...element.props, href: options.linkURL(element.props.href) };\n\n child.properties.dataAndesineProps = JSON.stringify(props);\n }\n\n await visit(child);\n\n return [child];\n };\n\n await visit(root);\n\n return { nodes: root, headings, iconCSS: await createIconCSS(icons) };\n};\n\n/** Prepares Markdown, such as OpenAPI descriptions, like Andesine content. Images keep their URLs. */\nconst prepareMarkdown = async (\n markdown: string,\n highlight: HighlightOptions\n): Promise<PreparedContent> => {\n const document = await fromMarkdown(markdown, {\n document: \"fragment\",\n imageAssetID: (image) => image.url,\n // Content has no raw HTML or footnotes: HTML is left out, and footnotes become text.\n decode: {\n nodes: {\n html: () => null,\n footnoteReference: (node) => {\n return { type: \"text\", text: `[${\"label\" in node ? node.label : \"\"}]` };\n },\n footnoteDefinition: (node, context) => {\n return \"children\" in node ? context.children(node.children) : null;\n }\n }\n }\n });\n\n return prepareContent(document, {\n imageURL: (image) => String(image.attrs?.assetID),\n linkURL: (href) => href,\n highlight\n });\n};\n\nexport { highlightCode, prepareContent, prepareMarkdown };\nexport type { HighlightOptions, PrepareOptions, PreparedContent };\n"],"mappings":";;;;;;AAUA,MAAM,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;AACpC,MAAM,aAAa,EAChB,OAAO;CACN,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;CAC1B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;AAC5B,CAAC,CAAC,CACD,OAAO;;AAEV,MAAM,iBAAiB;CACrB,SAAS,EACN,OAAO;EACN,MAAM,EAAE,KAAK;GAAC;GAAQ;GAAO;GAAW;EAAQ,CAAC,CAAC,CAAC,SAAS;EAC5D,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,CAAC,CAAC,CACD,OAAO;CACV,MAAM;CACN,UAAU;CACV,WAAW;CACX,YAAY,EAAE,OAAO;EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EAAG,MAAM,EAAE,QAAQ,CAAC,CAAC,SAAS;CAAE,CAAC,CAAC,CAAC,OAAO;CACxF,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO;CACxD,UAAU;CAEV,WAAW,EACR,OAAO;EACN,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACxB,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACpB,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;EAC5B,aAAa,EAAE,QAAQ,CAAC,CAAC,SAAS;EAClC,gBAAgB,EAAE,QAAQ,CAAC,CAAC,SAAS;EACrC,YAAY,EAAE,QAAQ,CAAC,CAAC,SAAS;EACjC,aAAa,EAAE,QAAQ,CAAC,CAAC,SAAS;EAClC,WAAW,EAAE,QAAQ,CAAC,CAAC,SAAS;EAChC,UAAU,EAAE,QAAQ,CAAC,CAAC,SAAS;CACjC,CAAC,CAAC,CACD,OAAO;CACV,OAAO;CACP,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO;CACnD,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,OAAO;CAE1D,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO;AACrE;;AAEA,MAAM,aAAmD;CACvD,WAAW;CACX,UAAU;CACV,OAAO;CACP,MAAM;AACR;AAEA,MAAM,kBAAkB,SAAsC;CAC5D,MAAM,OAAO,KAAK,WAAW;CAE7B,OAAO,OAAO,SAAS,WAAW,OAAO,KAAA;AAC3C;;AAEA,MAAM,eAAe,SAA8C;CACjE,MAAM,OAAO,eAAe,IAAI;CAEhC,IAAI,CAAC,MAAM,OAAO,KAAA;CAElB,IAAI,EAAE,QAAQ,iBACZ,MAAM,IAAI,MAAM,oBAAoB,KAAK,SAAS,OAAO,KAAK,cAAc,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;CAG7F,MAAM,SAAS,eAAe,KAAoB,CAAC,UACjD,KAAK,MAAM,OAAO,KAAK,WAAW,qBAAqB,IAAI,CAAC,CAC9D;CAEA,IAAI,CAAC,OAAO,SACV,MAAM,IAAI,MAAM,qBAAqB,KAAK,MAAM,EAAE,cAAc,OAAO,KAAK,GAAG;CAGjF,MAAM,OAAO,WAAW;CAExB,MAAM,eADW,KAAK,SAAS,QAAQ,UAAU,MAAM,SAAS,SACpC,CAAC,CAAC,MAAM,WAAW,eAAe,KAAK,KAAK,MAAM,aAAa,IAAI;CAE/F,IAAI,QAAQ,cAAc,MAAM,IAAI,MAAM,IAAI,KAAK,qBAAqB,KAAK,UAAU;CAEvF,OAAO;EAAQ;EAAqB,OAAO,OAAO;CAAK;AACzD;;;ACxFA,MAAM,aAAa,SAAwD,KAAK,SAAS;AACzF,MAAM,WAAW,SAAqC;CACpD,IAAI,KAAK,SAAS,QAAQ,OAAO,KAAK;CAEtC,OAAO,cAAc,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,KAAK,EAAE,IAAI;AACpE;;AAEA,MAAM,mBAAmB,SAAkC;CACzD,OAAO,KAAK,SAAS,OAAO,SAAS,CAAC,CAAC,KAAK,SAAS;EACnD,MAAM,SAAS,KAAK,SAAS,MAC1B,UAA4B,UAAU,KAAK,KAAK,MAAM,YAAY,IACrE;EACA,MAAM,OAAO,KAAK,SACf,QAAQ,UAAU,UAAU,MAAM,CAAC,CACnC,IAAI,OAAO,CAAC,CACZ,KAAK,EAAE,CAAC,CACR,KAAK;EACR,MAAM,SAAS,KAAK,SAAS,GAAG,KAAK,QAAQ,MAAM;EAEnD,OAAO;GACL,MAAM,KAAK,QAAQ,OAAO,EAAE;GAC5B,MAAM,SAAS,WAAW;GAC1B,UAAU,SAAS,gBAAgB,MAAM,IAAI,CAAC;EAChD;CACF,CAAC;AACH;;;;;;;;ACrBA,MAAM,gBAAgB,OAAO,UAA6C;CAExE,MAAM,EAAE,qBAAqB,MAAM,OAAO;CAiB1C,QAAO,MAhBa,QAAQ,IAC1B,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,OAAO,SAAS;EACtC,MAAM,GAAG,QAAQ,QAAQ,wBAAwB,KAAK,IAAI,KAAK,CAAC;EAChE,MAAM,aAAa,SAAS,MAAM,iBAAiB,MAAM,CAAC,CAAC,YAAY,KAAA,CAAS,IAAI,KAAA;EACpF,MAAM,OAAO,cAAc,OAAO,YAAY,YAAY,IAAI,IAAI,KAAA;EAElE,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,gBAAgB,KAAK,4CAA4C;EAE5F,OAAO,WAAW,MAAM;GACtB,cAAc,WAAW,KAAK,QAAQ,YAAY,cAAc,KAAK,WAAW,EAAE;GAClF,MAAM;GACN,QAAQ;EACV,CAAC;CACH,CAAC,CACH,EAAA,CAEa,KAAK,EAAE;AACtB;;;;ACIA,MAAM,gBAAgB,OACpB,MACA,UACA,YACqB;CACrB,MAAM,OAAO,YAAY,mBAAoB,WAA+B;CAE5E,MAAM,YAAY,OAAO,KAAK,QAAQ,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS;CAO7D,MAAM,eAAc,MAND,WAAW,MAAM;EAClC;EACA,GAAI,YACA;GAAE,QAAQ,QAAQ;GAAQ,cAAc,QAAQ;EAAa,IAC7D,EAAE,OAAO,QAAQ,SAAS,eAAe;CAC/C,CAAqC,EAAA,CACZ,SAAS,MAAM,UAA4B,MAAM,SAAS,SAAS;CAE5F,YAAY,WAAW,eAAe;CAEtC,OAAO;AACT;;AAEA,MAAM,aAAa,KAAc,YAAgD;CAC/E,MAAM,OAAO,IAAI,SAAS,MAAM,UAA4B,MAAM,SAAS,SAAS;CAEpF,OAAO,cACL,QAAQ,QAAQ,GAAG,GACnB,OAAO,MAAM,WAAW,gBAAgB,MAAM,GAC9C,OACF;AACF;;;;;AAMA,MAAM,iBAAiB,OACrB,UACA,YAC6B;CAC7B,MAAM,EAAE,SAAS,MAAM,UAAU,UAAU;EACzC,OAAO;EACP,YAAY;EACZ,UAAU,QAAQ;CACpB,CAAC;CACD,MAAM,OAAO;CACb,MAAM,WAA6B,CAAC;CACpC,MAAM,QAAkB,CAAC;CAEzB,MAAM,QAAQ,OAAO,SAAwC;EAC3D,MAAM,WAA6B,CAAC;EAEpC,KAAK,MAAM,SAAS,KAAK,UAAU,SAAS,KAAK,GAAI,MAAM,WAAW,KAAK,CAAE;EAE7E,KAAK,WAAW;CAClB;CACA,MAAM,aAAa,OAAO,UAA+D;EACvF,IAAI,MAAM,SAAS,WAAW,OAAO,CAAC,KAAuB;EAE7D,MAAM,UAAU,YAAY,KAAK;EAGjC,IAAI,SAAS,SAAS,cAAc;GAClC,IAAI,QAAQ,MAAM,QAAQ,UAAU,OAAO,CAAC;GAE5C,MAAM,MAAM,KAAK;GAEjB,OAAO,MAAM;EACf;EAEA,MAAM,QAAQ,aAAa,KAAK,MAAM,OAAO,CAAC,GAAG;EACjD,MAAM,OAAO,MAAM,WAAW;EAC9B,MAAM,SAAS,SAAS,SAAS;EAEjC,IAAI,MAAM,YAAY,OAAO,OAAO,CAAC,MAAM,UAAU,OAAO,QAAQ,SAAS,CAAC;EAE9E,IAAI,SAAS,OAAO,MAAM,WAAW,OAAO,UAC1C,SAAS,KAAK;GAAE,IAAI,MAAM,WAAW;GAAI,MAAM,QAAQ,KAAK;GAAG,OAAO,OAAO,KAAK;EAAE,CAAC;EAGvF,IAAI,MAAM,YAAY,OAAO,OAAO,SAAS,UAC3C,MAAM,WAAW,OAAO,QAAQ,QAAQ,IAAI;EAG9C,IAAI,OAAO,SAAS,MAAM,SAAS,UAAU,MAAM,KAAK,QAAQ,MAAM,IAAI;EAE1E,IAAI,UAAU,OAAO,QAAQ,MAAM,SAAS,UAAU;GACpD,MAAM,QAAQ;IAAE,GAAG,QAAQ;IAAO,MAAM,QAAQ,QAAQ,QAAQ,MAAM,IAAI;GAAE;GAE5E,MAAM,WAAW,oBAAoB,KAAK,UAAU,KAAK;EAC3D;EAEA,MAAM,MAAM,KAAK;EAEjB,OAAO,CAAC,KAAK;CACf;CAEA,MAAM,MAAM,IAAI;CAEhB,OAAO;EAAE,OAAO;EAAM;EAAU,SAAS,MAAM,cAAc,KAAK;CAAE;AACtE;;AAGA,MAAM,kBAAkB,OACtB,UACA,cAC6B;CAC7B,MAAM,WAAW,MAAM,aAAa,UAAU;EAC5C,UAAU;EACV,eAAe,UAAU,MAAM;EAE/B,QAAQ,EACN,OAAO;GACL,YAAY;GACZ,oBAAoB,SAAS;IAC3B,OAAO;KAAE,MAAM;KAAQ,MAAM,IAAI,WAAW,OAAO,KAAK,QAAQ,GAAG;IAAG;GACxE;GACA,qBAAqB,MAAM,YAAY;IACrC,OAAO,cAAc,OAAO,QAAQ,SAAS,KAAK,QAAQ,IAAI;GAChE;EACF,EACF;CACF,CAAC;CAED,OAAO,eAAe,UAAU;EAC9B,WAAW,UAAU,OAAO,MAAM,OAAO,OAAO;EAChD,UAAU,SAAS;EACnB;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { AndesineAPIError, createClient, paginatePages } from "@andesine/sdk";
|
|
2
|
+
import { getSecret } from "astro:env/server";
|
|
3
|
+
//#region src/sources/andesine/read.ts
|
|
4
|
+
/**
|
|
5
|
+
* Returns the secret key from the environment of the build or the server, through the adapter.
|
|
6
|
+
* Without one, builds use the publishable key; server routes only exist for sources without it.
|
|
7
|
+
*/
|
|
8
|
+
const getAPIKey = (source) => {
|
|
9
|
+
const key = getSecret(source.apiKeyEnv) ?? source.publicKey;
|
|
10
|
+
if (!key) throw new Error(`Source "${source.id}": set publicKey or ${source.apiKeyEnv}. Never use a PUBLIC_ variable for a secret key.`);
|
|
11
|
+
return key;
|
|
12
|
+
};
|
|
13
|
+
const toReadError = (source, error) => {
|
|
14
|
+
const label = `Source "${source.id}"`;
|
|
15
|
+
if (!(error instanceof AndesineAPIError)) return /* @__PURE__ */ new Error(`${label}: the Andesine read failed. Check the connection and try again.`);
|
|
16
|
+
if (error.status === 404) return /* @__PURE__ */ new Error(`${label}: no publication found. Check the collection ID, then publish it.`);
|
|
17
|
+
if (error.status === 401 || error.status === 403) return /* @__PURE__ */ new Error(`${label}: the API key cannot read published content.`);
|
|
18
|
+
if (error.code === "CONTENT_SCHEMA_INVALID" || error.code === "CONTENT_SCHEMA_MISMATCH") return /* @__PURE__ */ new Error(`${label}: published content does not match its schema (${error.code}).`);
|
|
19
|
+
return /* @__PURE__ */ new Error(`${label}: the Andesine read failed (${error.status}, ${error.code}).`);
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Reads the latest publication of a collection. All reads use the snapshot of the first one,
|
|
23
|
+
* so the tree and the entries always agree.
|
|
24
|
+
*/
|
|
25
|
+
const readPublication = async (source) => {
|
|
26
|
+
const client = createClient({
|
|
27
|
+
baseURL: source.apiURL,
|
|
28
|
+
apiKey: getAPIKey(source)
|
|
29
|
+
});
|
|
30
|
+
const entries = [];
|
|
31
|
+
try {
|
|
32
|
+
const tree = await client.content.getTree({
|
|
33
|
+
collectionID: source.collection,
|
|
34
|
+
channel: "published"
|
|
35
|
+
});
|
|
36
|
+
const snapshot = client.atSnapshot(tree.snapshotID);
|
|
37
|
+
for await (const page of paginatePages((cursor) => {
|
|
38
|
+
return snapshot.listEntries({
|
|
39
|
+
collectionID: source.collection,
|
|
40
|
+
descendants: true,
|
|
41
|
+
includeContent: true,
|
|
42
|
+
limit: 100,
|
|
43
|
+
cursor
|
|
44
|
+
});
|
|
45
|
+
})) entries.push(...page.data);
|
|
46
|
+
return {
|
|
47
|
+
tree,
|
|
48
|
+
entries
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw toReadError(source, error);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { readPublication as n, getAPIKey as t };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=read-DCv1lmcM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-DCv1lmcM.js","names":[],"sources":["../../src/sources/andesine/read.ts"],"sourcesContent":["import {\n AndesineAPIError,\n createClient,\n paginatePages,\n type PublishedEntryContent,\n type PublishedTree\n} from \"@andesine/sdk\";\nimport { getSecret } from \"astro:env/server\";\nimport type { AndesineSourceConfig } from \"../../config\";\n\ninterface Publication {\n tree: PublishedTree;\n entries: PublishedEntryContent[];\n}\n\n/**\n * Returns the secret key from the environment of the build or the server, through the adapter.\n * Without one, builds use the publishable key; server routes only exist for sources without it.\n */\nconst getAPIKey = (source: AndesineSourceConfig): string => {\n const key = getSecret(source.apiKeyEnv) ?? source.publicKey;\n\n if (!key) {\n throw new Error(\n `Source \"${source.id}\": set publicKey or ${source.apiKeyEnv}. Never use a PUBLIC_ variable for a secret key.`\n );\n }\n\n return key;\n};\nconst toReadError = (source: AndesineSourceConfig, error: unknown): Error => {\n const label = `Source \"${source.id}\"`;\n\n if (!(error instanceof AndesineAPIError)) {\n // Keep request URLs, keys, and upstream bodies out of messages.\n return new Error(`${label}: the Andesine read failed. Check the connection and try again.`);\n }\n\n if (error.status === 404) {\n return new Error(`${label}: no publication found. Check the collection ID, then publish it.`);\n }\n\n if (error.status === 401 || error.status === 403) {\n return new Error(`${label}: the API key cannot read published content.`);\n }\n\n if (error.code === \"CONTENT_SCHEMA_INVALID\" || error.code === \"CONTENT_SCHEMA_MISMATCH\") {\n return new Error(`${label}: published content does not match its schema (${error.code}).`);\n }\n\n return new Error(`${label}: the Andesine read failed (${error.status}, ${error.code}).`);\n};\n/**\n * Reads the latest publication of a collection. All reads use the snapshot of the first one,\n * so the tree and the entries always agree.\n */\nconst readPublication = async (source: AndesineSourceConfig): Promise<Publication> => {\n const client = createClient({ baseURL: source.apiURL, apiKey: getAPIKey(source) });\n const entries: PublishedEntryContent[] = [];\n\n try {\n const tree = await client.content.getTree({\n collectionID: source.collection,\n channel: \"published\"\n });\n const snapshot = client.atSnapshot(tree.snapshotID);\n\n for await (const page of paginatePages((cursor) => {\n return snapshot.listEntries({\n collectionID: source.collection,\n descendants: true,\n includeContent: true,\n limit: 100,\n cursor\n });\n })) {\n entries.push(...page.data);\n }\n\n return { tree, entries };\n } catch (error) {\n throw toReadError(source, error);\n }\n};\n\nexport { getAPIKey, readPublication };\nexport type { Publication };\n"],"mappings":";;;;;;;AAmBA,MAAM,aAAa,WAAyC;CAC1D,MAAM,MAAM,UAAU,OAAO,SAAS,KAAK,OAAO;CAElD,IAAI,CAAC,KACH,MAAM,IAAI,MACR,WAAW,OAAO,GAAG,sBAAsB,OAAO,UAAU,iDAC9D;CAGF,OAAO;AACT;AACA,MAAM,eAAe,QAA8B,UAA0B;CAC3E,MAAM,QAAQ,WAAW,OAAO,GAAG;CAEnC,IAAI,EAAE,iBAAiB,mBAErB,uBAAO,IAAI,MAAM,GAAG,MAAM,gEAAgE;CAG5F,IAAI,MAAM,WAAW,KACnB,uBAAO,IAAI,MAAM,GAAG,MAAM,kEAAkE;CAG9F,IAAI,MAAM,WAAW,OAAO,MAAM,WAAW,KAC3C,uBAAO,IAAI,MAAM,GAAG,MAAM,6CAA6C;CAGzE,IAAI,MAAM,SAAS,4BAA4B,MAAM,SAAS,2BAC5D,uBAAO,IAAI,MAAM,GAAG,MAAM,iDAAiD,MAAM,KAAK,GAAG;CAG3F,uBAAO,IAAI,MAAM,GAAG,MAAM,8BAA8B,MAAM,OAAO,IAAI,MAAM,KAAK,GAAG;AACzF;;;;;AAKA,MAAM,kBAAkB,OAAO,WAAuD;CACpF,MAAM,SAAS,aAAa;EAAE,SAAS,OAAO;EAAQ,QAAQ,UAAU,MAAM;CAAE,CAAC;CACjF,MAAM,UAAmC,CAAC;CAE1C,IAAI;EACF,MAAM,OAAO,MAAM,OAAO,QAAQ,QAAQ;GACxC,cAAc,OAAO;GACrB,SAAS;EACX,CAAC;EACD,MAAM,WAAW,OAAO,WAAW,KAAK,UAAU;EAElD,WAAW,MAAM,QAAQ,eAAe,WAAW;GACjD,OAAO,SAAS,YAAY;IAC1B,cAAc,OAAO;IACrB,aAAa;IACb,gBAAgB;IAChB,OAAO;IACP;GACF,CAAC;EACH,CAAC,GACC,QAAQ,KAAK,GAAG,KAAK,IAAI;EAG3B,OAAO;GAAE;GAAM;EAAQ;CACzB,SAAS,OAAO;EACd,MAAM,YAAY,QAAQ,KAAK;CACjC;AACF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { t as loadAndesineSource } from "./stored-CabomV-C.js";
|
|
2
|
+
import { t as getAPIKey } from "./read-DCv1lmcM.js";
|
|
3
|
+
import { t as isRateLimited } from "./limits-Ppc7BQPG.js";
|
|
4
|
+
import { AndesineAPIError, createClient } from "@andesine/sdk";
|
|
5
|
+
//#region src/search/request.ts
|
|
6
|
+
const resolvers = /* @__PURE__ */ new Map();
|
|
7
|
+
const parseJSON = (text) => {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(text);
|
|
10
|
+
} catch {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const json = (body, status = 200, headers = {}) => {
|
|
15
|
+
return Response.json(body, {
|
|
16
|
+
status,
|
|
17
|
+
headers: {
|
|
18
|
+
"Cache-Control": "private, no-store",
|
|
19
|
+
...headers
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
/** Reads a body of up to `maxSize` characters; larger bodies give `undefined`. */
|
|
24
|
+
const readText = async (request, maxSize) => {
|
|
25
|
+
const reader = request.body?.getReader();
|
|
26
|
+
const decoder = new TextDecoder();
|
|
27
|
+
let text = "";
|
|
28
|
+
if (!reader) return text;
|
|
29
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
30
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
31
|
+
if (text.length > maxSize) {
|
|
32
|
+
await reader.cancel();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return `${text}${decoder.decode()}`;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Checks the method, the origin and the body of a browser request for an Andesine source.
|
|
40
|
+
* The browser can only choose a configured source; the API key stays on the server.
|
|
41
|
+
*/
|
|
42
|
+
const readRequest = async (request, config, options) => {
|
|
43
|
+
const origin = request.headers.get("origin");
|
|
44
|
+
const allowed = [new URL(request.url).origin, new URL(config.site).origin];
|
|
45
|
+
if (request.method !== "POST") return { response: json({ error: "Use POST." }, 405) };
|
|
46
|
+
if (origin && !allowed.includes(origin)) return { response: json({ error: "Origin not allowed." }, 403) };
|
|
47
|
+
const text = await readText(request, options.maxSize);
|
|
48
|
+
const body = text === void 0 ? void 0 : options.schema.safeParse(parseJSON(text));
|
|
49
|
+
const source = config.sources.find((item) => {
|
|
50
|
+
return item.type === "andesine" && item.id === body?.data?.source;
|
|
51
|
+
});
|
|
52
|
+
if (!body?.success || !source) return { response: json({ error: "Invalid request." }, 400) };
|
|
53
|
+
try {
|
|
54
|
+
return { request: {
|
|
55
|
+
body: body.data,
|
|
56
|
+
source,
|
|
57
|
+
client: createClient({
|
|
58
|
+
baseURL: source.apiURL,
|
|
59
|
+
apiKey: getAPIKey(source)
|
|
60
|
+
}),
|
|
61
|
+
signal: AbortSignal.any([request.signal, AbortSignal.timeout(options.timeout)])
|
|
62
|
+
} };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(error);
|
|
65
|
+
return { response: json({ error: "The server is not configured." }, 502) };
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Maps results to page URLs, from the built pages. Results for pages that were published after
|
|
70
|
+
* the build have no URL and are left out. The pages do not change, so each source loads once.
|
|
71
|
+
*/
|
|
72
|
+
const createPageResolver = (source, base) => {
|
|
73
|
+
if (!resolvers.has(source.id)) {
|
|
74
|
+
const resolver = loadAndesineSource(source, base).then(({ pages }) => {
|
|
75
|
+
const shown = new Map(pages.filter((page) => !page.searchHidden).map((page) => [page.id, page]));
|
|
76
|
+
return (result) => shown.get(result.entryID)?.href;
|
|
77
|
+
});
|
|
78
|
+
resolvers.set(source.id, resolver);
|
|
79
|
+
resolver.catch(() => resolvers.delete(source.id));
|
|
80
|
+
}
|
|
81
|
+
return resolvers.get(source.id);
|
|
82
|
+
};
|
|
83
|
+
/** Maps API failures to a response; only unexpected failures are logged. */
|
|
84
|
+
const toErrorResponse = (error, messages) => {
|
|
85
|
+
const limited = error instanceof AndesineAPIError && isRateLimited(error.status, error.data);
|
|
86
|
+
if (!limited) console.error(error);
|
|
87
|
+
return limited ? json({ error: messages.limited }, 429) : json({ error: messages.failed }, 502);
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
export { toErrorResponse as i, json as n, readRequest as r, createPageResolver as t };
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=request-DOuBHGQs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-DOuBHGQs.js","names":[],"sources":["../../src/search/request.ts"],"sourcesContent":["import { AndesineAPIError, createClient } from \"@andesine/sdk\";\nimport type { z } from \"zod\";\nimport type { AndesineSourceConfig, PagesConfig } from \"../config\";\nimport { loadAndesineSource } from \"../sources/andesine/stored\";\nimport { getAPIKey } from \"../sources/andesine/read\";\nimport type { PageResolver } from \"./items\";\nimport { isRateLimited } from \"./limits\";\n\ninterface RequestOptions<T> {\n schema: z.ZodType<T>;\n /** Largest accepted body, in characters. */\n maxSize: number;\n /** Time limit for the upstream API, in milliseconds. */\n timeout: number;\n}\n\ninterface AndesineRequest<T> {\n body: T;\n source: AndesineSourceConfig;\n client: ReturnType<typeof createClient>;\n signal: AbortSignal;\n}\n\ntype RequestResult<T> = { request: AndesineRequest<T> } | { response: Response };\n\nconst resolvers = new Map<string, Promise<PageResolver>>();\n\nconst parseJSON = (text: string): unknown => {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n};\nconst json = (body: unknown, status = 200, headers: Record<string, string> = {}): Response => {\n return Response.json(body, {\n status,\n headers: { \"Cache-Control\": \"private, no-store\", ...headers }\n });\n};\n/** Reads a body of up to `maxSize` characters; larger bodies give `undefined`. */\nconst readText = async (request: Request, maxSize: number): Promise<string | undefined> => {\n const reader = request.body?.getReader();\n const decoder = new TextDecoder();\n\n let text = \"\";\n\n if (!reader) return text;\n\n for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {\n text += decoder.decode(chunk.value, { stream: true });\n\n if (text.length > maxSize) {\n await reader.cancel();\n\n return undefined;\n }\n }\n\n return `${text}${decoder.decode()}`;\n};\n/**\n * Checks the method, the origin and the body of a browser request for an Andesine source.\n * The browser can only choose a configured source; the API key stays on the server.\n */\nconst readRequest = async <T extends { source: string }>(\n request: Request,\n config: PagesConfig,\n options: RequestOptions<T>\n): Promise<RequestResult<T>> => {\n const origin = request.headers.get(\"origin\");\n const allowed = [new URL(request.url).origin, new URL(config.site).origin];\n\n if (request.method !== \"POST\") return { response: json({ error: \"Use POST.\" }, 405) };\n\n if (origin && !allowed.includes(origin)) {\n return { response: json({ error: \"Origin not allowed.\" }, 403) };\n }\n\n const text = await readText(request, options.maxSize);\n const body = text === undefined ? undefined : options.schema.safeParse(parseJSON(text));\n const source = config.sources.find((item): item is AndesineSourceConfig => {\n return item.type === \"andesine\" && item.id === body?.data?.source;\n });\n\n if (!body?.success || !source) return { response: json({ error: \"Invalid request.\" }, 400) };\n\n try {\n return {\n request: {\n body: body.data,\n source,\n client: createClient({ baseURL: source.apiURL, apiKey: getAPIKey(source) }),\n signal: AbortSignal.any([request.signal, AbortSignal.timeout(options.timeout)])\n }\n };\n } catch (error) {\n console.error(error);\n\n return { response: json({ error: \"The server is not configured.\" }, 502) };\n }\n};\n/**\n * Maps results to page URLs, from the built pages. Results for pages that were published after\n * the build have no URL and are left out. The pages do not change, so each source loads once.\n */\nconst createPageResolver = (source: AndesineSourceConfig, base: string): Promise<PageResolver> => {\n if (!resolvers.has(source.id)) {\n const resolver = loadAndesineSource(source, base).then(({ pages }): PageResolver => {\n const shown = new Map(\n pages.filter((page) => !page.searchHidden).map((page) => [page.id, page])\n );\n\n return (result) => shown.get(result.entryID)?.href;\n });\n\n resolvers.set(source.id, resolver);\n resolver.catch(() => resolvers.delete(source.id));\n }\n\n return resolvers.get(source.id)!;\n};\n/** Maps API failures to a response; only unexpected failures are logged. */\nconst toErrorResponse = (\n error: unknown,\n messages: { limited: string; failed: string }\n): Response => {\n const limited = error instanceof AndesineAPIError && isRateLimited(error.status, error.data);\n\n if (!limited) console.error(error);\n\n return limited ? json({ error: messages.limited }, 429) : json({ error: messages.failed }, 502);\n};\n\nexport { readRequest, createPageResolver, toErrorResponse, json };\nexport type { AndesineRequest };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/output/response.ts
|
|
2
|
+
const textResponse = (body, type, init = {}) => {
|
|
3
|
+
return new Response(body, {
|
|
4
|
+
...init,
|
|
5
|
+
headers: {
|
|
6
|
+
"Content-Type": `${type}; charset=utf-8`,
|
|
7
|
+
"X-Content-Type-Options": "nosniff",
|
|
8
|
+
...init.headers
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { textResponse as t };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=response-CtzaKMWH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-CtzaKMWH.js","names":[],"sources":["../../src/output/response.ts"],"sourcesContent":["const textResponse = (body: string | null, type: string, init: ResponseInit = {}): Response => {\n return new Response(body, {\n ...init,\n headers: {\n \"Content-Type\": `${type}; charset=utf-8`,\n \"X-Content-Type-Options\": \"nosniff\",\n ...init.headers\n }\n });\n};\nexport { textResponse };\n"],"mappings":";AAAA,MAAM,gBAAgB,MAAqB,MAAc,OAAqB,CAAC,MAAgB;CAC7F,OAAO,IAAI,SAAS,MAAM;EACxB,GAAG;EACH,SAAS;GACP,gBAAgB,GAAG,KAAK;GACxB,0BAA0B;GAC1B,GAAG,KAAK;EACV;CACF,CAAC;AACH"}
|