radiant-docs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/index.js +312 -0
  2. package/package.json +38 -0
  3. package/template/.vscode/extensions.json +4 -0
  4. package/template/.vscode/launch.json +11 -0
  5. package/template/astro.config.mjs +216 -0
  6. package/template/ec.config.mjs +51 -0
  7. package/template/package-lock.json +12546 -0
  8. package/template/package.json +51 -0
  9. package/template/public/favicon.svg +9 -0
  10. package/template/src/assets/icons/check.svg +33 -0
  11. package/template/src/assets/icons/danger.svg +37 -0
  12. package/template/src/assets/icons/info.svg +36 -0
  13. package/template/src/assets/icons/lightbulb.svg +74 -0
  14. package/template/src/assets/icons/warning.svg +37 -0
  15. package/template/src/components/Header.astro +176 -0
  16. package/template/src/components/MdxPage.astro +49 -0
  17. package/template/src/components/OpenApiPage.astro +270 -0
  18. package/template/src/components/Search.astro +362 -0
  19. package/template/src/components/Sidebar.astro +19 -0
  20. package/template/src/components/SidebarDropdown.astro +149 -0
  21. package/template/src/components/SidebarGroup.astro +51 -0
  22. package/template/src/components/SidebarLink.astro +56 -0
  23. package/template/src/components/SidebarMenu.astro +46 -0
  24. package/template/src/components/SidebarSubgroup.astro +136 -0
  25. package/template/src/components/TableOfContents.astro +480 -0
  26. package/template/src/components/ThemeSwitcher.astro +84 -0
  27. package/template/src/components/endpoint/PlaygroundBar.astro +68 -0
  28. package/template/src/components/endpoint/PlaygroundButton.astro +44 -0
  29. package/template/src/components/endpoint/PlaygroundField.astro +54 -0
  30. package/template/src/components/endpoint/PlaygroundForm.astro +203 -0
  31. package/template/src/components/endpoint/RequestSnippets.astro +308 -0
  32. package/template/src/components/endpoint/ResponseDisplay.astro +177 -0
  33. package/template/src/components/endpoint/ResponseFields.astro +224 -0
  34. package/template/src/components/endpoint/ResponseSnippets.astro +247 -0
  35. package/template/src/components/sidebar/SidebarEndpointLink.astro +51 -0
  36. package/template/src/components/sidebar/SidebarOpenApi.astro +207 -0
  37. package/template/src/components/ui/Field.astro +69 -0
  38. package/template/src/components/ui/Tag.astro +5 -0
  39. package/template/src/components/ui/demo/CodeDemo.astro +15 -0
  40. package/template/src/components/ui/demo/Demo.astro +3 -0
  41. package/template/src/components/ui/demo/UiDisplay.astro +13 -0
  42. package/template/src/components/user/Accordian.astro +69 -0
  43. package/template/src/components/user/AccordianGroup.astro +13 -0
  44. package/template/src/components/user/Callout.astro +101 -0
  45. package/template/src/components/user/Step.astro +51 -0
  46. package/template/src/components/user/Steps.astro +9 -0
  47. package/template/src/components/user/Tab.astro +25 -0
  48. package/template/src/components/user/Tabs.astro +122 -0
  49. package/template/src/content.config.ts +11 -0
  50. package/template/src/entrypoint.ts +9 -0
  51. package/template/src/layouts/Layout.astro +92 -0
  52. package/template/src/lib/component-error.ts +163 -0
  53. package/template/src/lib/frontmatter-schema.ts +9 -0
  54. package/template/src/lib/oas.ts +24 -0
  55. package/template/src/lib/pagefind.ts +88 -0
  56. package/template/src/lib/routes.ts +316 -0
  57. package/template/src/lib/utils.ts +59 -0
  58. package/template/src/lib/validation.ts +1097 -0
  59. package/template/src/pages/[...slug].astro +77 -0
  60. package/template/src/styles/global.css +209 -0
  61. package/template/tsconfig.json +5 -0
@@ -0,0 +1,316 @@
1
+ import {
2
+ getConfig,
3
+ type NavGroup,
4
+ type NavPage,
5
+ type NavMenuItem,
6
+ type NavOpenApi,
7
+ loadOpenApiSpec,
8
+ } from "./validation";
9
+ import { deriveTitleFromEntryId, slugify } from "./utils";
10
+ import path from "node:path";
11
+ import { getCollection } from "astro:content";
12
+
13
+ // NOTE: You need to define NavPageItem to include strings, pages, and groups
14
+ // Since the input array is already validated, we can use the types from validation.ts
15
+ type NavPageItem = string | NavPage;
16
+
17
+ // Base route interface
18
+ export interface BaseRoute {
19
+ slug: string;
20
+ title: string;
21
+ }
22
+
23
+ // MDX route
24
+ export interface MdxRoute extends BaseRoute {
25
+ type: "mdx";
26
+ filePath: string; // e.g., "quickstart" or "writing-content/text"
27
+ }
28
+
29
+ // OpenAPI route
30
+ export interface OpenApiRoute extends BaseRoute {
31
+ type: "openapi";
32
+ filePath: string; // e.g., "open-api/open-api.json"
33
+ openApiPath: string; // e.g., "/burgers"
34
+ openApiMethod: string; // e.g., "get"
35
+ }
36
+
37
+ // Discriminated union
38
+ export type Route = MdxRoute | OpenApiRoute;
39
+
40
+ function processPageItem(
41
+ item: NavPageItem,
42
+ parentSlug: string = "",
43
+ docs: any[]
44
+ ): MdxRoute {
45
+ const filePath = typeof item === "string" ? item : item.page;
46
+ const filename = filePath.split("/").pop() || filePath;
47
+ const pageSlug = slugify(filename);
48
+ const fullSlug = parentSlug ? `${parentSlug}/${pageSlug}` : pageSlug;
49
+
50
+ // Find the entry matching this filePath
51
+ const entry = docs.find((doc: any) => {
52
+ const docPath = doc.id.replace(/\.mdx$/, "");
53
+ return docPath === filePath;
54
+ });
55
+
56
+ if (!entry) {
57
+ throw new Error(
58
+ `Could not find content collection entry for path: ${filePath}`
59
+ );
60
+ }
61
+
62
+ // Get title from frontmatter
63
+ const title = entry.data.title || deriveTitleFromEntryId(entry.filePath);
64
+
65
+ return {
66
+ type: "mdx",
67
+ slug: fullSlug,
68
+ filePath: filePath,
69
+ title: title,
70
+ };
71
+ }
72
+
73
+ function processGroup(
74
+ group: NavGroup,
75
+ parentSlug: string = "",
76
+ docs: any[]
77
+ ): Route[] {
78
+ let routes: Route[] = [];
79
+ const groupSlug = slugify(group.group);
80
+ const currentPrefix = parentSlug ? `${parentSlug}/${groupSlug}` : groupSlug;
81
+
82
+ group.pages.forEach((item) => {
83
+ // Check if it's a simple page or a page object
84
+ if (typeof item === "string" || "page" in item) {
85
+ routes.push(processPageItem(item as NavPageItem, currentPrefix, docs));
86
+ } else if ("group" in item) {
87
+ // Nested group
88
+ routes = routes.concat(
89
+ processGroup(item as NavGroup, currentPrefix, docs)
90
+ );
91
+ }
92
+ });
93
+
94
+ return routes;
95
+ }
96
+
97
+ // Helper function to parse endpoint string (same as in validation.ts)
98
+ function parseEndpointString(
99
+ endpointStr: string
100
+ ): { method: string; path: string } | null {
101
+ const trimmed = endpointStr.trim();
102
+ const parts = trimmed.split(/\s+/);
103
+
104
+ if (parts.length !== 2) {
105
+ return null;
106
+ }
107
+
108
+ const method = parts[0].toUpperCase();
109
+ let path = parts[1];
110
+
111
+ // Ensure path starts with /
112
+ if (!path.startsWith("/")) {
113
+ path = "/" + path;
114
+ }
115
+
116
+ // Normalize path to lowercase for comparison
117
+ const normalizedPath = path.toLowerCase();
118
+
119
+ return { method, path: normalizedPath };
120
+ }
121
+
122
+ // Helper function to check if an endpoint matches include/exclude filters
123
+ function shouldIncludeEndpoint(
124
+ method: string,
125
+ pathStr: string,
126
+ include?: string[],
127
+ exclude?: string[]
128
+ ): boolean {
129
+ // Normalize for comparison
130
+ const normalizedMethod = method.toUpperCase();
131
+ const normalizedPath = pathStr.toLowerCase();
132
+ const endpointKey = `${normalizedMethod} ${normalizedPath}`;
133
+
134
+ // If include is specified, only include matching endpoints
135
+ if (include) {
136
+ return include.some((entry) => {
137
+ const parsed = parseEndpointString(entry);
138
+ if (!parsed) return false;
139
+ return `${parsed.method} ${parsed.path}` === endpointKey;
140
+ });
141
+ }
142
+
143
+ // If exclude is specified, exclude matching endpoints
144
+ if (exclude) {
145
+ return !exclude.some((entry) => {
146
+ const parsed = parseEndpointString(entry);
147
+ if (!parsed) return false;
148
+ return `${parsed.method} ${parsed.path}` === endpointKey;
149
+ });
150
+ }
151
+
152
+ // If neither include nor exclude, include all endpoints
153
+ return true;
154
+ }
155
+
156
+ async function processOpenApiFile(
157
+ openApiPathOrConfig: string | NavOpenApi,
158
+ parentSlug: string = ""
159
+ ): Promise<OpenApiRoute[]> {
160
+ const routes: OpenApiRoute[] = [];
161
+ const CWD = process.cwd();
162
+ const DOCS_DIR = path.join(CWD, "src/content/docs");
163
+
164
+ // Extract file path and filter options
165
+ let openApiPath: string;
166
+ let include: string[] | undefined;
167
+ let exclude: string[] | undefined;
168
+
169
+ if (typeof openApiPathOrConfig === "string") {
170
+ openApiPath = openApiPathOrConfig;
171
+ } else {
172
+ openApiPath = openApiPathOrConfig.source;
173
+ include = openApiPathOrConfig.include;
174
+ exclude = openApiPathOrConfig.exclude;
175
+ }
176
+
177
+ // Load and parse the OpenAPI file using the shared function
178
+ let openApiDoc: any;
179
+ try {
180
+ openApiDoc = await loadOpenApiSpec(openApiPath);
181
+ } catch (error) {
182
+ // If loading fails (e.g., file doesn't exist or URL is invalid), return empty routes
183
+ return routes;
184
+ }
185
+
186
+ // Extract paths from the OpenAPI document
187
+ const paths = openApiDoc.paths || {};
188
+ const httpMethods = [
189
+ "get",
190
+ "post",
191
+ "put",
192
+ "delete",
193
+ "patch",
194
+ "head",
195
+ "options",
196
+ "trace",
197
+ ];
198
+
199
+ // Iterate through each path
200
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
201
+ if (!pathItem || typeof pathItem !== "object") continue;
202
+
203
+ // Iterate through each HTTP method in the path
204
+ for (const method of httpMethods) {
205
+ const operation = (pathItem as any)[method];
206
+ if (!operation) continue;
207
+
208
+ // Check if this endpoint should be included based on filters
209
+ if (!shouldIncludeEndpoint(method, pathStr, include, exclude)) {
210
+ continue;
211
+ }
212
+
213
+ // Generate slug from path and method (guaranteed to be unique)
214
+ // e.g., "/burgers" + "get" -> "burgers-get"
215
+ const pathSlug = pathStr
216
+ .replace(/^\//, "") // Remove leading slash
217
+ .replace(/\//g, "-") // Replace slashes with hyphens
218
+ .replace(/\{[^}]+\}/g, "param"); // Replace path params like {id} with "param"
219
+
220
+ const methodSlug = slugify(method);
221
+ const endpointSlug = pathSlug ? `${pathSlug}-${methodSlug}` : methodSlug;
222
+ const fullSlug = parentSlug
223
+ ? `${parentSlug}/${endpointSlug}`
224
+ : endpointSlug;
225
+
226
+ // Use summary for title (more user-friendly), fallback to path + method
227
+ const title = operation.summary || `${method.toUpperCase()} ${pathStr}`;
228
+
229
+ routes.push({
230
+ type: "openapi",
231
+ slug: fullSlug,
232
+ filePath: openApiPath,
233
+ title: title,
234
+ openApiPath: pathStr,
235
+ openApiMethod: method,
236
+ });
237
+ }
238
+ }
239
+
240
+ return routes;
241
+ }
242
+
243
+ async function processMenuItem(
244
+ menuItem: NavMenuItem,
245
+ parentSlug: string = "",
246
+ docs: any[]
247
+ ): Promise<Route[]> {
248
+ let routes: Route[] = [];
249
+ const menuItemSlug = slugify(menuItem.label);
250
+ const currentPrefix = parentSlug
251
+ ? `${parentSlug}/${menuItemSlug}`
252
+ : menuItemSlug;
253
+
254
+ const submenu = menuItem.submenu;
255
+
256
+ // Process pages if they exist
257
+ if (submenu.pages) {
258
+ submenu.pages.forEach((page) => {
259
+ routes.push(processPageItem(page, currentPrefix, docs));
260
+ });
261
+ }
262
+
263
+ // Process groups if they exist
264
+ if (submenu.groups) {
265
+ submenu.groups.forEach((group) => {
266
+ routes = routes.concat(processGroup(group, currentPrefix, docs));
267
+ });
268
+ }
269
+
270
+ // Process OpenAPI file if it exists
271
+ if (submenu.openapi) {
272
+ const openApiRoutes = await processOpenApiFile(
273
+ submenu.openapi,
274
+ currentPrefix
275
+ );
276
+ routes = routes.concat(openApiRoutes);
277
+ }
278
+
279
+ return routes;
280
+ }
281
+
282
+ export async function getAllRoutes(): Promise<Route[]> {
283
+ // 1. Get the validated config directly
284
+ const config = await getConfig();
285
+ const navigation = config.navigation;
286
+
287
+ // 2. Load the docs collection to access frontmatter
288
+ const docs = await getCollection("docs");
289
+
290
+ let allRoutes: Route[] = [];
291
+
292
+ // 3. Identify the active navigation container (pages, groups, or menu)
293
+ // We use the XOR guarantee from validateNavigation (only one key exists)
294
+ if (navigation.pages) {
295
+ // Case 1: Simple pages array at the top level
296
+ navigation.pages.forEach((item) => {
297
+ // items are guaranteed to be string | NavPage
298
+ allRoutes.push(processPageItem(item, "", docs));
299
+ });
300
+ } else if (navigation.groups) {
301
+ // Case 2: Groups array at the top level
302
+ navigation.groups.forEach((group) => {
303
+ // items are guaranteed to be NavGroup
304
+ allRoutes = allRoutes.concat(processGroup(group, "", docs));
305
+ });
306
+ } else if (navigation.menu) {
307
+ // Case 3: Menu object at the top level
308
+ // Need to await all menu items since processMenuItem is now async
309
+ for (const menuItem of navigation.menu.items) {
310
+ const menuItemRoutes = await processMenuItem(menuItem, "", docs);
311
+ allRoutes = allRoutes.concat(menuItemRoutes);
312
+ }
313
+ }
314
+
315
+ return allRoutes;
316
+ }
@@ -0,0 +1,59 @@
1
+ import { unified } from "unified";
2
+ import remarkParse from "remark-parse";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkRehype from "remark-rehype";
5
+ import rehypeStringify from "rehype-stringify";
6
+ import path from "node:path";
7
+
8
+ export function slugify(text: string): string {
9
+ if (typeof text !== "string") {
10
+ return "";
11
+ }
12
+ return text
13
+ .toLowerCase()
14
+ .replace(/\s+/g, "-")
15
+ .replace(/[^\w\-]+/g, "")
16
+ .replace(/\-\-+/g, "-")
17
+ .replace(/^-+/, "")
18
+ .replace(/-+$/, "");
19
+ }
20
+
21
+ export async function renderMarkdown(markdown: string): Promise<string> {
22
+ if (!markdown) return "";
23
+
24
+ const result = await unified()
25
+ .use(remarkParse)
26
+ .use(remarkGfm) // GitHub Flavored Markdown (for lists, etc.)
27
+ .use(remarkRehype, { allowDangerousHtml: false })
28
+ .use(rehypeStringify)
29
+ .process(markdown);
30
+
31
+ return result.toString();
32
+ }
33
+
34
+ export const methodColors: Record<string, string> = {
35
+ get: "bg-blue-50 dark:bg-blue-900 text-blue-700/70 dark:text-blue-200 border-blue-700/10",
36
+ post: "bg-green-50 dark:bg-green-900 text-green-700/70 dark:text-green-200 border-green-700/10",
37
+ put: "bg-orange-50 dark:bg-orange-900 text-orange-700/70 dark:text-orange-700/10 border-orange-700/10",
38
+ patch:
39
+ "bg-yellow-50 dark:bg-yellow-900 text-yellow-700/70 dark:text-yellow-700/10 border-yellow-700/10",
40
+ delete:
41
+ "bg-red-50 dark:bg-red-900 text-red-700/70 dark:text-red-700/10 border-red-700/10",
42
+ };
43
+
44
+ export function deriveTitleFromEntryId(filePath: string): string {
45
+ const filename = path.basename(filePath);
46
+ const raw = filename.replace(/\.(md|mdx)$/i, "");
47
+
48
+ // Capitalize and replace dashes
49
+ return (
50
+ raw
51
+ .split(/[-_]/)
52
+ .map((segment) => {
53
+ if (!segment) return segment;
54
+ return segment.charAt(0).toUpperCase() + segment.slice(1);
55
+ })
56
+ .join(" ")
57
+ .trim() || "Untitled"
58
+ );
59
+ }