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.
- package/dist/index.js +312 -0
- package/package.json +38 -0
- package/template/.vscode/extensions.json +4 -0
- package/template/.vscode/launch.json +11 -0
- package/template/astro.config.mjs +216 -0
- package/template/ec.config.mjs +51 -0
- package/template/package-lock.json +12546 -0
- package/template/package.json +51 -0
- package/template/public/favicon.svg +9 -0
- package/template/src/assets/icons/check.svg +33 -0
- package/template/src/assets/icons/danger.svg +37 -0
- package/template/src/assets/icons/info.svg +36 -0
- package/template/src/assets/icons/lightbulb.svg +74 -0
- package/template/src/assets/icons/warning.svg +37 -0
- package/template/src/components/Header.astro +176 -0
- package/template/src/components/MdxPage.astro +49 -0
- package/template/src/components/OpenApiPage.astro +270 -0
- package/template/src/components/Search.astro +362 -0
- package/template/src/components/Sidebar.astro +19 -0
- package/template/src/components/SidebarDropdown.astro +149 -0
- package/template/src/components/SidebarGroup.astro +51 -0
- package/template/src/components/SidebarLink.astro +56 -0
- package/template/src/components/SidebarMenu.astro +46 -0
- package/template/src/components/SidebarSubgroup.astro +136 -0
- package/template/src/components/TableOfContents.astro +480 -0
- package/template/src/components/ThemeSwitcher.astro +84 -0
- package/template/src/components/endpoint/PlaygroundBar.astro +68 -0
- package/template/src/components/endpoint/PlaygroundButton.astro +44 -0
- package/template/src/components/endpoint/PlaygroundField.astro +54 -0
- package/template/src/components/endpoint/PlaygroundForm.astro +203 -0
- package/template/src/components/endpoint/RequestSnippets.astro +308 -0
- package/template/src/components/endpoint/ResponseDisplay.astro +177 -0
- package/template/src/components/endpoint/ResponseFields.astro +224 -0
- package/template/src/components/endpoint/ResponseSnippets.astro +247 -0
- package/template/src/components/sidebar/SidebarEndpointLink.astro +51 -0
- package/template/src/components/sidebar/SidebarOpenApi.astro +207 -0
- package/template/src/components/ui/Field.astro +69 -0
- package/template/src/components/ui/Tag.astro +5 -0
- package/template/src/components/ui/demo/CodeDemo.astro +15 -0
- package/template/src/components/ui/demo/Demo.astro +3 -0
- package/template/src/components/ui/demo/UiDisplay.astro +13 -0
- package/template/src/components/user/Accordian.astro +69 -0
- package/template/src/components/user/AccordianGroup.astro +13 -0
- package/template/src/components/user/Callout.astro +101 -0
- package/template/src/components/user/Step.astro +51 -0
- package/template/src/components/user/Steps.astro +9 -0
- package/template/src/components/user/Tab.astro +25 -0
- package/template/src/components/user/Tabs.astro +122 -0
- package/template/src/content.config.ts +11 -0
- package/template/src/entrypoint.ts +9 -0
- package/template/src/layouts/Layout.astro +92 -0
- package/template/src/lib/component-error.ts +163 -0
- package/template/src/lib/frontmatter-schema.ts +9 -0
- package/template/src/lib/oas.ts +24 -0
- package/template/src/lib/pagefind.ts +88 -0
- package/template/src/lib/routes.ts +316 -0
- package/template/src/lib/utils.ts +59 -0
- package/template/src/lib/validation.ts +1097 -0
- package/template/src/pages/[...slug].astro +77 -0
- package/template/src/styles/global.css +209 -0
- 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
|
+
}
|