radiant-docs 0.1.6 → 0.1.8
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 +32 -6
- package/package.json +3 -3
- package/template/astro.config.mjs +76 -3
- package/template/package-lock.json +924 -737
- package/template/package.json +7 -5
- package/template/scripts/generate-og-images.mjs +335 -0
- package/template/scripts/generate-og-metadata.mjs +173 -0
- package/template/scripts/rewrite-static-asset-host.mjs +408 -0
- package/template/scripts/stamp-image-versions.mjs +277 -0
- package/template/scripts/stamp-og-image-versions.mjs +199 -0
- package/template/scripts/stamp-pagefind-runtime-version.mjs +140 -0
- package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
- package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
- package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
- package/template/src/components/Footer.astro +94 -0
- package/template/src/components/Header.astro +11 -66
- package/template/src/components/LogoLink.astro +103 -0
- package/template/src/components/MdxPage.astro +126 -11
- package/template/src/components/OpenApiPage.astro +1036 -69
- package/template/src/components/Search.astro +0 -2
- package/template/src/components/SidebarDropdown.astro +34 -14
- package/template/src/components/SidebarGroup.astro +3 -6
- package/template/src/components/SidebarLink.astro +22 -12
- package/template/src/components/SidebarMenu.astro +19 -16
- package/template/src/components/SidebarSegmented.astro +99 -0
- package/template/src/components/SidebarSubgroup.astro +12 -12
- package/template/src/components/ThemeSwitcher.astro +30 -7
- package/template/src/components/endpoint/PlaygroundBar.astro +32 -36
- package/template/src/components/endpoint/PlaygroundButton.astro +40 -4
- package/template/src/components/endpoint/PlaygroundField.astro +1068 -22
- package/template/src/components/endpoint/PlaygroundForm.astro +559 -61
- package/template/src/components/endpoint/RequestSnippets.astro +342 -193
- package/template/src/components/endpoint/ResponseDisplay.astro +161 -147
- package/template/src/components/endpoint/ResponseFieldTree.astro +134 -0
- package/template/src/components/endpoint/ResponseFields.astro +711 -68
- package/template/src/components/endpoint/ResponseSnippets.astro +299 -173
- package/template/src/components/sidebar/SidebarEndpointLink.astro +1 -1
- package/template/src/components/ui/CodeLanguageIcon.astro +19 -0
- package/template/src/components/ui/CodeTabEdge.astro +79 -0
- package/template/src/components/ui/Field.astro +103 -20
- package/template/src/components/ui/Icon.astro +32 -0
- package/template/src/components/ui/ListChevronsToggle.astro +31 -0
- package/template/src/components/ui/Tag.astro +1 -1
- package/template/src/components/user/{Accordian.astro → Accordion.astro} +6 -6
- package/template/src/components/user/Callout.astro +5 -9
- package/template/src/components/user/CodeBlock.astro +400 -0
- package/template/src/components/user/CodeGroup.astro +225 -0
- package/template/src/components/user/ComponentPreview.astro +1 -0
- package/template/src/components/user/ComponentPreviewBlock.astro +181 -0
- package/template/src/components/user/Image.astro +132 -0
- package/template/src/components/user/Steps.astro +1 -3
- package/template/src/components/user/Tabs.astro +2 -2
- package/template/src/content.config.ts +1 -0
- package/template/src/layouts/Layout.astro +109 -8
- package/template/src/lib/code/code-block.ts +546 -0
- package/template/src/lib/frontmatter-schema.ts +8 -7
- package/template/src/lib/mdx/remark-code-block-component.ts +342 -0
- package/template/src/lib/mdx/remark-demote-h1.ts +16 -0
- package/template/src/lib/pagefind.ts +19 -5
- package/template/src/lib/routes.ts +49 -31
- package/template/src/lib/utils.ts +20 -0
- package/template/src/lib/validation.ts +638 -200
- package/template/src/pages/[...slug].astro +18 -5
- package/template/src/styles/geist-mono.css +33 -0
- package/template/src/styles/global.css +89 -84
- package/template/src/styles/google-sans-flex.css +143 -0
- package/template/ec.config.mjs +0 -51
- /package/template/src/components/user/{AccordianGroup.astro → AccordionGroup.astro} +0 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
3
|
+
import { mdxFromMarkdown } from "mdast-util-mdx";
|
|
4
|
+
import { mdxjs } from "micromark-extension-mdxjs";
|
|
5
|
+
import type { Plugin } from "unified";
|
|
6
|
+
import { visitParents } from "unist-util-visit-parents";
|
|
7
|
+
|
|
8
|
+
type CodeNode = {
|
|
9
|
+
type: "code";
|
|
10
|
+
lang?: string | null;
|
|
11
|
+
meta?: string | null;
|
|
12
|
+
value?: string | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ParentNode = {
|
|
16
|
+
children?: unknown[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ParsedCodeMeta = {
|
|
20
|
+
filename: string;
|
|
21
|
+
showLineNumbers: boolean;
|
|
22
|
+
hideLanguageIcon: boolean;
|
|
23
|
+
highlightedLines: string;
|
|
24
|
+
collapsedLines: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type MdxJsxAttributeNode = {
|
|
28
|
+
type: "mdxJsxAttribute";
|
|
29
|
+
name: string;
|
|
30
|
+
value: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type MdxJsxFlowElementNode = {
|
|
34
|
+
type: "mdxJsxFlowElement";
|
|
35
|
+
name: string;
|
|
36
|
+
attributes: MdxJsxAttributeNode[];
|
|
37
|
+
children: unknown[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ParagraphNode = {
|
|
41
|
+
type: "paragraph";
|
|
42
|
+
children?: unknown[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type MdxJsxTextElementNode = {
|
|
46
|
+
type: "mdxJsxTextElement";
|
|
47
|
+
name?: string | null;
|
|
48
|
+
attributes?: unknown[];
|
|
49
|
+
children?: unknown[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const COMPONENT_PREVIEW_NAME = "ComponentPreview";
|
|
53
|
+
const COMPONENT_PREVIEW_BLOCK_NAME = "ComponentPreviewBlock";
|
|
54
|
+
const COMPONENT_PREVIEW_LANGUAGES = new Set(["jsx", "tsx", "mdx"]);
|
|
55
|
+
const INTERNAL_CODE_BLOCK_NAME = "CodeBlockInternal";
|
|
56
|
+
|
|
57
|
+
const FILE_EXTENSION_BY_LANGUAGE: Record<string, string> = {
|
|
58
|
+
bash: "sh",
|
|
59
|
+
shell: "sh",
|
|
60
|
+
c: "c",
|
|
61
|
+
cpp: "cpp",
|
|
62
|
+
csharp: "cs",
|
|
63
|
+
css: "css",
|
|
64
|
+
diff: "diff",
|
|
65
|
+
go: "go",
|
|
66
|
+
graphql: "graphql",
|
|
67
|
+
html: "html",
|
|
68
|
+
java: "java",
|
|
69
|
+
javascript: "js",
|
|
70
|
+
jsx: "jsx",
|
|
71
|
+
json: "json",
|
|
72
|
+
kotlin: "kt",
|
|
73
|
+
markdown: "md",
|
|
74
|
+
php: "php",
|
|
75
|
+
plaintext: "txt",
|
|
76
|
+
python: "py",
|
|
77
|
+
ruby: "rb",
|
|
78
|
+
rust: "rs",
|
|
79
|
+
sql: "sql",
|
|
80
|
+
swift: "swift",
|
|
81
|
+
toml: "toml",
|
|
82
|
+
typescript: "ts",
|
|
83
|
+
tsx: "tsx",
|
|
84
|
+
xml: "xml",
|
|
85
|
+
yaml: "yml",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function readMetaString(meta: string | null | undefined): string {
|
|
89
|
+
return typeof meta === "string" ? meta.trim() : "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseMetaValue(meta: string, key: string): string {
|
|
93
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
|
+
const regex = new RegExp(
|
|
95
|
+
`(?:^|\\s)${escapedKey}=(?:\\{([^}]*)\\}|"([^"]*)"|'([^']*)'|\\\`([^\\\`]*)\\\`|([^\\s]+))`,
|
|
96
|
+
);
|
|
97
|
+
const match = meta.match(regex);
|
|
98
|
+
return (
|
|
99
|
+
match?.[1] ??
|
|
100
|
+
match?.[2] ??
|
|
101
|
+
match?.[3] ??
|
|
102
|
+
match?.[4] ??
|
|
103
|
+
match?.[5] ??
|
|
104
|
+
""
|
|
105
|
+
).trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseBooleanFlag(meta: string, key: string): boolean {
|
|
109
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
110
|
+
const bareFlagRegex = new RegExp(`(?:^|\\s)${escapedKey}(?:\\s|$)`);
|
|
111
|
+
if (bareFlagRegex.test(meta)) return true;
|
|
112
|
+
|
|
113
|
+
const explicitTrueRegex = new RegExp(
|
|
114
|
+
`(?:^|\\s)${escapedKey}=\\{?["']?true["']?\\}?`,
|
|
115
|
+
"i",
|
|
116
|
+
);
|
|
117
|
+
return explicitTrueRegex.test(meta);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseHighlightedLines(meta: string): string {
|
|
121
|
+
const match = meta.match(/(?:^|\s)\{([^}]+)\}/);
|
|
122
|
+
return (match?.[1] ?? "").trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseParsedCodeMeta(meta: string): ParsedCodeMeta {
|
|
126
|
+
const filename = parseMetaValue(meta, "title");
|
|
127
|
+
const showLineNumbers = parseBooleanFlag(meta, "showLineNumbers");
|
|
128
|
+
const collapsedLines = parseMetaValue(meta, "collapse");
|
|
129
|
+
const highlightedLines = parseHighlightedLines(meta);
|
|
130
|
+
const hideLanguageIcon = parseBooleanFlag(meta, "hideLanguageIcon");
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
filename,
|
|
134
|
+
showLineNumbers,
|
|
135
|
+
hideLanguageIcon,
|
|
136
|
+
highlightedLines,
|
|
137
|
+
collapsedLines,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createAttribute(name: string, value: string): MdxJsxAttributeNode {
|
|
142
|
+
return {
|
|
143
|
+
type: "mdxJsxAttribute",
|
|
144
|
+
name,
|
|
145
|
+
value,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseComponentPreviewChildren(rawCode: string): unknown[] {
|
|
150
|
+
const parsedTree = fromMarkdown(rawCode, {
|
|
151
|
+
extensions: [mdxjs()],
|
|
152
|
+
mdastExtensions: [mdxFromMarkdown()],
|
|
153
|
+
}) as Root;
|
|
154
|
+
|
|
155
|
+
const children = Array.isArray(parsedTree.children) ? parsedTree.children : [];
|
|
156
|
+
|
|
157
|
+
// Promote a single inline MDX JSX node into flow-level JSX so the preview
|
|
158
|
+
// renders block components (e.g. <Callout />) as expected.
|
|
159
|
+
return children.map((node) => {
|
|
160
|
+
const paragraphNode = node as ParagraphNode;
|
|
161
|
+
if (
|
|
162
|
+
paragraphNode.type !== "paragraph" ||
|
|
163
|
+
!Array.isArray(paragraphNode.children) ||
|
|
164
|
+
paragraphNode.children.length !== 1
|
|
165
|
+
) {
|
|
166
|
+
return node;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const onlyChild = paragraphNode.children[0] as MdxJsxTextElementNode;
|
|
170
|
+
if (
|
|
171
|
+
onlyChild.type !== "mdxJsxTextElement" ||
|
|
172
|
+
typeof onlyChild.name !== "string" ||
|
|
173
|
+
onlyChild.name.trim().length === 0
|
|
174
|
+
) {
|
|
175
|
+
return node;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
type: "mdxJsxFlowElement",
|
|
180
|
+
name: onlyChild.name,
|
|
181
|
+
attributes: Array.isArray(onlyChild.attributes)
|
|
182
|
+
? onlyChild.attributes
|
|
183
|
+
: [],
|
|
184
|
+
children: Array.isArray(onlyChild.children) ? onlyChild.children : [],
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getNearestMdxJsxFlowElementName(
|
|
190
|
+
ancestors: unknown[],
|
|
191
|
+
): string | null {
|
|
192
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
193
|
+
const ancestor = ancestors[index] as {
|
|
194
|
+
type?: string;
|
|
195
|
+
name?: string | null;
|
|
196
|
+
};
|
|
197
|
+
if (ancestor?.type !== "mdxJsxFlowElement") continue;
|
|
198
|
+
return typeof ancestor.name === "string" ? ancestor.name : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isInsideMdxJsxTextElement(ancestors: unknown[]): boolean {
|
|
205
|
+
return ancestors.some((ancestor) => {
|
|
206
|
+
const node = ancestor as { type?: string };
|
|
207
|
+
return node?.type === "mdxJsxTextElement";
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildDefaultCodeGroupFileName(
|
|
212
|
+
language: string,
|
|
213
|
+
codeIndexInGroup: number,
|
|
214
|
+
): string {
|
|
215
|
+
const normalizedLanguage = language.trim().toLowerCase() || "plaintext";
|
|
216
|
+
const extension = FILE_EXTENSION_BY_LANGUAGE[normalizedLanguage] ?? "txt";
|
|
217
|
+
return `file-name-${codeIndexInGroup + 1}.${extension}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const remarkCodeBlockComponent: Plugin<[], Root> = () => {
|
|
221
|
+
return (tree) => {
|
|
222
|
+
visitParents(tree, "code", (node, ancestors) => {
|
|
223
|
+
const codeNode = node as CodeNode;
|
|
224
|
+
const parent = ancestors[ancestors.length - 1] as ParentNode | undefined;
|
|
225
|
+
const siblings = parent?.children;
|
|
226
|
+
if (!siblings) return;
|
|
227
|
+
|
|
228
|
+
const currentIndex = siblings.indexOf(node);
|
|
229
|
+
if (currentIndex < 0) return;
|
|
230
|
+
|
|
231
|
+
const nearestMdxFlowElementName = getNearestMdxJsxFlowElementName(
|
|
232
|
+
ancestors,
|
|
233
|
+
);
|
|
234
|
+
const isInsideCodeGroup = nearestMdxFlowElementName === "CodeGroup";
|
|
235
|
+
const isInsideComponentPreview =
|
|
236
|
+
nearestMdxFlowElementName === COMPONENT_PREVIEW_NAME;
|
|
237
|
+
const isInsideInlineMdx = isInsideMdxJsxTextElement(ancestors);
|
|
238
|
+
if (isInsideInlineMdx) return;
|
|
239
|
+
|
|
240
|
+
const meta = readMetaString(codeNode.meta);
|
|
241
|
+
const parsedMeta = parseParsedCodeMeta(meta);
|
|
242
|
+
const language =
|
|
243
|
+
typeof codeNode.lang === "string" && codeNode.lang.trim().length > 0
|
|
244
|
+
? codeNode.lang.trim()
|
|
245
|
+
: "plaintext";
|
|
246
|
+
const normalizedLanguage = language.trim().toLowerCase();
|
|
247
|
+
const rawCode = typeof codeNode.value === "string" ? codeNode.value : "";
|
|
248
|
+
const codeIndexInGroup = isInsideCodeGroup
|
|
249
|
+
? siblings
|
|
250
|
+
.slice(0, currentIndex)
|
|
251
|
+
.filter(
|
|
252
|
+
(sibling) => (sibling as { type?: string }).type === "code",
|
|
253
|
+
).length
|
|
254
|
+
: 0;
|
|
255
|
+
const fileName =
|
|
256
|
+
parsedMeta.filename.length > 0
|
|
257
|
+
? parsedMeta.filename
|
|
258
|
+
: isInsideCodeGroup
|
|
259
|
+
? buildDefaultCodeGroupFileName(language, codeIndexInGroup)
|
|
260
|
+
: "";
|
|
261
|
+
const showFilename = isInsideCodeGroup || fileName.length > 0;
|
|
262
|
+
|
|
263
|
+
const attributes: MdxJsxAttributeNode[] = [
|
|
264
|
+
createAttribute("language", language),
|
|
265
|
+
createAttribute("raw", rawCode),
|
|
266
|
+
createAttribute("showFilename", showFilename ? "true" : "false"),
|
|
267
|
+
createAttribute(
|
|
268
|
+
"showLineNumbers",
|
|
269
|
+
parsedMeta.showLineNumbers ? "true" : "false",
|
|
270
|
+
),
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
if (parsedMeta.hideLanguageIcon) {
|
|
274
|
+
attributes.push(createAttribute("hideLanguageIcon", "true"));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (showFilename) {
|
|
278
|
+
attributes.push(createAttribute("filename", fileName));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (isInsideCodeGroup) {
|
|
282
|
+
attributes.push(createAttribute("inCodeGroup", "true"));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (parsedMeta.highlightedLines.length > 0) {
|
|
286
|
+
attributes.push(
|
|
287
|
+
createAttribute("highlightedLines", parsedMeta.highlightedLines),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (parsedMeta.collapsedLines.length > 0) {
|
|
292
|
+
attributes.push(
|
|
293
|
+
createAttribute("collapsedLines", parsedMeta.collapsedLines),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (isInsideComponentPreview) {
|
|
298
|
+
if (!COMPONENT_PREVIEW_LANGUAGES.has(normalizedLanguage)) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`[USER_ERROR]: <${COMPONENT_PREVIEW_NAME}>: Fenced code blocks must use jsx, tsx, or mdx language (received "${language}")`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let previewChildren: unknown[] = [];
|
|
305
|
+
try {
|
|
306
|
+
previewChildren = parseComponentPreviewChildren(rawCode);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const reason =
|
|
309
|
+
error instanceof Error && error.message.trim().length > 0
|
|
310
|
+
? ` -> ${error.message}`
|
|
311
|
+
: "";
|
|
312
|
+
throw new Error(
|
|
313
|
+
`[USER_ERROR]: <${COMPONENT_PREVIEW_NAME}>: Failed to parse fenced code block as MDX content${reason}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const previewAttributes = attributes.filter(
|
|
318
|
+
(attribute) => attribute.name !== "inCodeGroup",
|
|
319
|
+
);
|
|
320
|
+
const previewNode: MdxJsxFlowElementNode = {
|
|
321
|
+
type: "mdxJsxFlowElement",
|
|
322
|
+
name: COMPONENT_PREVIEW_BLOCK_NAME,
|
|
323
|
+
attributes: previewAttributes,
|
|
324
|
+
children: previewChildren,
|
|
325
|
+
};
|
|
326
|
+
siblings[currentIndex] = previewNode;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const replacementNode: MdxJsxFlowElementNode = {
|
|
331
|
+
type: "mdxJsxFlowElement",
|
|
332
|
+
name: INTERNAL_CODE_BLOCK_NAME,
|
|
333
|
+
attributes,
|
|
334
|
+
children: [],
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
siblings[currentIndex] = replacementNode;
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export default remarkCodeBlockComponent;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Heading, Root } from "mdast";
|
|
2
|
+
import type { Plugin } from "unified";
|
|
3
|
+
import { visitParents } from "unist-util-visit-parents";
|
|
4
|
+
|
|
5
|
+
export const remarkDemoteH1: Plugin<[], Root> = () => {
|
|
6
|
+
return (tree) => {
|
|
7
|
+
visitParents(tree, "heading", (node) => {
|
|
8
|
+
const heading = node as Heading;
|
|
9
|
+
if (heading.depth === 1) {
|
|
10
|
+
heading.depth = 2;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default remarkDemoteH1;
|
|
@@ -37,6 +37,7 @@ export interface PagefindSearchResponse {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface PagefindInstance {
|
|
40
|
+
options?: (options: { baseUrl?: string; basePath?: string }) => Promise<void>;
|
|
40
41
|
init: () => Promise<void>;
|
|
41
42
|
search: (
|
|
42
43
|
query: string,
|
|
@@ -48,17 +49,30 @@ export interface PagefindInstance {
|
|
|
48
49
|
|
|
49
50
|
let pagefindInstance: PagefindInstance | null = null;
|
|
50
51
|
|
|
52
|
+
function getPagefindScriptUrl(): string {
|
|
53
|
+
return "/pagefind/pagefind.js";
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
export async function getPagefind(): Promise<PagefindInstance | null> {
|
|
52
57
|
if (pagefindInstance) return pagefindInstance;
|
|
53
58
|
|
|
54
59
|
try {
|
|
55
|
-
// Completely bypass Vite's module resolution by constructing the import dynamically
|
|
56
|
-
// This
|
|
60
|
+
// Completely bypass Vite's module resolution by constructing the import dynamically.
|
|
61
|
+
// This keeps Pagefind loading purely runtime and allows versioned query params.
|
|
57
62
|
const importPagefind = new Function(
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
"moduleUrl",
|
|
64
|
+
"return import(moduleUrl)"
|
|
65
|
+
) as (moduleUrl: string) => Promise<PagefindInstance>;
|
|
60
66
|
|
|
61
|
-
const pagefind = await importPagefind();
|
|
67
|
+
const pagefind = await importPagefind(getPagefindScriptUrl());
|
|
68
|
+
// Pagefind uses the script import location to derive result URL base.
|
|
69
|
+
// Since we import from the static host, force result links back to the
|
|
70
|
+
// current docs site origin while leaving asset fetches on static host.
|
|
71
|
+
if (typeof pagefind.options === "function") {
|
|
72
|
+
const baseUrl =
|
|
73
|
+
typeof window !== "undefined" ? window.location.origin : "/";
|
|
74
|
+
await pagefind.options({ baseUrl });
|
|
75
|
+
}
|
|
62
76
|
await pagefind.init();
|
|
63
77
|
pagefindInstance = pagefind;
|
|
64
78
|
return pagefindInstance;
|
|
@@ -37,10 +37,27 @@ export interface OpenApiRoute extends BaseRoute {
|
|
|
37
37
|
// Discriminated union
|
|
38
38
|
export type Route = MdxRoute | OpenApiRoute;
|
|
39
39
|
|
|
40
|
+
function normalizeTitle(value: unknown): string | undefined {
|
|
41
|
+
if (typeof value !== "string") return undefined;
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
return trimmed.length ? trimmed : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveMdxPageTitle(args: {
|
|
47
|
+
entry: any;
|
|
48
|
+
docsTitle?: unknown;
|
|
49
|
+
filePath: string;
|
|
50
|
+
}): string {
|
|
51
|
+
const frontmatterTitle = normalizeTitle(args.entry?.data?.title);
|
|
52
|
+
const docsJsonTitle = normalizeTitle(args.docsTitle);
|
|
53
|
+
|
|
54
|
+
return frontmatterTitle || docsJsonTitle || deriveTitleFromEntryId(args.filePath);
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
function processPageItem(
|
|
41
58
|
item: NavPageItem,
|
|
42
59
|
parentSlug: string = "",
|
|
43
|
-
docs: any[]
|
|
60
|
+
docs: any[],
|
|
44
61
|
): MdxRoute {
|
|
45
62
|
const filePath = typeof item === "string" ? item : item.page;
|
|
46
63
|
const filename = filePath.split("/").pop() || filePath;
|
|
@@ -55,12 +72,17 @@ function processPageItem(
|
|
|
55
72
|
|
|
56
73
|
if (!entry) {
|
|
57
74
|
throw new Error(
|
|
58
|
-
`Could not find content collection entry for path: ${filePath}
|
|
75
|
+
`Could not find content collection entry for path: ${filePath}`,
|
|
59
76
|
);
|
|
60
77
|
}
|
|
61
78
|
|
|
62
|
-
// Get title from
|
|
63
|
-
const
|
|
79
|
+
// Get title from docs.json NavPage object first, then fallback to filename
|
|
80
|
+
const configTitle = typeof item === "object" ? item.title : undefined;
|
|
81
|
+
const title = resolveMdxPageTitle({
|
|
82
|
+
entry,
|
|
83
|
+
docsTitle: configTitle,
|
|
84
|
+
filePath,
|
|
85
|
+
});
|
|
64
86
|
|
|
65
87
|
return {
|
|
66
88
|
type: "mdx",
|
|
@@ -73,7 +95,7 @@ function processPageItem(
|
|
|
73
95
|
function processGroup(
|
|
74
96
|
group: NavGroup,
|
|
75
97
|
parentSlug: string = "",
|
|
76
|
-
docs: any[]
|
|
98
|
+
docs: any[],
|
|
77
99
|
): Route[] {
|
|
78
100
|
let routes: Route[] = [];
|
|
79
101
|
const groupSlug = slugify(group.group);
|
|
@@ -86,7 +108,7 @@ function processGroup(
|
|
|
86
108
|
} else if ("group" in item) {
|
|
87
109
|
// Nested group
|
|
88
110
|
routes = routes.concat(
|
|
89
|
-
processGroup(item as NavGroup, currentPrefix, docs)
|
|
111
|
+
processGroup(item as NavGroup, currentPrefix, docs),
|
|
90
112
|
);
|
|
91
113
|
}
|
|
92
114
|
});
|
|
@@ -96,7 +118,7 @@ function processGroup(
|
|
|
96
118
|
|
|
97
119
|
// Helper function to parse endpoint string (same as in validation.ts)
|
|
98
120
|
function parseEndpointString(
|
|
99
|
-
endpointStr: string
|
|
121
|
+
endpointStr: string,
|
|
100
122
|
): { method: string; path: string } | null {
|
|
101
123
|
const trimmed = endpointStr.trim();
|
|
102
124
|
const parts = trimmed.split(/\s+/);
|
|
@@ -124,7 +146,7 @@ function shouldIncludeEndpoint(
|
|
|
124
146
|
method: string,
|
|
125
147
|
pathStr: string,
|
|
126
148
|
include?: string[],
|
|
127
|
-
exclude?: string[]
|
|
149
|
+
exclude?: string[],
|
|
128
150
|
): boolean {
|
|
129
151
|
// Normalize for comparison
|
|
130
152
|
const normalizedMethod = method.toUpperCase();
|
|
@@ -155,7 +177,7 @@ function shouldIncludeEndpoint(
|
|
|
155
177
|
|
|
156
178
|
async function processOpenApiFile(
|
|
157
179
|
openApiPathOrConfig: string | NavOpenApi,
|
|
158
|
-
parentSlug: string = ""
|
|
180
|
+
parentSlug: string = "",
|
|
159
181
|
): Promise<OpenApiRoute[]> {
|
|
160
182
|
const routes: OpenApiRoute[] = [];
|
|
161
183
|
const CWD = process.cwd();
|
|
@@ -243,7 +265,7 @@ async function processOpenApiFile(
|
|
|
243
265
|
async function processMenuItem(
|
|
244
266
|
menuItem: NavMenuItem,
|
|
245
267
|
parentSlug: string = "",
|
|
246
|
-
docs: any[]
|
|
268
|
+
docs: any[],
|
|
247
269
|
): Promise<Route[]> {
|
|
248
270
|
let routes: Route[] = [];
|
|
249
271
|
const menuItemSlug = slugify(menuItem.label);
|
|
@@ -253,17 +275,16 @@ async function processMenuItem(
|
|
|
253
275
|
|
|
254
276
|
const submenu = menuItem.submenu;
|
|
255
277
|
|
|
256
|
-
// Process pages if they exist
|
|
278
|
+
// Process pages if they exist (pages array can contain pages and groups)
|
|
257
279
|
if (submenu.pages) {
|
|
258
|
-
submenu.pages.forEach((
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
routes = routes.concat(processGroup(group, currentPrefix, docs));
|
|
280
|
+
submenu.pages.forEach((item) => {
|
|
281
|
+
if (typeof item === "string" || "page" in item) {
|
|
282
|
+
routes.push(processPageItem(item as NavPageItem, currentPrefix, docs));
|
|
283
|
+
} else if ("group" in item) {
|
|
284
|
+
routes = routes.concat(
|
|
285
|
+
processGroup(item as NavGroup, currentPrefix, docs),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
267
288
|
});
|
|
268
289
|
}
|
|
269
290
|
|
|
@@ -271,7 +292,7 @@ async function processMenuItem(
|
|
|
271
292
|
if (submenu.openapi) {
|
|
272
293
|
const openApiRoutes = await processOpenApiFile(
|
|
273
294
|
submenu.openapi,
|
|
274
|
-
currentPrefix
|
|
295
|
+
currentPrefix,
|
|
275
296
|
);
|
|
276
297
|
routes = routes.concat(openApiRoutes);
|
|
277
298
|
}
|
|
@@ -289,19 +310,16 @@ export async function getAllRoutes(): Promise<Route[]> {
|
|
|
289
310
|
|
|
290
311
|
let allRoutes: Route[] = [];
|
|
291
312
|
|
|
292
|
-
// 3. Identify the active navigation container (pages
|
|
313
|
+
// 3. Identify the active navigation container (pages or menu)
|
|
293
314
|
// We use the XOR guarantee from validateNavigation (only one key exists)
|
|
294
315
|
if (navigation.pages) {
|
|
295
|
-
// Case 1:
|
|
316
|
+
// Case 1: Pages array at the top level (can contain pages and groups)
|
|
296
317
|
navigation.pages.forEach((item) => {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
navigation.groups.forEach((group) => {
|
|
303
|
-
// items are guaranteed to be NavGroup
|
|
304
|
-
allRoutes = allRoutes.concat(processGroup(group, "", docs));
|
|
318
|
+
if (typeof item === "string" || "page" in item) {
|
|
319
|
+
allRoutes.push(processPageItem(item as NavPageItem, "", docs));
|
|
320
|
+
} else if ("group" in item) {
|
|
321
|
+
allRoutes = allRoutes.concat(processGroup(item as NavGroup, "", docs));
|
|
322
|
+
}
|
|
305
323
|
});
|
|
306
324
|
} else if (navigation.menu) {
|
|
307
325
|
// Case 3: Menu object at the top level
|
|
@@ -57,3 +57,23 @@ export function deriveTitleFromEntryId(filePath: string): string {
|
|
|
57
57
|
.trim() || "Untitled"
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
export function buildMdxPageHref(args: {
|
|
62
|
+
filePath: string;
|
|
63
|
+
groupSlug?: string;
|
|
64
|
+
homePath?: string;
|
|
65
|
+
}): string {
|
|
66
|
+
if (args.homePath && args.filePath === args.homePath) {
|
|
67
|
+
return "/";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const filename = path.basename(args.filePath);
|
|
71
|
+
const pageSlug = slugify(filename);
|
|
72
|
+
const normalizedGroupSlug = (args.groupSlug || "")
|
|
73
|
+
.replace(/^\/+/, "")
|
|
74
|
+
.replace(/\/+$/, "");
|
|
75
|
+
|
|
76
|
+
return normalizedGroupSlug
|
|
77
|
+
? `/${normalizedGroupSlug}/${pageSlug}`
|
|
78
|
+
: `/${pageSlug}`;
|
|
79
|
+
}
|