reallysimpledocs 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +84 -0
- package/package.json +42 -0
- package/src/astro/index.js +131 -0
- package/src/components/index.js +4 -0
- package/src/css/custom.css +157 -0
- package/src/css/overrides.css +1 -0
- package/src/css/sources.css +1 -0
- package/src/icons/logos/claude.svg +1 -0
- package/src/icons/logos/cursor.svg +1 -0
- package/src/icons/logos/markdown.svg +1 -0
- package/src/icons/logos/openai.svg +1 -0
- package/src/index.js +1 -0
- package/src/runtime/components/BaseLayout.astro +83 -0
- package/src/runtime/components/CommandDialog.astro +269 -0
- package/src/runtime/components/DefaultSidebarHeader.astro +24 -0
- package/src/runtime/components/DocsLayout.astro +287 -0
- package/src/runtime/components/DropdownMenu.astro +22 -0
- package/src/runtime/components/Sidebar.astro +20 -0
- package/src/runtime/lib/docs.js +334 -0
- package/src/runtime/lib/html.js +20 -0
- package/src/runtime/lib/macros.js +180 -0
- package/src/runtime/lib/paths.js +22 -0
- package/src/runtime/lib/site.js +26 -0
- package/src/runtime/pages/doc.astro +20 -0
- package/src/runtime/pages/llms-full.txt.ts +20 -0
- package/src/runtime/pages/llms.txt.ts +34 -0
- package/src/runtime/pages/markdown.ts +17 -0
- package/src/runtime/pages/search-index.ts +27 -0
- package/src/runtime/scripts/copy-code.js +54 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { marked } from "marked";
|
|
4
|
+
import GithubSlugger from "github-slugger";
|
|
5
|
+
import { codeToHtml } from "shiki";
|
|
6
|
+
import * as lucideIcons from "lucide-static";
|
|
7
|
+
import { markdownPath, routePath } from "./paths.js";
|
|
8
|
+
|
|
9
|
+
export function getManifest(config) {
|
|
10
|
+
const manifestPath = path.join(config.docsDir, "docs.json");
|
|
11
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const fallbackLabelFromSlug = (slug) => {
|
|
15
|
+
if (slug === "index") return "Introduction";
|
|
16
|
+
const parts = String(slug).split("/");
|
|
17
|
+
const last = parts.at(-1);
|
|
18
|
+
const base = last === "index" && parts.length > 1 ? parts.at(-2) : last;
|
|
19
|
+
return base.replace(/-/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const slugToFile = (slug) => path.join(...String(slug).split("/")) + ".md";
|
|
23
|
+
|
|
24
|
+
export function flattenMenuSlugs(menu) {
|
|
25
|
+
const out = [];
|
|
26
|
+
const walk = (items = []) => {
|
|
27
|
+
items.forEach((item) => {
|
|
28
|
+
if (typeof item === "string") {
|
|
29
|
+
out.push(item);
|
|
30
|
+
} else if (getPageItemSlug(item)) {
|
|
31
|
+
out.push(getPageItemSlug(item));
|
|
32
|
+
} else if (item?.type === "submenu") {
|
|
33
|
+
walk(item.items || []);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
(menu || []).forEach((group) => {
|
|
39
|
+
if (group?.type === "group") walk(group.items || []);
|
|
40
|
+
});
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getPages(config) {
|
|
45
|
+
const manifest = getManifest(config);
|
|
46
|
+
return flattenMenuSlugs(manifest.menu || []).map((slug) => {
|
|
47
|
+
const file = slugToFile(slug);
|
|
48
|
+
const filePath = path.join(config.docsDir, file);
|
|
49
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
50
|
+
const parsed = parseDocMarkdown(source, fallbackLabelFromSlug(slug));
|
|
51
|
+
return {
|
|
52
|
+
slug,
|
|
53
|
+
file,
|
|
54
|
+
path: routePath(config.routeBase, slug),
|
|
55
|
+
markdownPath: markdownPath(config.routeBase, slug),
|
|
56
|
+
title: parsed.title,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getPage(config, slug) {
|
|
62
|
+
const normalizedSlug = slug || "index";
|
|
63
|
+
return getPages(config).find((page) => page.slug === normalizedSlug);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getDocMarkdown(config, page) {
|
|
67
|
+
const filePath = path.resolve(config.docsDir, page.file);
|
|
68
|
+
const docsRoot = path.resolve(config.docsDir);
|
|
69
|
+
const relativePath = path.relative(docsRoot, filePath);
|
|
70
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath) || !fs.existsSync(filePath)) {
|
|
71
|
+
throw new Error(`Invalid docs page file for ${page.slug}: ${page.file}`);
|
|
72
|
+
}
|
|
73
|
+
return fs.readFileSync(filePath, "utf8");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getMarkdownExport(config, page) {
|
|
77
|
+
return `${getDocMarkdown(config, page).trim()}\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function renderDoc(config, page) {
|
|
81
|
+
const parsed = parseDocMarkdown(getDocMarkdown(config, page), page.title);
|
|
82
|
+
const content = renderNunjucksCompat(parsed.body);
|
|
83
|
+
const headings = [];
|
|
84
|
+
const slugger = new GithubSlugger();
|
|
85
|
+
const renderer = new marked.Renderer();
|
|
86
|
+
|
|
87
|
+
renderer.heading = ({ tokens, depth }) => {
|
|
88
|
+
const text = renderer.parser.parseInline(tokens);
|
|
89
|
+
const headingText = plainText(tokens);
|
|
90
|
+
const id = slugger.slug(headingText);
|
|
91
|
+
if (depth > 1 && depth < 4) headings.push({ depth, id, text: headingText });
|
|
92
|
+
return `<h${depth} id="${id}" tabindex="-1"><a class="header-anchor" href="#${id}">${text}</a></h${depth}>`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
renderer.table = (token) => {
|
|
96
|
+
const header = renderer.tablerow({
|
|
97
|
+
text: token.header.map((cell) => renderer.tablecell(cell)).join(""),
|
|
98
|
+
});
|
|
99
|
+
const body = token.rows
|
|
100
|
+
.map((row) =>
|
|
101
|
+
renderer.tablerow({
|
|
102
|
+
text: row.map((cell) => renderer.tablecell(cell)).join(""),
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
.join("");
|
|
106
|
+
const tbody = body ? `<tbody>${body}</tbody>` : "";
|
|
107
|
+
return `<div class="relative my-6 w-full overflow-auto"><table><thead>${header}</thead>${tbody}</table></div>`;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const tokens = marked.lexer(content);
|
|
111
|
+
await highlightCode(tokens);
|
|
112
|
+
const html = marked.parser(tokens, { renderer });
|
|
113
|
+
return { page, html, headings };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderNunjucksCompat(content) {
|
|
117
|
+
return String(content).replace(
|
|
118
|
+
/\{%\s*lucide\s+["']([^"']+)["'](?:\s*,\s*[^%]+)?\s*%\}/g,
|
|
119
|
+
(_, icon) => resolveIcon(icon),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getNavigation(config, page) {
|
|
124
|
+
const pages = getPages(config);
|
|
125
|
+
const index = pages.findIndex((entry) => entry.slug === page.slug);
|
|
126
|
+
return {
|
|
127
|
+
prev: index > 0 ? pages[index - 1] : null,
|
|
128
|
+
next: index >= 0 && index < pages.length - 1 ? pages[index + 1] : null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function resolveIcon(icon) {
|
|
133
|
+
if (!icon || typeof icon !== "string") return "";
|
|
134
|
+
const trimmed = icon.trim();
|
|
135
|
+
if (!trimmed) return "";
|
|
136
|
+
if (trimmed.startsWith("<svg")) return trimmed;
|
|
137
|
+
const pascalName = trimmed.includes("-")
|
|
138
|
+
? trimmed
|
|
139
|
+
.split("-")
|
|
140
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
141
|
+
.join("")
|
|
142
|
+
: trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
|
|
143
|
+
const iconClass = trimmed.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
144
|
+
return lucideIcons[pascalName]?.replace('class="lucide', `class="lucide lucide-${iconClass}`) || "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function buildMenus(config, currentSlug) {
|
|
148
|
+
const pages = getPages(config);
|
|
149
|
+
const bySlug = new Map(pages.map((page) => [page.slug, page]));
|
|
150
|
+
|
|
151
|
+
const processSlug = (slug, item = {}) => {
|
|
152
|
+
const page = bySlug.get(slug);
|
|
153
|
+
if (!page) return null;
|
|
154
|
+
const icon = resolveIcon(item.icon);
|
|
155
|
+
return {
|
|
156
|
+
type: "item",
|
|
157
|
+
icon,
|
|
158
|
+
url: page.path,
|
|
159
|
+
label: page.title,
|
|
160
|
+
current: page.slug === currentSlug,
|
|
161
|
+
keywords: page.title,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const processItem = (item) => {
|
|
166
|
+
if (typeof item === "string") return processSlug(item);
|
|
167
|
+
const slug = getPageItemSlug(item);
|
|
168
|
+
if (slug) return processSlug(slug, item);
|
|
169
|
+
if (item?.type === "submenu") {
|
|
170
|
+
const items = (item.items || []).map(processItem).filter(Boolean);
|
|
171
|
+
return {
|
|
172
|
+
type: "submenu",
|
|
173
|
+
label: item.label,
|
|
174
|
+
icon: resolveIcon(item.icon),
|
|
175
|
+
open: item.open || items.some((child) => child.current),
|
|
176
|
+
items,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const sidebar = (getManifest(config).menu || [])
|
|
183
|
+
.filter((group) => group?.type === "group")
|
|
184
|
+
.map((group) => ({
|
|
185
|
+
type: "group",
|
|
186
|
+
label: group.label,
|
|
187
|
+
items: (group.items || []).map(processItem).filter(Boolean),
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
const command = sidebar.map((group) => ({
|
|
191
|
+
type: "group",
|
|
192
|
+
label: group.label,
|
|
193
|
+
items: flattenSidebarItems(group.items).map((item) => ({
|
|
194
|
+
type: "item",
|
|
195
|
+
icon: item.icon,
|
|
196
|
+
url: item.url,
|
|
197
|
+
label: item.label,
|
|
198
|
+
keywords: item.keywords,
|
|
199
|
+
})),
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
return { sidebar, command };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function getLlmDocs(config) {
|
|
206
|
+
return getPages(config).map((page) => ({
|
|
207
|
+
...page,
|
|
208
|
+
content: parseDocMarkdown(getDocMarkdown(config, page), page.title).body.trim(),
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function getSearchDocs(config) {
|
|
213
|
+
return getPages(config).map((page) => {
|
|
214
|
+
const parsed = parseDocMarkdown(getDocMarkdown(config, page), page.title);
|
|
215
|
+
return {
|
|
216
|
+
slug: page.slug,
|
|
217
|
+
title: page.title,
|
|
218
|
+
path: page.path,
|
|
219
|
+
body: markdownPlainText(parsed.body),
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function flattenSidebarItems(items = []) {
|
|
225
|
+
return items.flatMap((item) => (item.type === "submenu" ? flattenSidebarItems(item.items) : [item]));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function getPageItemSlug(item) {
|
|
229
|
+
if (!item || typeof item !== "object" || item.type === "submenu") return "";
|
|
230
|
+
return item.slug || item.page || "";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseDocMarkdown(source, fallbackTitle) {
|
|
234
|
+
const content = String(source || "").trimStart();
|
|
235
|
+
const tokens = marked.lexer(content);
|
|
236
|
+
const first = tokens[0];
|
|
237
|
+
|
|
238
|
+
if (first?.type !== "heading" || first.depth !== 1) {
|
|
239
|
+
return {
|
|
240
|
+
title: fallbackTitle,
|
|
241
|
+
body: content,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
title: plainText(first.tokens || []).trim() || fallbackTitle,
|
|
247
|
+
body: content.slice(first.raw.length).trimStart(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function markdownPlainText(markdown) {
|
|
252
|
+
return tokensText(marked.lexer(renderNunjucksCompat(markdown)));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function highlightCode(tokens) {
|
|
256
|
+
for (const token of tokens) {
|
|
257
|
+
if (token.type === "code") {
|
|
258
|
+
const html = await highlightToken(token);
|
|
259
|
+
Object.assign(token, {
|
|
260
|
+
type: "html",
|
|
261
|
+
raw: html,
|
|
262
|
+
text: html,
|
|
263
|
+
block: true,
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (const childTokens of nestedTokenLists(token)) {
|
|
269
|
+
await highlightCode(childTokens);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function highlightToken(token) {
|
|
275
|
+
try {
|
|
276
|
+
return await codeToHtml(token.text, {
|
|
277
|
+
lang: normalizeLanguage(token.lang),
|
|
278
|
+
themes: {
|
|
279
|
+
light: "github-light",
|
|
280
|
+
dark: "github-dark",
|
|
281
|
+
},
|
|
282
|
+
defaultColor: false,
|
|
283
|
+
});
|
|
284
|
+
} catch {
|
|
285
|
+
return codeToHtml(token.text, {
|
|
286
|
+
lang: "text",
|
|
287
|
+
themes: {
|
|
288
|
+
light: "github-light",
|
|
289
|
+
dark: "github-dark",
|
|
290
|
+
},
|
|
291
|
+
defaultColor: false,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeLanguage(lang) {
|
|
297
|
+
const value = lang?.trim().split(/\s+/)[0].toLowerCase() || "text";
|
|
298
|
+
if (value === "njk") return "html";
|
|
299
|
+
if (value === "sh" || value === "shell") return "bash";
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function nestedTokenLists(token) {
|
|
304
|
+
const lists = [];
|
|
305
|
+
if (token.tokens) lists.push(token.tokens);
|
|
306
|
+
if (token.items) lists.push(...token.items.flatMap((item) => (item.tokens ? [item.tokens] : [])));
|
|
307
|
+
if (token.rows) lists.push(...token.rows.flatMap((cell) => (cell.tokens ? [cell.tokens] : [])));
|
|
308
|
+
if (token.header) lists.push(...token.header.flatMap((cell) => (cell.tokens ? [cell.tokens] : [])));
|
|
309
|
+
return lists;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function plainText(tokens) {
|
|
313
|
+
return tokens
|
|
314
|
+
.map((token) => {
|
|
315
|
+
if (token.tokens) return plainText(token.tokens);
|
|
316
|
+
return token.text ?? token.raw ?? "";
|
|
317
|
+
})
|
|
318
|
+
.join("");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function tokensText(tokens) {
|
|
322
|
+
return tokens
|
|
323
|
+
.map((token) => {
|
|
324
|
+
const childTokens = [];
|
|
325
|
+
if (token.tokens) childTokens.push(token.tokens);
|
|
326
|
+
if (token.items) childTokens.push(...token.items.flatMap((item) => (item.tokens ? [item.tokens] : [])));
|
|
327
|
+
if (token.rows) childTokens.push(...token.rows.flatMap((row) => row.flatMap((cell) => (cell.tokens ? [cell.tokens] : []))));
|
|
328
|
+
if (token.header) childTokens.push(...token.header.flatMap((cell) => (cell.tokens ? [cell.tokens] : [])));
|
|
329
|
+
return childTokens.length ? childTokens.map(tokensText).join(" ") : token.text || "";
|
|
330
|
+
})
|
|
331
|
+
.join(" ")
|
|
332
|
+
.replace(/\s+/g, " ")
|
|
333
|
+
.trim();
|
|
334
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function escapeHtml(value) {
|
|
2
|
+
return String(value ?? "")
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/"/g, """);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function attrs(values = {}) {
|
|
10
|
+
return Object.entries(values)
|
|
11
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== false)
|
|
12
|
+
.map(([key, value]) => (value === true ? ` ${key}` : ` ${key}="${escapeHtml(value)}"`))
|
|
13
|
+
.join("");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function addSvgClass(svg, className) {
|
|
17
|
+
if (!svg || !className) return svg || "";
|
|
18
|
+
if (svg.includes('class="')) return svg.replace('class="', `class="${className} `);
|
|
19
|
+
return svg.replace("<svg", `<svg class="${escapeHtml(className)}"`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import Search from "lucide-static/icons/search.svg?raw";
|
|
2
|
+
import { attrs, escapeHtml } from "./html.js";
|
|
3
|
+
|
|
4
|
+
export function renderSidebar({
|
|
5
|
+
id = "sidebar",
|
|
6
|
+
label = "Sidebar navigation",
|
|
7
|
+
open = true,
|
|
8
|
+
side = "left",
|
|
9
|
+
header = "",
|
|
10
|
+
footer = "",
|
|
11
|
+
menu = [],
|
|
12
|
+
contentAttrs = { class: "scrollbar" },
|
|
13
|
+
} = {}) {
|
|
14
|
+
return `<aside${attrs({
|
|
15
|
+
id,
|
|
16
|
+
class: "sidebar",
|
|
17
|
+
"data-side": side,
|
|
18
|
+
"aria-hidden": open ? "false" : "true",
|
|
19
|
+
inert: open ? undefined : true,
|
|
20
|
+
})}>
|
|
21
|
+
<nav aria-label="${escapeHtml(label)}">
|
|
22
|
+
${header ? `<header>${header}</header>` : ""}
|
|
23
|
+
<section${attrs(contentAttrs)}>${renderSidebarContent(menu, `${id}-content`)}</section>
|
|
24
|
+
${footer ? `<footer>${footer}</footer>` : ""}
|
|
25
|
+
</nav>
|
|
26
|
+
</aside>`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function renderSidebarContent(items = [], parentIdPrefix = "content") {
|
|
30
|
+
return items
|
|
31
|
+
.map((item, index) => {
|
|
32
|
+
const itemId = `${parentIdPrefix}-${index + 1}`;
|
|
33
|
+
|
|
34
|
+
if (item.type === "group") {
|
|
35
|
+
const groupLabelId = item.id || `group-label-${itemId}`;
|
|
36
|
+
return `<div role="group"${item.label ? attrs({ "aria-labelledby": groupLabelId }) : ""}${attrs(item.attrs)}>
|
|
37
|
+
${item.label ? `<h3 id="${escapeHtml(groupLabelId)}">${escapeHtml(item.label)}</h3>` : ""}
|
|
38
|
+
<ul>${renderSidebarContent(item.items || [], itemId)}</ul>
|
|
39
|
+
</div>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (item.type === "separator") return `<hr role="separator" />`;
|
|
43
|
+
|
|
44
|
+
if (item.type === "submenu") {
|
|
45
|
+
const submenuId = `submenu-${itemId}`;
|
|
46
|
+
return `<li>
|
|
47
|
+
<details${attrs({ id: submenuId, open: item.open ? true : undefined, ...without(item.attrs, ["open"]) })}>
|
|
48
|
+
<summary aria-controls="${escapeHtml(submenuId)}-content">
|
|
49
|
+
${item.icon || ""}
|
|
50
|
+
<span>${escapeHtml(item.label)}</span>
|
|
51
|
+
</summary>
|
|
52
|
+
<ul id="${escapeHtml(submenuId)}-content">${renderSidebarContent(item.items || [], itemId)}</ul>
|
|
53
|
+
</details>
|
|
54
|
+
</li>`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `<li>
|
|
58
|
+
<a${attrs({ href: item.url, "aria-current": item.current ? "page" : undefined, ...without(item.attrs, ["href", "aria-current"]) })}>
|
|
59
|
+
${item.icon || ""}
|
|
60
|
+
<span>${escapeHtml(item.label)}</span>
|
|
61
|
+
</a>
|
|
62
|
+
</li>`;
|
|
63
|
+
})
|
|
64
|
+
.join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function renderCommandDialog({
|
|
68
|
+
id = "command-search",
|
|
69
|
+
items = [],
|
|
70
|
+
placeholder = "Search the docs...",
|
|
71
|
+
emptyText = "No results found.",
|
|
72
|
+
open = false,
|
|
73
|
+
commandAttrs = {},
|
|
74
|
+
} = {}) {
|
|
75
|
+
return `<dialog${attrs({
|
|
76
|
+
id,
|
|
77
|
+
class: "command-dialog",
|
|
78
|
+
"aria-label": "Command menu",
|
|
79
|
+
open: open ? true : undefined,
|
|
80
|
+
onclick: "if (event.target === this) this.close()",
|
|
81
|
+
})}>
|
|
82
|
+
<div${attrs({ ...commandAttrs, class: ["command", commandAttrs.class].filter(Boolean).join(" ") })}>
|
|
83
|
+
<header>
|
|
84
|
+
${Search}
|
|
85
|
+
<input${attrs({
|
|
86
|
+
type: "text",
|
|
87
|
+
id: `${id}-input`,
|
|
88
|
+
placeholder,
|
|
89
|
+
autocomplete: "off",
|
|
90
|
+
autocorrect: "off",
|
|
91
|
+
spellcheck: "false",
|
|
92
|
+
"aria-autocomplete": "list",
|
|
93
|
+
role: "combobox",
|
|
94
|
+
"aria-expanded": "true",
|
|
95
|
+
"aria-controls": `${id}-menu`,
|
|
96
|
+
})}>
|
|
97
|
+
</header>
|
|
98
|
+
<div${attrs({
|
|
99
|
+
role: "menu",
|
|
100
|
+
id: `${id}-menu`,
|
|
101
|
+
"aria-orientation": "vertical",
|
|
102
|
+
"data-empty": emptyText,
|
|
103
|
+
class: "scrollbar",
|
|
104
|
+
})}>${renderCommandItems(items, `${id}-items`)}</div>
|
|
105
|
+
</div>
|
|
106
|
+
</dialog>`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function renderCommandItems(items = [], parentIdPrefix = "items") {
|
|
110
|
+
return items
|
|
111
|
+
.map((item, index) => {
|
|
112
|
+
const itemId = `${parentIdPrefix}-${index + 1}`;
|
|
113
|
+
if (item.type === "group") {
|
|
114
|
+
const groupLabelId = item.id || `group-label-${itemId}`;
|
|
115
|
+
return `<div role="group" aria-labelledby="${escapeHtml(groupLabelId)}"${attrs(item.attrs)}>
|
|
116
|
+
<span role="heading" id="${escapeHtml(groupLabelId)}">${escapeHtml(item.label)}</span>
|
|
117
|
+
${renderCommandItems(item.items || [], itemId)}
|
|
118
|
+
</div>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (item.type === "separator") return `<hr role="separator" />`;
|
|
122
|
+
|
|
123
|
+
const commonAttrs = {
|
|
124
|
+
id: itemId,
|
|
125
|
+
role: "menuitem",
|
|
126
|
+
"data-keywords": item.keywords,
|
|
127
|
+
"aria-disabled": item.disabled ? "true" : undefined,
|
|
128
|
+
...without(item.attrs, ["href"]),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (item.url) {
|
|
132
|
+
return `<a${attrs({ ...commonAttrs, href: item.url })}>${item.icon || ""}<span>${escapeHtml(item.label)}</span></a>`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return `<div${attrs(commonAttrs)}>${item.icon || ""}<span>${escapeHtml(item.label)}</span></div>`;
|
|
136
|
+
})
|
|
137
|
+
.join("");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function renderDropdownMenu({ id = "dropdown-menu", trigger = "", items = [], triggerAttrs = {}, popoverAttrs = {} } = {}) {
|
|
141
|
+
return `<div id="${escapeHtml(id)}" class="dropdown-menu">
|
|
142
|
+
<button${attrs({
|
|
143
|
+
type: "button",
|
|
144
|
+
id: `${id}-trigger`,
|
|
145
|
+
"aria-haspopup": "menu",
|
|
146
|
+
"aria-controls": `${id}-menu`,
|
|
147
|
+
"aria-expanded": "false",
|
|
148
|
+
...triggerAttrs,
|
|
149
|
+
})}>${trigger}</button>
|
|
150
|
+
<div${attrs({ id: `${id}-popover`, "data-popover": true, "aria-hidden": "true", ...popoverAttrs })}>
|
|
151
|
+
<div role="menu" id="${escapeHtml(id)}-menu" aria-labelledby="${escapeHtml(id)}-trigger">
|
|
152
|
+
${renderDropdownItems(items, `${id}-items`)}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function renderDropdownItems(items = [], parentIdPrefix = "items") {
|
|
159
|
+
return items
|
|
160
|
+
.map((item, index) => {
|
|
161
|
+
const itemId = `${parentIdPrefix}-${index + 1}`;
|
|
162
|
+
if (item.type === "separator") return `<hr role="separator" />`;
|
|
163
|
+
if (item.type === "group") {
|
|
164
|
+
const groupLabelId = item.id || `group-label-${itemId}`;
|
|
165
|
+
return `<div role="group" aria-labelledby="${escapeHtml(groupLabelId)}"${attrs(item.attrs)}>
|
|
166
|
+
<div role="heading" id="${escapeHtml(groupLabelId)}">${escapeHtml(item.label)}</div>
|
|
167
|
+
${renderDropdownItems(item.items || [], itemId)}
|
|
168
|
+
</div>`;
|
|
169
|
+
}
|
|
170
|
+
if (item.url) {
|
|
171
|
+
return `<a${attrs({ id: itemId, role: "menuitem", href: item.url, ...without(item.attrs, ["href"]) })}>${item.icon || ""}${escapeHtml(item.label)}</a>`;
|
|
172
|
+
}
|
|
173
|
+
return `<div${attrs({ id: itemId, role: "menuitem", ...item.attrs })}>${item.icon || ""}${escapeHtml(item.label)}</div>`;
|
|
174
|
+
})
|
|
175
|
+
.join("");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function without(value = {}, keys = []) {
|
|
179
|
+
return Object.fromEntries(Object.entries(value || {}).filter(([key]) => !keys.includes(key)));
|
|
180
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const stripSlashes = (value) => String(value || "").replace(/^\/+|\/+$/g, "");
|
|
2
|
+
|
|
3
|
+
export const routePath = (routeBase, slug) => {
|
|
4
|
+
const base = stripSlashes(routeBase);
|
|
5
|
+
const normalizedSlug = slug === "index" ? "" : stripSlashes(slug);
|
|
6
|
+
const parts = [base, normalizedSlug].filter(Boolean);
|
|
7
|
+
return parts.length ? `/${parts.join("/")}/` : "/";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const routeSlug = (slug) => (slug === "index" ? undefined : slug);
|
|
11
|
+
|
|
12
|
+
export const markdownPath = (routeBase, slug) => {
|
|
13
|
+
const base = stripSlashes(routeBase);
|
|
14
|
+
const normalizedSlug = stripSlashes(slug || "index");
|
|
15
|
+
return `/${[base, `${normalizedSlug}.md`].filter(Boolean).join("/")}`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const routeFilePath = (routeBase, file) => {
|
|
19
|
+
const base = stripSlashes(routeBase);
|
|
20
|
+
const normalizedFile = stripSlashes(file);
|
|
21
|
+
return `/${[base, normalizedFile].filter(Boolean).join("/")}`;
|
|
22
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const defaultSite = {
|
|
4
|
+
title: "ReallySimpleDocs",
|
|
5
|
+
description: "A really simple documentation site.",
|
|
6
|
+
url: "",
|
|
7
|
+
links: [],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function getSite(config) {
|
|
11
|
+
let fileSite = {};
|
|
12
|
+
|
|
13
|
+
if (config.siteFile) {
|
|
14
|
+
try {
|
|
15
|
+
fileSite = JSON.parse(fs.readFileSync(config.siteFile, "utf8")) || {};
|
|
16
|
+
} catch {
|
|
17
|
+
fileSite = {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...defaultSite,
|
|
23
|
+
...fileSite,
|
|
24
|
+
...(config.site || {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from "virtual:reallysimpledocs/config";
|
|
3
|
+
import DocsLayout from "../components/DocsLayout.astro";
|
|
4
|
+
import { getPages, renderDoc } from "../lib/docs.js";
|
|
5
|
+
import { routeSlug } from "../lib/paths.js";
|
|
6
|
+
|
|
7
|
+
export function getStaticPaths() {
|
|
8
|
+
return getPages(config).map((page) => ({
|
|
9
|
+
params: { slug: routeSlug(page.slug) },
|
|
10
|
+
props: { page },
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { page } = Astro.props;
|
|
15
|
+
const doc = await renderDoc(config, page);
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<DocsLayout page={page} headings={doc.headings}>
|
|
19
|
+
<Fragment set:html={doc.html} />
|
|
20
|
+
</DocsLayout>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import config from "virtual:reallysimpledocs/config";
|
|
2
|
+
import { getLlmDocs } from "../lib/docs.js";
|
|
3
|
+
import { getSite } from "../lib/site.js";
|
|
4
|
+
|
|
5
|
+
export function GET({ site }) {
|
|
6
|
+
const siteData = getSite(config);
|
|
7
|
+
const siteUrl = site?.toString().replace(/\/$/, "") || siteData.url || "";
|
|
8
|
+
const lines = [];
|
|
9
|
+
|
|
10
|
+
getLlmDocs(config).forEach((doc) => {
|
|
11
|
+
lines.push(`# ${doc.title}`, `Source: ${siteUrl}${doc.path}`, "");
|
|
12
|
+
lines.push(doc.content, "", "---", "");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return new Response(lines.join("\n"), {
|
|
16
|
+
headers: {
|
|
17
|
+
"content-type": "text/plain; charset=utf-8",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import config from "virtual:reallysimpledocs/config";
|
|
2
|
+
import { flattenMenuSlugs, getLlmDocs, getManifest } from "../lib/docs.js";
|
|
3
|
+
import { getSite } from "../lib/site.js";
|
|
4
|
+
|
|
5
|
+
export function GET({ site }) {
|
|
6
|
+
const manifest = getManifest(config);
|
|
7
|
+
const docs = getLlmDocs(config);
|
|
8
|
+
const bySlug = new Map(docs.map((doc) => [doc.slug, doc]));
|
|
9
|
+
const siteData = getSite(config);
|
|
10
|
+
const siteUrl = site?.toString().replace(/\/$/, "") || siteData.url || "";
|
|
11
|
+
const lines = [`# ${siteData.title}`, "", "## Docs", ""];
|
|
12
|
+
|
|
13
|
+
(manifest.menu || []).forEach((group) => {
|
|
14
|
+
if (group?.type !== "group") return;
|
|
15
|
+
lines.push(`### ${group.label || "Docs"}`, "");
|
|
16
|
+
|
|
17
|
+
const addSlug = (slug) => {
|
|
18
|
+
const doc = bySlug.get(slug);
|
|
19
|
+
if (!doc) return;
|
|
20
|
+
const url = `${siteUrl}${doc.path}`;
|
|
21
|
+
lines.push(`- [${doc.title}](${url})`);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
flattenMenuSlugs([group]).forEach(addSlug);
|
|
25
|
+
|
|
26
|
+
lines.push("");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return new Response(lines.join("\n"), {
|
|
30
|
+
headers: {
|
|
31
|
+
"content-type": "text/plain; charset=utf-8",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import config from "virtual:reallysimpledocs/config";
|
|
2
|
+
import { getMarkdownExport, getPages } from "../lib/docs.js";
|
|
3
|
+
|
|
4
|
+
export function getStaticPaths() {
|
|
5
|
+
return getPages(config).map((page) => ({
|
|
6
|
+
params: { slug: page.slug },
|
|
7
|
+
props: { page },
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function GET({ props }) {
|
|
12
|
+
return new Response(getMarkdownExport(config, props.page), {
|
|
13
|
+
headers: {
|
|
14
|
+
"content-type": "text/markdown; charset=utf-8",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|