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.
Files changed (78) hide show
  1. package/dist/index.js +32 -6
  2. package/package.json +3 -3
  3. package/template/astro.config.mjs +76 -3
  4. package/template/package-lock.json +924 -737
  5. package/template/package.json +7 -5
  6. package/template/scripts/generate-og-images.mjs +335 -0
  7. package/template/scripts/generate-og-metadata.mjs +173 -0
  8. package/template/scripts/rewrite-static-asset-host.mjs +408 -0
  9. package/template/scripts/stamp-image-versions.mjs +277 -0
  10. package/template/scripts/stamp-og-image-versions.mjs +199 -0
  11. package/template/scripts/stamp-pagefind-runtime-version.mjs +140 -0
  12. package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
  13. package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
  14. package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
  15. package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
  16. package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
  17. package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
  18. package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
  19. package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
  20. package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
  21. package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
  22. package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
  23. package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
  24. package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
  25. package/template/src/components/Footer.astro +94 -0
  26. package/template/src/components/Header.astro +11 -66
  27. package/template/src/components/LogoLink.astro +103 -0
  28. package/template/src/components/MdxPage.astro +126 -11
  29. package/template/src/components/OpenApiPage.astro +1036 -69
  30. package/template/src/components/Search.astro +0 -2
  31. package/template/src/components/SidebarDropdown.astro +34 -14
  32. package/template/src/components/SidebarGroup.astro +3 -6
  33. package/template/src/components/SidebarLink.astro +22 -12
  34. package/template/src/components/SidebarMenu.astro +19 -16
  35. package/template/src/components/SidebarSegmented.astro +99 -0
  36. package/template/src/components/SidebarSubgroup.astro +12 -12
  37. package/template/src/components/ThemeSwitcher.astro +30 -7
  38. package/template/src/components/endpoint/PlaygroundBar.astro +32 -36
  39. package/template/src/components/endpoint/PlaygroundButton.astro +40 -4
  40. package/template/src/components/endpoint/PlaygroundField.astro +1068 -22
  41. package/template/src/components/endpoint/PlaygroundForm.astro +559 -61
  42. package/template/src/components/endpoint/RequestSnippets.astro +342 -193
  43. package/template/src/components/endpoint/ResponseDisplay.astro +161 -147
  44. package/template/src/components/endpoint/ResponseFieldTree.astro +134 -0
  45. package/template/src/components/endpoint/ResponseFields.astro +711 -68
  46. package/template/src/components/endpoint/ResponseSnippets.astro +299 -173
  47. package/template/src/components/sidebar/SidebarEndpointLink.astro +1 -1
  48. package/template/src/components/ui/CodeLanguageIcon.astro +19 -0
  49. package/template/src/components/ui/CodeTabEdge.astro +79 -0
  50. package/template/src/components/ui/Field.astro +103 -20
  51. package/template/src/components/ui/Icon.astro +32 -0
  52. package/template/src/components/ui/ListChevronsToggle.astro +31 -0
  53. package/template/src/components/ui/Tag.astro +1 -1
  54. package/template/src/components/user/{Accordian.astro → Accordion.astro} +6 -6
  55. package/template/src/components/user/Callout.astro +5 -9
  56. package/template/src/components/user/CodeBlock.astro +400 -0
  57. package/template/src/components/user/CodeGroup.astro +225 -0
  58. package/template/src/components/user/ComponentPreview.astro +1 -0
  59. package/template/src/components/user/ComponentPreviewBlock.astro +181 -0
  60. package/template/src/components/user/Image.astro +132 -0
  61. package/template/src/components/user/Steps.astro +1 -3
  62. package/template/src/components/user/Tabs.astro +2 -2
  63. package/template/src/content.config.ts +1 -0
  64. package/template/src/layouts/Layout.astro +109 -8
  65. package/template/src/lib/code/code-block.ts +546 -0
  66. package/template/src/lib/frontmatter-schema.ts +8 -7
  67. package/template/src/lib/mdx/remark-code-block-component.ts +342 -0
  68. package/template/src/lib/mdx/remark-demote-h1.ts +16 -0
  69. package/template/src/lib/pagefind.ts +19 -5
  70. package/template/src/lib/routes.ts +49 -31
  71. package/template/src/lib/utils.ts +20 -0
  72. package/template/src/lib/validation.ts +638 -200
  73. package/template/src/pages/[...slug].astro +18 -5
  74. package/template/src/styles/geist-mono.css +33 -0
  75. package/template/src/styles/global.css +89 -84
  76. package/template/src/styles/google-sans-flex.css +143 -0
  77. package/template/ec.config.mjs +0 -51
  78. /package/template/src/components/user/{AccordianGroup.astro → AccordionGroup.astro} +0 -0
@@ -0,0 +1,546 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ bundledLanguagesInfo,
5
+ bundledLanguages,
6
+ getSingletonHighlighter,
7
+ type ThemedToken,
8
+ } from "shiki";
9
+ import { DEFAULT_FILE, getIconForFile } from "vscode-icons-js";
10
+
11
+ export const DEFAULT_CODE_BLOCK_LANGUAGE = "plaintext";
12
+
13
+ const SHIKI_LANGUAGE_ALIAS_TO_VALUE: Record<string, string> = (() => {
14
+ const aliasToValueMap: Record<string, string> = Object.create(null);
15
+
16
+ for (const languageInfo of bundledLanguagesInfo) {
17
+ const id = languageInfo.id.toLowerCase();
18
+ aliasToValueMap[id] = id;
19
+
20
+ for (const alias of languageInfo.aliases ?? []) {
21
+ const normalizedAlias = alias.toLowerCase();
22
+ aliasToValueMap[normalizedAlias] = id;
23
+ }
24
+ }
25
+
26
+ return aliasToValueMap;
27
+ })();
28
+
29
+ const LANGUAGE_ALIAS_COMPAT_TO_VALUE: Record<string, string> = {
30
+ text: "plaintext",
31
+ plain: "plaintext",
32
+ txt: "plaintext",
33
+ node: "javascript",
34
+ graphqls: "graphql",
35
+ htm: "html",
36
+ };
37
+
38
+ const FILE_EXTENSION_BY_LANGUAGE: Record<string, string> = {
39
+ bash: "sh",
40
+ shell: "sh",
41
+ shellscript: "sh",
42
+ c: "c",
43
+ cpp: "cpp",
44
+ csharp: "cs",
45
+ css: "css",
46
+ diff: "diff",
47
+ go: "go",
48
+ graphql: "graphql",
49
+ html: "html",
50
+ java: "java",
51
+ javascript: "js",
52
+ jsx: "jsx",
53
+ json: "json",
54
+ kotlin: "kt",
55
+ markdown: "md",
56
+ mdx: "mdx",
57
+ php: "php",
58
+ plaintext: "txt",
59
+ python: "py",
60
+ ruby: "rb",
61
+ rust: "rs",
62
+ sql: "sql",
63
+ swift: "swift",
64
+ toml: "toml",
65
+ typescript: "ts",
66
+ tsx: "tsx",
67
+ xml: "xml",
68
+ yaml: "yml",
69
+ };
70
+
71
+ const CODE_BLOCK_LANGUAGE_BY_FILE_EXTENSION: Record<string, string> = {
72
+ sh: "shellscript",
73
+ c: "c",
74
+ cpp: "cpp",
75
+ cc: "cpp",
76
+ cxx: "cpp",
77
+ cs: "csharp",
78
+ css: "css",
79
+ diff: "diff",
80
+ go: "go",
81
+ graphql: "graphql",
82
+ gql: "graphql",
83
+ htm: "html",
84
+ html: "html",
85
+ java: "java",
86
+ js: "javascript",
87
+ jsx: "jsx",
88
+ json: "json",
89
+ kt: "kotlin",
90
+ md: "markdown",
91
+ mdx: "mdx",
92
+ markdown: "markdown",
93
+ php: "php",
94
+ py: "python",
95
+ rb: "ruby",
96
+ rs: "rust",
97
+ sql: "sql",
98
+ swift: "swift",
99
+ toml: "toml",
100
+ ts: "typescript",
101
+ tsx: "tsx",
102
+ xml: "xml",
103
+ yaml: "yaml",
104
+ yml: "yaml",
105
+ };
106
+
107
+ const CODE_BLOCK_LANGUAGE_ICON_FILE_BY_VALUE: Record<string, string> = {
108
+ bash: "file_type_shell.svg",
109
+ shell: "file_type_shell.svg",
110
+ shellscript: "file_type_shell.svg",
111
+ c: "file_type_c.svg",
112
+ cpp: "file_type_cpp.svg",
113
+ csharp: "file_type_csharp.svg",
114
+ css: "file_type_css.svg",
115
+ diff: "file_type_diff.svg",
116
+ go: "file_type_go.svg",
117
+ graphql: "file_type_graphql.svg",
118
+ html: "file_type_html.svg",
119
+ java: "file_type_java.svg",
120
+ javascript: "file_type_js_official.svg",
121
+ jsx: "file_type_reactjs.svg",
122
+ json: "file_type_json_official.svg",
123
+ kotlin: "file_type_kotlin.svg",
124
+ markdown: "file_type_markdown.svg",
125
+ mdx: "file_type_markdown.svg",
126
+ php: "file_type_php.svg",
127
+ plaintext: "default_file.svg",
128
+ python: "file_type_python.svg",
129
+ ruby: "file_type_ruby.svg",
130
+ rust: "file_type_rust.svg",
131
+ sql: "file_type_sql.svg",
132
+ swift: "file_type_swift.svg",
133
+ toml: "file_type_toml.svg",
134
+ typescript: "file_type_typescript_official.svg",
135
+ tsx: "file_type_reactts.svg",
136
+ xml: "file_type_xml.svg",
137
+ yaml: "file_type_yaml_official.svg",
138
+ };
139
+
140
+ const SHIKI_THEME = "github-light";
141
+ const BUNDLED_LANGUAGE_SET = new Set(Object.keys(bundledLanguages));
142
+ const LANGUAGE_RUNTIME_DEPENDENCIES: Record<string, string[]> = {
143
+ // MDX tokenization relies on TSX grammar injections for JSX-style tags.
144
+ // Without TSX loaded first, Shiki can return mostly plain tokens.
145
+ mdx: ["tsx"],
146
+ };
147
+
148
+ const ICON_DIRECTORY = path.resolve(
149
+ process.cwd(),
150
+ "node_modules/@xt0rted/expressive-code-file-icons/dist/icons",
151
+ );
152
+
153
+ const iconSvgCache = new Map<string, string>();
154
+ let iconSvgInstanceCounter = 0;
155
+ let iconFileNameSetPromise: Promise<Set<string>> | null = null;
156
+
157
+ let highlighterPromise:
158
+ | Promise<Awaited<ReturnType<typeof getSingletonHighlighter>>>
159
+ | null = null;
160
+ const loadedLanguageSet = new Set<string>([DEFAULT_CODE_BLOCK_LANGUAGE]);
161
+ const languageLoadPromiseByName = new Map<string, Promise<void>>();
162
+
163
+ function resolveAliasLanguage(rawValue: string): string {
164
+ const normalized = rawValue.trim().toLowerCase();
165
+ if (!normalized) return "";
166
+
167
+ const firstToken = normalized.split(/\s+/)[0] ?? "";
168
+ if (!firstToken) return "";
169
+
170
+ const tokenMatch = firstToken.match(/[a-z0-9+#-]+/i);
171
+ const token = (tokenMatch?.[0] ?? firstToken).toLowerCase();
172
+ return (
173
+ SHIKI_LANGUAGE_ALIAS_TO_VALUE[token] ??
174
+ LANGUAGE_ALIAS_COMPAT_TO_VALUE[token] ??
175
+ token
176
+ );
177
+ }
178
+
179
+ function resolveLanguageFromFileName(fileName: string): string {
180
+ const extensionIndex = fileName.lastIndexOf(".");
181
+ if (extensionIndex < 0 || extensionIndex >= fileName.length - 1) return "";
182
+ const extension = fileName.slice(extensionIndex + 1).toLowerCase();
183
+ return CODE_BLOCK_LANGUAGE_BY_FILE_EXTENSION[extension] ?? "";
184
+ }
185
+
186
+ async function getIconFileNameSet(): Promise<Set<string>> {
187
+ if (!iconFileNameSetPromise) {
188
+ iconFileNameSetPromise = fs
189
+ .readdir(ICON_DIRECTORY)
190
+ .then((entries) => new Set(entries))
191
+ .catch(() => new Set<string>(["default_file.svg"]));
192
+ }
193
+
194
+ return iconFileNameSetPromise;
195
+ }
196
+
197
+ async function hasIconFileName(iconFileName: string): Promise<boolean> {
198
+ const iconFileNameSet = await getIconFileNameSet();
199
+ return iconFileNameSet.has(iconFileName);
200
+ }
201
+
202
+ function uniqueValues(values: string[]): string[] {
203
+ return Array.from(new Set(values.filter((value) => value.length > 0)));
204
+ }
205
+
206
+ function sanitizeIconLookupToken(value: string): string {
207
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "");
208
+ }
209
+
210
+ function buildIconLookupTokens(language: string): string[] {
211
+ const baseToken = sanitizeIconLookupToken(language);
212
+ if (!baseToken) return [];
213
+
214
+ const kebabToken = baseToken
215
+ .replace(/_/g, "-")
216
+ .replace(/-+/g, "-")
217
+ .replace(/^-|-$/g, "");
218
+ const snakeToken = kebabToken.replace(/-/g, "_");
219
+ const compactToken = snakeToken.replace(/_/g, "");
220
+
221
+ return uniqueValues([baseToken, kebabToken, snakeToken, compactToken]);
222
+ }
223
+
224
+ function pickIconFromFileName(fileName: string): string | null {
225
+ const trimmedFileName = fileName.trim();
226
+ if (!trimmedFileName) return null;
227
+
228
+ const candidates = uniqueValues([trimmedFileName, trimmedFileName.toLowerCase()]);
229
+ for (const candidate of candidates) {
230
+ const iconFileName = getIconForFile(candidate);
231
+ if (iconFileName !== DEFAULT_FILE) return iconFileName;
232
+ }
233
+
234
+ return null;
235
+ }
236
+
237
+ async function resolveIconFileName(
238
+ language: string,
239
+ fileName: string,
240
+ ): Promise<string> {
241
+ const resolvedLanguage = resolveAliasLanguage(language);
242
+ const languageFromFileName = fileName
243
+ ? resolveAliasLanguage(resolveLanguageFromFileName(fileName))
244
+ : "";
245
+
246
+ // Preserve explicit icon choices for core languages.
247
+ const explicitIconLanguageCandidates = uniqueValues([
248
+ resolvedLanguage,
249
+ languageFromFileName,
250
+ ]);
251
+ for (const candidate of explicitIconLanguageCandidates) {
252
+ const mappedIconFileName = CODE_BLOCK_LANGUAGE_ICON_FILE_BY_VALUE[candidate];
253
+ if (mappedIconFileName && (await hasIconFileName(mappedIconFileName))) {
254
+ return mappedIconFileName;
255
+ }
256
+ }
257
+
258
+ // Use vscode-icons-js matching first for best coverage.
259
+ const hasMappedDefaultExtension =
260
+ !!resolvedLanguage && FILE_EXTENSION_BY_LANGUAGE[resolvedLanguage] !== undefined;
261
+ const mappedDefaultFileName = hasMappedDefaultExtension
262
+ ? buildDefaultCodeFileName(resolvedLanguage)
263
+ : "";
264
+ const fileNameCandidates = uniqueValues([
265
+ fileName.trim(),
266
+ resolvedLanguage ? `file.${resolvedLanguage}` : "",
267
+ languageFromFileName ? `file.${languageFromFileName}` : "",
268
+ language ? `file.${language.trim().toLowerCase()}` : "",
269
+ mappedDefaultFileName,
270
+ ]);
271
+ for (const candidate of fileNameCandidates) {
272
+ const matchedIconFileName = pickIconFromFileName(candidate);
273
+ if (matchedIconFileName && (await hasIconFileName(matchedIconFileName))) {
274
+ return matchedIconFileName;
275
+ }
276
+ }
277
+
278
+ // Last attempt: direct language icon file lookup.
279
+ const iconTokenCandidates = uniqueValues([
280
+ ...buildIconLookupTokens(resolvedLanguage),
281
+ ...buildIconLookupTokens(languageFromFileName),
282
+ ...buildIconLookupTokens(language),
283
+ ]);
284
+ for (const token of iconTokenCandidates) {
285
+ const directIconFileNames = [
286
+ `file_type_${token}.svg`,
287
+ `file_type_light_${token}.svg`,
288
+ ];
289
+ for (const iconFileName of directIconFileNames) {
290
+ if (await hasIconFileName(iconFileName)) return iconFileName;
291
+ }
292
+ }
293
+
294
+ return "default_file.svg";
295
+ }
296
+
297
+ async function readIconSvg(iconFileName: string): Promise<string> {
298
+ const cached = iconSvgCache.get(iconFileName);
299
+ if (cached) return cached;
300
+
301
+ const filePath = path.join(ICON_DIRECTORY, iconFileName);
302
+ const svg = await fs.readFile(filePath, "utf8");
303
+ iconSvgCache.set(iconFileName, svg);
304
+ return svg;
305
+ }
306
+
307
+ function addClassToSvgRoot(svg: string, className: string): string {
308
+ const openTagStart = svg.indexOf("<svg");
309
+ if (openTagStart === -1) return svg;
310
+
311
+ const openTagEnd = svg.indexOf(">", openTagStart);
312
+ if (openTagEnd === -1) return svg;
313
+
314
+ const openTag = svg.slice(openTagStart, openTagEnd);
315
+ const classMatch = openTag.match(/\sclass=(["'])(.*?)\1/);
316
+
317
+ if (classMatch) {
318
+ const existingClass = classMatch[2].trim();
319
+ const nextClass = existingClass.length
320
+ ? `${existingClass} ${className}`
321
+ : className;
322
+ const nextTag = openTag.replace(
323
+ /\sclass=(["'])(.*?)\1/,
324
+ ` class="${nextClass}"`,
325
+ );
326
+ return `${svg.slice(0, openTagStart)}${nextTag}${svg.slice(openTagEnd)}`;
327
+ }
328
+
329
+ const withClass = `${openTag} class="${className}"`;
330
+ return `${svg.slice(0, openTagStart)}${withClass}${svg.slice(openTagEnd)}`;
331
+ }
332
+
333
+ function escapeRegExp(value: string): string {
334
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
335
+ }
336
+
337
+ function namespaceSvgIds(svg: string, namespace: string): string {
338
+ const idMatches = [...svg.matchAll(/\bid=(["'])([^"']+)\1/g)];
339
+ if (idMatches.length === 0) return svg;
340
+
341
+ const idMap = new Map<string, string>();
342
+ for (const match of idMatches) {
343
+ const idValue = match[2];
344
+ if (!idMap.has(idValue)) {
345
+ idMap.set(idValue, `${idValue}-${namespace}`);
346
+ }
347
+ }
348
+
349
+ let namespacedSvg = svg;
350
+ for (const [originalId, namespacedId] of idMap) {
351
+ const escapedId = escapeRegExp(originalId);
352
+
353
+ namespacedSvg = namespacedSvg.replace(
354
+ new RegExp(`\\bid=(["'])${escapedId}\\1`, "g"),
355
+ `id="${namespacedId}"`,
356
+ );
357
+
358
+ namespacedSvg = namespacedSvg.replace(
359
+ new RegExp(`url\\(#${escapedId}\\)`, "g"),
360
+ `url(#${namespacedId})`,
361
+ );
362
+
363
+ namespacedSvg = namespacedSvg.replace(
364
+ new RegExp(`\\b(href|xlink:href)=(["'])#${escapedId}\\2`, "g"),
365
+ `$1="#${namespacedId}"`,
366
+ );
367
+ }
368
+
369
+ return namespacedSvg;
370
+ }
371
+
372
+ async function getHighlighter() {
373
+ if (!highlighterPromise) {
374
+ highlighterPromise = getSingletonHighlighter({
375
+ themes: [SHIKI_THEME],
376
+ langs: [DEFAULT_CODE_BLOCK_LANGUAGE],
377
+ });
378
+ }
379
+
380
+ return highlighterPromise;
381
+ }
382
+
383
+ function resolveLoadableLanguage(
384
+ highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
385
+ normalizedLanguage: string,
386
+ ): string | null {
387
+ const aliasResolvedLanguage = highlighter.resolveLangAlias(normalizedLanguage);
388
+
389
+ if (
390
+ typeof aliasResolvedLanguage === "string" &&
391
+ aliasResolvedLanguage.length > 0 &&
392
+ BUNDLED_LANGUAGE_SET.has(aliasResolvedLanguage)
393
+ ) {
394
+ return aliasResolvedLanguage;
395
+ }
396
+
397
+ if (BUNDLED_LANGUAGE_SET.has(normalizedLanguage)) {
398
+ return normalizedLanguage;
399
+ }
400
+
401
+ return null;
402
+ }
403
+
404
+ async function ensureLanguageLoaded(
405
+ highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
406
+ language: string,
407
+ ): Promise<void> {
408
+ if (!language || loadedLanguageSet.has(language)) return;
409
+
410
+ const existingLoadPromise = languageLoadPromiseByName.get(language);
411
+ if (existingLoadPromise) {
412
+ await existingLoadPromise;
413
+ return;
414
+ }
415
+
416
+ const loadPromise = highlighter
417
+ .loadLanguage(language)
418
+ .then(() => {
419
+ loadedLanguageSet.add(language);
420
+ })
421
+ .finally(() => {
422
+ languageLoadPromiseByName.delete(language);
423
+ });
424
+
425
+ languageLoadPromiseByName.set(language, loadPromise);
426
+ await loadPromise;
427
+ }
428
+
429
+ function buildLanguageLoadOrder(language: string): string[] {
430
+ const orderedLanguages: string[] = [];
431
+ const visitedLanguageSet = new Set<string>();
432
+
433
+ const visitLanguage = (languageValue: string) => {
434
+ if (!languageValue || visitedLanguageSet.has(languageValue)) return;
435
+ visitedLanguageSet.add(languageValue);
436
+
437
+ const dependencyLanguages =
438
+ LANGUAGE_RUNTIME_DEPENDENCIES[languageValue] ?? [];
439
+ for (const dependencyLanguage of dependencyLanguages) {
440
+ visitLanguage(dependencyLanguage);
441
+ }
442
+
443
+ orderedLanguages.push(languageValue);
444
+ };
445
+
446
+ visitLanguage(language);
447
+ return orderedLanguages;
448
+ }
449
+
450
+ export function normalizeCodeLanguageValue(rawLanguage: string): string {
451
+ const normalizedLanguage = resolveAliasLanguage(rawLanguage);
452
+ return normalizedLanguage || DEFAULT_CODE_BLOCK_LANGUAGE;
453
+ }
454
+
455
+ export function buildDefaultCodeFileName(language: string): string {
456
+ const normalizedLanguage = normalizeCodeLanguageValue(language);
457
+ const extension = FILE_EXTENSION_BY_LANGUAGE[normalizedLanguage] ?? "txt";
458
+ return `file-name.${extension}`;
459
+ }
460
+
461
+ export async function getLanguageIconSvg({
462
+ language,
463
+ fileName,
464
+ className,
465
+ }: {
466
+ language: string;
467
+ fileName: string;
468
+ className: string;
469
+ }): Promise<string> {
470
+ const iconFileName = await resolveIconFileName(language, fileName);
471
+
472
+ let iconSvg = "";
473
+ try {
474
+ iconSvg = await readIconSvg(iconFileName);
475
+ } catch {
476
+ iconSvg = await readIconSvg("default_file.svg");
477
+ }
478
+
479
+ iconSvgInstanceCounter += 1;
480
+ const namespacedSvg = namespaceSvgIds(
481
+ iconSvg,
482
+ `rd-icon-${iconSvgInstanceCounter.toString(36)}`,
483
+ );
484
+
485
+ return addClassToSvgRoot(namespacedSvg, className);
486
+ }
487
+
488
+ export async function getCodeLineTokens({
489
+ code,
490
+ language,
491
+ }: {
492
+ code: string;
493
+ language: string;
494
+ }): Promise<{
495
+ normalizedLanguage: string;
496
+ lines: ThemedToken[][];
497
+ }> {
498
+ const highlighter = await getHighlighter();
499
+ const normalizedLanguage = normalizeCodeLanguageValue(language);
500
+ const loadableLanguage = resolveLoadableLanguage(
501
+ highlighter,
502
+ normalizedLanguage,
503
+ );
504
+ const targetLanguage = loadableLanguage ?? DEFAULT_CODE_BLOCK_LANGUAGE;
505
+
506
+ if (targetLanguage !== DEFAULT_CODE_BLOCK_LANGUAGE) {
507
+ const languageLoadOrder = buildLanguageLoadOrder(targetLanguage);
508
+ try {
509
+ for (const languageToLoad of languageLoadOrder) {
510
+ if (languageToLoad === DEFAULT_CODE_BLOCK_LANGUAGE) continue;
511
+
512
+ const loadableDependencyLanguage = resolveLoadableLanguage(
513
+ highlighter,
514
+ languageToLoad,
515
+ );
516
+ if (!loadableDependencyLanguage) continue;
517
+
518
+ await ensureLanguageLoaded(highlighter, loadableDependencyLanguage);
519
+ }
520
+ } catch {
521
+ // Fall through to plaintext rendering below.
522
+ }
523
+ }
524
+
525
+ try {
526
+ const tokenResult = highlighter.codeToTokens(code, {
527
+ lang: targetLanguage,
528
+ theme: SHIKI_THEME,
529
+ });
530
+
531
+ return {
532
+ normalizedLanguage: targetLanguage,
533
+ lines: tokenResult.tokens,
534
+ };
535
+ } catch {
536
+ const tokenResult = highlighter.codeToTokens(code, {
537
+ lang: DEFAULT_CODE_BLOCK_LANGUAGE,
538
+ theme: SHIKI_THEME,
539
+ });
540
+
541
+ return {
542
+ normalizedLanguage: DEFAULT_CODE_BLOCK_LANGUAGE,
543
+ lines: tokenResult.tokens,
544
+ };
545
+ }
546
+ }
@@ -1,9 +1,10 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const docsSchema = z.object({
4
- title: z
5
- .string({
6
- invalid_type_error: "'title' must be a text string.",
7
- })
8
- .optional(),
9
- });
3
+ // Frontmatter schema for MDX files.
4
+ // `title` controls the rendered page title (not navigation labels).
5
+ export const docsSchema = z
6
+ .object({
7
+ title: z.string().optional(),
8
+ description: z.string().optional(),
9
+ })
10
+ .passthrough();