radiant-docs 0.1.61 → 0.1.63

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 (39) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +38 -27
  3. package/template/package-lock.json +2858 -1140
  4. package/template/package.json +18 -13
  5. package/template/scripts/generate-proxy-allowed-origins.mjs +10 -179
  6. package/template/scripts/publish-shiki-platform-assets.mjs +1177 -0
  7. package/template/src/components/Header.astro +6 -1
  8. package/template/src/components/NavigationTabList.astro +65 -0
  9. package/template/src/components/NavigationTabs.astro +109 -0
  10. package/template/src/components/OpenApiPage.astro +17 -1
  11. package/template/src/components/Sidebar.astro +2 -2
  12. package/template/src/components/SidebarDropdown.astro +105 -44
  13. package/template/src/components/SidebarMenu.astro +3 -0
  14. package/template/src/components/SidebarSegmented.astro +87 -52
  15. package/template/src/components/SidebarTabs.astro +86 -0
  16. package/template/src/components/chat/AssistantDocsWidget.tsx +127 -2
  17. package/template/src/components/chat/AssistantEmbedPanel.tsx +401 -283
  18. package/template/src/components/endpoint/PlaygroundForm.astro +69 -55
  19. package/template/src/components/endpoint/ResponseDisplay.astro +2 -2
  20. package/template/src/components/user/Accordion.astro +1 -1
  21. package/template/src/components/user/Callout.astro +2 -2
  22. package/template/src/components/user/CodeBlock.astro +58 -7
  23. package/template/src/components/user/CodeGroup.astro +52 -1
  24. package/template/src/components/user/Column.astro +1 -1
  25. package/template/src/components/user/Step.astro +1 -1
  26. package/template/src/components/user/Tabs.astro +1 -1
  27. package/template/src/generated/shiki-platform-assets.json +24 -0
  28. package/template/src/layouts/Layout.astro +111 -8
  29. package/template/src/lib/assistant-panel-config.ts +4 -0
  30. package/template/src/lib/assistant-shiki-client.ts +522 -0
  31. package/template/src/lib/client-shiki-config.ts +60 -0
  32. package/template/src/lib/dev-playground-proxy.mjs +597 -0
  33. package/template/src/lib/mdx/remark-resolve-internal-links.ts +334 -17
  34. package/template/src/lib/proxy-allowed-origins.mjs +189 -0
  35. package/template/src/lib/routes.ts +66 -24
  36. package/template/src/styles/global.css +16 -4
  37. package/template/src/components/ui/demo/CodeDemo.astro +0 -15
  38. package/template/src/components/ui/demo/Demo.astro +0 -3
  39. package/template/src/components/ui/demo/UiDisplay.astro +0 -13
@@ -4,6 +4,8 @@ import {
4
4
  } from "./assistant-chrome";
5
5
  import { withBasePath } from "./base-path";
6
6
  import { getAssistantLauncherIconConfig } from "./assistant-embed-script";
7
+ import type { AssistantShikiRuntimeConfig } from "./assistant-shiki-client";
8
+ import { getClientShikiRuntimeConfig } from "./client-shiki-config";
7
9
  import type { DocsConfig } from "./validation";
8
10
 
9
11
  export type AssistantPanelRuntimeConfig = {
@@ -26,6 +28,7 @@ export type AssistantPanelRuntimeConfig = {
26
28
  emptyStateQuestions?: string[];
27
29
  devProxyToken?: string;
28
30
  chrome: AssistantChromeConfig;
31
+ shiki?: AssistantShikiRuntimeConfig;
29
32
  };
30
33
 
31
34
  export function getAssistantPanelRuntimeConfig(
@@ -76,5 +79,6 @@ export function getAssistantPanelRuntimeConfig(
76
79
  emptyStateQuestions: assistantConfig?.questions,
77
80
  devProxyToken: isDev ? assistantDevProxySecret : undefined,
78
81
  chrome: getAssistantChromeConfig(config),
82
+ shiki: getClientShikiRuntimeConfig(config),
79
83
  };
80
84
  }
@@ -0,0 +1,522 @@
1
+ export type AssistantCodeSyntaxThemes = {
2
+ light: string;
3
+ dark: string;
4
+ };
5
+
6
+ export type AssistantShikiRuntimeConfig = {
7
+ assetBaseUrl: string;
8
+ syntaxThemes: AssistantCodeSyntaxThemes;
9
+ };
10
+
11
+ type ShikiPlatformManifest = {
12
+ assetVersion: string;
13
+ languageAliases: Record<string, string>;
14
+ languages: Record<
15
+ string,
16
+ {
17
+ aliases?: string[];
18
+ displayName?: string;
19
+ module: string;
20
+ registrations?: string[];
21
+ }
22
+ >;
23
+ runtime: {
24
+ module: string;
25
+ };
26
+ themes: Record<
27
+ string,
28
+ {
29
+ displayName?: string;
30
+ module: string;
31
+ type?: string;
32
+ }
33
+ >;
34
+ };
35
+
36
+ type ShikiToken = {
37
+ content: string;
38
+ color?: string;
39
+ bgColor?: string;
40
+ fontStyle?: number;
41
+ htmlStyle?: Record<string, string>;
42
+ };
43
+
44
+ type ShikiTokensResult = {
45
+ tokens: ShikiToken[][];
46
+ };
47
+
48
+ type ShikiHighlighter = {
49
+ codeToTokens: (
50
+ code: string,
51
+ options: {
52
+ lang: string;
53
+ theme: string;
54
+ },
55
+ ) => ShikiTokensResult;
56
+ loadLanguage: (...languages: unknown[]) => Promise<void> | void;
57
+ };
58
+
59
+ type ShikiRuntimeModule = {
60
+ createRadiantShikiHighlighter: (options: {
61
+ langs?: unknown[];
62
+ themes?: unknown[];
63
+ warnings?: boolean;
64
+ }) => Promise<ShikiHighlighter>;
65
+ };
66
+
67
+ type LoadedShikiRuntime = {
68
+ highlighter: ShikiHighlighter;
69
+ manifest: ShikiPlatformManifest;
70
+ };
71
+
72
+ type AssistantCodeLineToken = {
73
+ content: string;
74
+ color?: string;
75
+ darkColor?: string;
76
+ bgColor?: string;
77
+ darkBgColor?: string;
78
+ fontStyle?: number;
79
+ htmlStyle?: Record<string, string>;
80
+ darkHtmlStyle?: Record<string, string>;
81
+ };
82
+
83
+ const SHIKI_PLAINTEXT_LANGUAGE = "plaintext";
84
+ const LANGUAGE_ALIAS_COMPAT_TO_VALUE: Record<string, string> = {
85
+ htm: "html",
86
+ node: "javascript",
87
+ plain: SHIKI_PLAINTEXT_LANGUAGE,
88
+ text: SHIKI_PLAINTEXT_LANGUAGE,
89
+ txt: SHIKI_PLAINTEXT_LANGUAGE,
90
+ };
91
+ const LANGUAGE_RUNTIME_DEPENDENCIES: Record<string, string[]> = {
92
+ mdx: ["tsx"],
93
+ };
94
+
95
+ const runtimePromiseByKey = new Map<string, Promise<LoadedShikiRuntime>>();
96
+ const languageLoadPromiseByKey = new Map<string, Promise<void>>();
97
+
98
+ function joinUrl(baseUrl: string, path: string): string {
99
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
100
+ }
101
+
102
+ function getRuntimeCacheKey(config: AssistantShikiRuntimeConfig): string {
103
+ return [
104
+ config.assetBaseUrl.trim(),
105
+ config.syntaxThemes.light,
106
+ config.syntaxThemes.dark,
107
+ ].join("\0");
108
+ }
109
+
110
+ async function importExternalModule<T>(url: string): Promise<T> {
111
+ return import(/* @vite-ignore */ url) as Promise<T>;
112
+ }
113
+
114
+ function normalizeModuleDefault(value: unknown): unknown[] {
115
+ const moduleValue = (value as { default?: unknown })?.default ?? value;
116
+ if (Array.isArray(moduleValue)) return moduleValue;
117
+ return moduleValue ? [moduleValue] : [];
118
+ }
119
+
120
+ async function loadManifest(
121
+ assetBaseUrl: string,
122
+ ): Promise<ShikiPlatformManifest> {
123
+ const response = await fetch(joinUrl(assetBaseUrl, "manifest.json"));
124
+ if (!response.ok) {
125
+ throw new Error(
126
+ `Failed to load Shiki manifest: ${response.status} ${response.statusText}`,
127
+ );
128
+ }
129
+
130
+ return (await response.json()) as ShikiPlatformManifest;
131
+ }
132
+
133
+ async function loadRuntime(
134
+ config: AssistantShikiRuntimeConfig,
135
+ ): Promise<LoadedShikiRuntime> {
136
+ const assetBaseUrl = config.assetBaseUrl.trim();
137
+ if (!assetBaseUrl) {
138
+ throw new Error("Shiki asset base URL is not configured.");
139
+ }
140
+
141
+ const runtimeCacheKey = getRuntimeCacheKey(config);
142
+ const cached = runtimePromiseByKey.get(runtimeCacheKey);
143
+ if (cached) return cached;
144
+
145
+ const runtimePromise = (async () => {
146
+ const manifest = await loadManifest(assetBaseUrl);
147
+ const runtime = await importExternalModule<ShikiRuntimeModule>(
148
+ joinUrl(assetBaseUrl, manifest.runtime.module),
149
+ );
150
+
151
+ const themeModules = await Promise.all(
152
+ Array.from(
153
+ new Set([config.syntaxThemes.light, config.syntaxThemes.dark]),
154
+ ).map(async (themeName) => {
155
+ const themeEntry = manifest.themes[themeName];
156
+ if (!themeEntry) {
157
+ throw new Error(`Shiki theme "${themeName}" is not available.`);
158
+ }
159
+
160
+ const moduleValue = await importExternalModule<unknown>(
161
+ joinUrl(assetBaseUrl, themeEntry.module),
162
+ );
163
+ return ((moduleValue as { default?: unknown }).default ??
164
+ moduleValue) as unknown;
165
+ }),
166
+ );
167
+
168
+ const highlighter = await runtime.createRadiantShikiHighlighter({
169
+ langs: [],
170
+ themes: themeModules,
171
+ warnings: false,
172
+ });
173
+
174
+ return { highlighter, manifest };
175
+ })();
176
+
177
+ runtimePromiseByKey.set(runtimeCacheKey, runtimePromise);
178
+ runtimePromise.catch(() => {
179
+ if (runtimePromiseByKey.get(runtimeCacheKey) === runtimePromise) {
180
+ runtimePromiseByKey.delete(runtimeCacheKey);
181
+ }
182
+ });
183
+ return runtimePromise;
184
+ }
185
+
186
+ export async function warmAssistantShikiRuntime(
187
+ config: AssistantShikiRuntimeConfig,
188
+ ): Promise<void> {
189
+ await loadRuntime(config);
190
+ }
191
+
192
+ function normalizeLanguageToken(rawLanguage: string): string {
193
+ const normalized = rawLanguage.trim().toLowerCase();
194
+ if (!normalized) return "";
195
+
196
+ const firstToken = normalized.split(/\s+/)[0] ?? "";
197
+ const tokenMatch = firstToken.match(/[a-z0-9+#-]+/i);
198
+ return (tokenMatch?.[0] ?? firstToken).toLowerCase();
199
+ }
200
+
201
+ export function normalizeAssistantCodeLanguage(rawLanguage: string): string {
202
+ return normalizeLanguageToken(rawLanguage);
203
+ }
204
+
205
+ function resolveManifestLanguage(
206
+ manifest: ShikiPlatformManifest,
207
+ rawLanguage: string,
208
+ ): string {
209
+ const token = normalizeLanguageToken(rawLanguage);
210
+ if (!token) return "";
211
+
212
+ return (
213
+ manifest.languageAliases[token] ??
214
+ manifest.languageAliases[LANGUAGE_ALIAS_COMPAT_TO_VALUE[token] ?? ""] ??
215
+ LANGUAGE_ALIAS_COMPAT_TO_VALUE[token] ??
216
+ token
217
+ );
218
+ }
219
+
220
+ function buildLanguageLoadOrder(language: string): string[] {
221
+ return [...(LANGUAGE_RUNTIME_DEPENDENCIES[language] ?? []), language].filter(
222
+ (value, index, values) => value && values.indexOf(value) === index,
223
+ );
224
+ }
225
+
226
+ async function ensureLanguageLoaded({
227
+ assetBaseUrl,
228
+ highlighter,
229
+ language,
230
+ manifest,
231
+ runtimeCacheKey,
232
+ }: {
233
+ assetBaseUrl: string;
234
+ highlighter: ShikiHighlighter;
235
+ language: string;
236
+ manifest: ShikiPlatformManifest;
237
+ runtimeCacheKey: string;
238
+ }): Promise<string> {
239
+ if (
240
+ !language ||
241
+ language === SHIKI_PLAINTEXT_LANGUAGE ||
242
+ language === "text"
243
+ ) {
244
+ return SHIKI_PLAINTEXT_LANGUAGE;
245
+ }
246
+
247
+ const manifestLanguage = manifest.languages[language]
248
+ ? language
249
+ : (manifest.languageAliases[language] ?? "");
250
+ const languageEntry = manifestLanguage
251
+ ? manifest.languages[manifestLanguage]
252
+ : undefined;
253
+ if (!manifestLanguage || !languageEntry) {
254
+ return SHIKI_PLAINTEXT_LANGUAGE;
255
+ }
256
+
257
+ const cacheKey = `${runtimeCacheKey}\0${manifestLanguage}`;
258
+ let loadPromise = languageLoadPromiseByKey.get(cacheKey);
259
+ if (!loadPromise) {
260
+ loadPromise = (async () => {
261
+ const moduleValue = await importExternalModule<unknown>(
262
+ joinUrl(assetBaseUrl, languageEntry.module),
263
+ );
264
+ const registrations = normalizeModuleDefault(moduleValue);
265
+ await highlighter.loadLanguage(...registrations);
266
+ })();
267
+ languageLoadPromiseByKey.set(cacheKey, loadPromise);
268
+ loadPromise.catch(() => {
269
+ if (languageLoadPromiseByKey.get(cacheKey) === loadPromise) {
270
+ languageLoadPromiseByKey.delete(cacheKey);
271
+ }
272
+ });
273
+ }
274
+
275
+ await loadPromise;
276
+ return manifestLanguage;
277
+ }
278
+
279
+ function escapeHtml(value: string): string {
280
+ return value
281
+ .replaceAll("&", "&amp;")
282
+ .replaceAll("<", "&lt;")
283
+ .replaceAll(">", "&gt;")
284
+ .replaceAll('"', "&quot;")
285
+ .replaceAll("'", "&#39;");
286
+ }
287
+
288
+ function escapeAttribute(value: string): string {
289
+ return escapeHtml(value).replaceAll("`", "&#96;");
290
+ }
291
+
292
+ function buildTokenStyle(token: AssistantCodeLineToken): string {
293
+ const styleEntries: string[] = [];
294
+
295
+ const pushStyleVariable = (property: string, value?: string) => {
296
+ if (value) styleEntries.push(`${property}:${value}`);
297
+ };
298
+
299
+ pushStyleVariable("--rd-token-color", token.color);
300
+ pushStyleVariable("--rd-token-color-dark", token.darkColor);
301
+ pushStyleVariable("--rd-token-bg", token.bgColor);
302
+ pushStyleVariable("--rd-token-bg-dark", token.darkBgColor);
303
+
304
+ const pushHtmlStyle = (
305
+ styleObject: Record<string, string> | undefined,
306
+ isDarkStyle: boolean,
307
+ ) => {
308
+ if (!styleObject || typeof styleObject !== "object") return;
309
+
310
+ for (const [property, value] of Object.entries(styleObject)) {
311
+ const normalizedProperty = property.trim().toLowerCase();
312
+ if (normalizedProperty === "color") {
313
+ pushStyleVariable(
314
+ isDarkStyle ? "--rd-token-color-dark" : "--rd-token-color",
315
+ value,
316
+ );
317
+ continue;
318
+ }
319
+ if (normalizedProperty === "background-color") {
320
+ pushStyleVariable(
321
+ isDarkStyle ? "--rd-token-bg-dark" : "--rd-token-bg",
322
+ value,
323
+ );
324
+ continue;
325
+ }
326
+
327
+ styleEntries.push(`${property}:${value}`);
328
+ }
329
+ };
330
+
331
+ pushHtmlStyle(token.htmlStyle, false);
332
+ pushHtmlStyle(token.darkHtmlStyle, true);
333
+
334
+ if (token.fontStyle) {
335
+ if (token.fontStyle & 1) styleEntries.push("font-style:italic");
336
+ if (token.fontStyle & 2) styleEntries.push("font-weight:600");
337
+ if (token.fontStyle & 4) styleEntries.push("text-decoration:underline");
338
+ }
339
+
340
+ return styleEntries.join(";");
341
+ }
342
+
343
+ function buildLightOnlyTokenLine(
344
+ lightLine: ShikiToken[],
345
+ ): AssistantCodeLineToken[] {
346
+ return lightLine.map((token) => ({
347
+ bgColor: token.bgColor,
348
+ color: token.color,
349
+ content: token.content,
350
+ fontStyle: token.fontStyle,
351
+ htmlStyle: token.htmlStyle,
352
+ }));
353
+ }
354
+
355
+ function mergeTokenLineByContent(
356
+ lightLine: ShikiToken[],
357
+ darkLine: ShikiToken[],
358
+ ): AssistantCodeLineToken[] | null {
359
+ const lightContent = lightLine.map((token) => token.content).join("");
360
+ const darkContent = darkLine.map((token) => token.content).join("");
361
+ if (lightContent !== darkContent) return null;
362
+ if (lightContent.length === 0) return [];
363
+
364
+ const mergedLine: AssistantCodeLineToken[] = [];
365
+ let lightTokenIndex = 0;
366
+ let darkTokenIndex = 0;
367
+ let lightTokenOffset = 0;
368
+ let darkTokenOffset = 0;
369
+
370
+ while (
371
+ lightTokenIndex < lightLine.length &&
372
+ darkTokenIndex < darkLine.length
373
+ ) {
374
+ const lightToken = lightLine[lightTokenIndex];
375
+ const darkToken = darkLine[darkTokenIndex];
376
+
377
+ if (lightTokenOffset >= lightToken.content.length) {
378
+ lightTokenIndex += 1;
379
+ lightTokenOffset = 0;
380
+ continue;
381
+ }
382
+
383
+ if (darkTokenOffset >= darkToken.content.length) {
384
+ darkTokenIndex += 1;
385
+ darkTokenOffset = 0;
386
+ continue;
387
+ }
388
+
389
+ const lightRemainingLength = lightToken.content.length - lightTokenOffset;
390
+ const darkRemainingLength = darkToken.content.length - darkTokenOffset;
391
+ const segmentLength = Math.min(lightRemainingLength, darkRemainingLength);
392
+ if (segmentLength <= 0) break;
393
+
394
+ const lightSegment = lightToken.content.slice(
395
+ lightTokenOffset,
396
+ lightTokenOffset + segmentLength,
397
+ );
398
+ const darkSegment = darkToken.content.slice(
399
+ darkTokenOffset,
400
+ darkTokenOffset + segmentLength,
401
+ );
402
+
403
+ if (lightSegment !== darkSegment) return null;
404
+
405
+ mergedLine.push({
406
+ bgColor: lightToken.bgColor,
407
+ color: lightToken.color,
408
+ content: lightSegment,
409
+ darkBgColor: darkToken.bgColor,
410
+ darkColor: darkToken.color,
411
+ darkHtmlStyle: darkToken.htmlStyle,
412
+ fontStyle: lightToken.fontStyle,
413
+ htmlStyle: lightToken.htmlStyle,
414
+ });
415
+
416
+ lightTokenOffset += segmentLength;
417
+ darkTokenOffset += segmentLength;
418
+ }
419
+
420
+ const mergedContent = mergedLine.map((token) => token.content).join("");
421
+ return mergedContent === lightContent ? mergedLine : null;
422
+ }
423
+
424
+ function mergeTokenLines(
425
+ lightTokenLines: ShikiToken[][],
426
+ darkTokenLines: ShikiToken[][],
427
+ ): AssistantCodeLineToken[][] {
428
+ const lineCount = Math.max(lightTokenLines.length, darkTokenLines.length);
429
+ const mergedLines: AssistantCodeLineToken[][] = [];
430
+
431
+ for (let lineIndex = 0; lineIndex < lineCount; lineIndex += 1) {
432
+ const lightLine = lightTokenLines[lineIndex] ?? [];
433
+ const darkLine = darkTokenLines[lineIndex] ?? [];
434
+ const mergedLine = mergeTokenLineByContent(lightLine, darkLine);
435
+ mergedLines.push(mergedLine ?? buildLightOnlyTokenLine(lightLine));
436
+ }
437
+
438
+ return mergedLines;
439
+ }
440
+
441
+ function renderTokenLinesHtml(
442
+ code: string,
443
+ tokenLines: AssistantCodeLineToken[][],
444
+ ): string {
445
+ const rawLines = code.split("\n");
446
+ const normalizedRawLines = rawLines.length > 0 ? rawLines : [""];
447
+ const lineCount = Math.max(1, normalizedRawLines.length, tokenLines.length);
448
+
449
+ return Array.from({ length: lineCount }, (_, lineIndex) => {
450
+ const lineTokens = tokenLines[lineIndex] ?? [];
451
+ const fallbackLineContent = normalizedRawLines[lineIndex] ?? "";
452
+ const tokenHtml =
453
+ lineTokens.length > 0
454
+ ? lineTokens
455
+ .map((token) => {
456
+ const tokenStyle = buildTokenStyle(token);
457
+ const tokenStyleAttribute = tokenStyle
458
+ ? ` style="${escapeAttribute(tokenStyle)}"`
459
+ : "";
460
+ return `<span data-rd-token${tokenStyleAttribute}>${escapeHtml(token.content)}</span>`;
461
+ })
462
+ .join("")
463
+ : fallbackLineContent.length > 0
464
+ ? escapeHtml(fallbackLineContent)
465
+ : "&nbsp;";
466
+
467
+ return `<span class="flex w-max min-w-full"><span class="flex-1 whitespace-pre pl-3 pr-3">${tokenHtml}</span></span>`;
468
+ }).join("");
469
+ }
470
+
471
+ export async function highlightAssistantCodeToHtml({
472
+ code,
473
+ config,
474
+ language,
475
+ }: {
476
+ code: string;
477
+ config: AssistantShikiRuntimeConfig;
478
+ language: string;
479
+ }): Promise<{
480
+ html: string;
481
+ language: string;
482
+ }> {
483
+ const runtime = await loadRuntime(config);
484
+ const runtimeCacheKey = getRuntimeCacheKey(config);
485
+ const requestedLanguage = resolveManifestLanguage(runtime.manifest, language);
486
+ let targetLanguage = requestedLanguage || SHIKI_PLAINTEXT_LANGUAGE;
487
+
488
+ for (const languageToLoad of buildLanguageLoadOrder(targetLanguage)) {
489
+ targetLanguage = await ensureLanguageLoaded({
490
+ assetBaseUrl: config.assetBaseUrl,
491
+ highlighter: runtime.highlighter,
492
+ language: languageToLoad,
493
+ manifest: runtime.manifest,
494
+ runtimeCacheKey,
495
+ });
496
+ }
497
+
498
+ try {
499
+ const lightTokenResult = runtime.highlighter.codeToTokens(code, {
500
+ lang: targetLanguage,
501
+ theme: config.syntaxThemes.light,
502
+ });
503
+ const darkTokenResult = runtime.highlighter.codeToTokens(code, {
504
+ lang: targetLanguage,
505
+ theme: config.syntaxThemes.dark,
506
+ });
507
+ const tokenLines = mergeTokenLines(
508
+ lightTokenResult.tokens,
509
+ darkTokenResult.tokens,
510
+ );
511
+
512
+ return {
513
+ html: renderTokenLinesHtml(code, tokenLines),
514
+ language: targetLanguage,
515
+ };
516
+ } catch {
517
+ return {
518
+ html: renderTokenLinesHtml(code, []),
519
+ language: SHIKI_PLAINTEXT_LANGUAGE,
520
+ };
521
+ }
522
+ }
@@ -0,0 +1,60 @@
1
+ import type { AssistantShikiRuntimeConfig } from "./assistant-shiki-client";
2
+ import type { DocsConfig } from "./validation";
3
+ import shikiPlatformAssets from "../generated/shiki-platform-assets.json";
4
+ import {
5
+ DEFAULT_SHIKI_DARK_THEME,
6
+ DEFAULT_SHIKI_LIGHT_THEME,
7
+ } from "radiant-docs-validator/shiki-theme-config";
8
+
9
+ export type ClientShikiRuntimeConfig = AssistantShikiRuntimeConfig;
10
+
11
+ function normalizeStaticAssetHost(value: unknown): string {
12
+ const rawValue = typeof value === "string" ? value.trim() : "";
13
+ if (!rawValue) return "";
14
+
15
+ const normalizedHost = rawValue.replace(/\/+$/, "");
16
+ return /^https?:\/\//i.test(normalizedHost)
17
+ ? normalizedHost
18
+ : `https://${normalizedHost}`;
19
+ }
20
+
21
+ function joinUrl(baseUrl: string, path: string): string {
22
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
23
+ }
24
+
25
+ function getConfiguredCodeSyntaxThemes(
26
+ config: DocsConfig,
27
+ ): ClientShikiRuntimeConfig["syntaxThemes"] {
28
+ const configuredSyntaxTheme = config.theme?.code?.syntaxTheme;
29
+
30
+ if (typeof configuredSyntaxTheme === "string") {
31
+ return {
32
+ light: configuredSyntaxTheme,
33
+ dark: configuredSyntaxTheme,
34
+ };
35
+ }
36
+
37
+ return {
38
+ light: configuredSyntaxTheme?.light ?? DEFAULT_SHIKI_LIGHT_THEME,
39
+ dark: configuredSyntaxTheme?.dark ?? DEFAULT_SHIKI_DARK_THEME,
40
+ };
41
+ }
42
+
43
+ export function getClientShikiRuntimeConfig(
44
+ config: DocsConfig,
45
+ ): ClientShikiRuntimeConfig | undefined {
46
+ const staticAssetHost = normalizeStaticAssetHost(
47
+ import.meta.env.STATIC_ASSET_HOST ?? process.env.STATIC_ASSET_HOST,
48
+ );
49
+ if (!staticAssetHost) {
50
+ return undefined;
51
+ }
52
+
53
+ return {
54
+ assetBaseUrl: joinUrl(
55
+ staticAssetHost,
56
+ `${shikiPlatformAssets.prefix}/${shikiPlatformAssets.assetVersion}`,
57
+ ),
58
+ syntaxThemes: getConfiguredCodeSyntaxThemes(config),
59
+ };
60
+ }