radiant-docs 0.1.47 → 0.1.49

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.
@@ -1,10 +1,9 @@
1
1
  ---
2
2
  import { validateNoUnknownProps, validateProps } from "../../lib/component-error";
3
- import { renderMarkdown } from "../../lib/utils";
4
3
  import { resolveStaticAssetUrl } from "../../lib/static-asset-url";
5
4
 
6
5
  interface Props {
7
- src: string;
6
+ src: string | { light?: string; dark?: string };
8
7
  alt?: string;
9
8
  title?: string;
10
9
  width?: number | string;
@@ -24,7 +23,7 @@ validateProps(
24
23
  "Image",
25
24
  imageProps,
26
25
  {
27
- src: { required: true, type: "string" },
26
+ src: { required: true, type: ["string", "object"] },
28
27
  alt: { type: "string" },
29
28
  title: { type: "string" },
30
29
  width: { type: ["number", "string"] },
@@ -35,17 +34,51 @@ validateProps(
35
34
 
36
35
  const { src, alt, title, width, zoom = true } = imageProps as Props;
37
36
  const zoomEnabled = zoom !== false;
38
- const resolvedSrc = resolveStaticAssetUrl(src);
39
37
  const rawWidth = width;
40
- const imageAttrs: Record<string, unknown> = { src: resolvedSrc };
41
- if (typeof alt === "string") {
42
- imageAttrs.alt = alt;
38
+
39
+ function normalizeImageSource(value: Props["src"]): {
40
+ light: string;
41
+ dark?: string;
42
+ } {
43
+ if (typeof value === "string") {
44
+ return { light: value };
45
+ }
46
+
47
+ const light = typeof value.light === "string" ? value.light : "";
48
+ const dark = typeof value.dark === "string" ? value.dark : undefined;
49
+
50
+ return { light, dark };
51
+ }
52
+
53
+ const normalizedSource = normalizeImageSource(src);
54
+ if (!normalizedSource.light.trim()) {
55
+ throw new Error(
56
+ `[USER_ERROR]: <Image>: Invalid prop "src": object form requires a non-empty "light" string (in ${Astro.url.pathname})`,
57
+ );
43
58
  }
44
- if (width !== undefined) {
45
- imageAttrs.width = width;
59
+
60
+ const resolvedLightSrc = resolveStaticAssetUrl(normalizedSource.light);
61
+ const resolvedDarkSrc =
62
+ typeof normalizedSource.dark === "string" && normalizedSource.dark.trim()
63
+ ? resolveStaticAssetUrl(normalizedSource.dark)
64
+ : undefined;
65
+ const hasDarkSource =
66
+ typeof resolvedDarkSrc === "string" && resolvedDarkSrc !== resolvedLightSrc;
67
+
68
+ function createImageAttrs(source: string): Record<string, unknown> {
69
+ const attrs: Record<string, unknown> = { src: source };
70
+ if (typeof alt === "string") {
71
+ attrs.alt = alt;
72
+ }
73
+ if (width !== undefined) {
74
+ attrs.width = width;
75
+ }
76
+ return attrs;
46
77
  }
47
78
 
48
- const zoomAttrs: Record<string, unknown> = { src: resolvedSrc };
79
+ const lightImageAttrs = createImageAttrs(resolvedLightSrc);
80
+ const darkImageAttrs = hasDarkSource ? createImageAttrs(resolvedDarkSrc!) : null;
81
+ const zoomAttrs: Record<string, unknown> = { src: resolvedLightSrc };
49
82
  if (typeof alt === "string") {
50
83
  zoomAttrs.alt = alt;
51
84
  }
@@ -83,15 +116,15 @@ function isConstrainedWidthValue(value: unknown): boolean {
83
116
 
84
117
  const hasCustomImageWidth = isConstrainedWidthValue(rawWidth);
85
118
 
86
- const captionHtml =
87
- typeof title === "string" && title.trim().length > 0
88
- ? (await renderMarkdown(title)).replace(/^<p>([\s\S]*)<\/p>\n?$/, "$1")
89
- : "";
119
+ const slotCaptionHtml = Astro.slots.has("default")
120
+ ? (await Astro.slots.render("default")).trim()
121
+ : "";
122
+ const hasCaption = slotCaptionHtml.length > 0;
90
123
  ---
91
124
 
92
125
  <figure
93
126
  class:list={[
94
- "rd-prose-block p-1.5 pb-1 xs:pb-1.5 group border border-neutral-200 dark:border-neutral-800 shadow-xs bg-neutral-50 dark:bg-(--rd-code-surface) rounded-2xl",
127
+ "rd-prose-block p-1.5 pb-1 xs:pb-1.5 group border-[0.5px] border-neutral-900/8 dark:border-white/6 bg-(--rd-code-surface) rounded-2xl shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-8px_rgba(0,0,0,0.08)] dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-8px_rgba(0,0,0,0.2)]",
95
128
  hasCustomImageWidth ? "w-fit max-w-full mx-auto" : "w-full",
96
129
  ]}
97
130
  x-data="{
@@ -102,8 +135,16 @@ const captionHtml =
102
135
  style: 'visibility: hidden;',
103
136
  fullShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
104
137
  noShadow: '0 0 0 rgba(0, 0, 0, 0)',
138
+ zoomSrc: '',
139
+ activeImage() {
140
+ const darkImg = this.$refs.darkImg;
141
+ if (darkImg && window.getComputedStyle(darkImg).display !== 'none') {
142
+ return darkImg;
143
+ }
144
+ return this.$refs.lightImg;
145
+ },
105
146
  computeZoomSize() {
106
- const img = this.$refs.img;
147
+ const img = this.activeImage();
107
148
  const zoomContainer = this.$refs.zoomViewport;
108
149
  if (!img || !zoomContainer) {
109
150
  this.zoomSize = `width: min(92vw, ${this.zoomMaxWidth}px);`;
@@ -138,9 +179,12 @@ const captionHtml =
138
179
  return this.zoomSize;
139
180
  },
140
181
  async zoom() {
182
+ const img = this.activeImage();
183
+ if (!img) return;
141
184
  // 1. Lock scroll and measure
142
185
  document.body.style.overflow = 'hidden';
143
- const rect = this.$refs.img.getBoundingClientRect();
186
+ this.zoomSrc = img.currentSrc || img.src;
187
+ const rect = img.getBoundingClientRect();
144
188
 
145
189
  // 2. Prepare the zoomed image (hidden but in DOM)
146
190
  this.style = 'opacity: 0; transition: none;';
@@ -176,7 +220,9 @@ const captionHtml =
176
220
  },
177
221
  close() {
178
222
  document.body.style.overflow = 'auto';
179
- const rect = this.$refs.img.getBoundingClientRect();
223
+ const img = this.activeImage();
224
+ if (!img) return;
225
+ const rect = img.getBoundingClientRect();
180
226
  const zRect = this.$refs.zoomedImg.getBoundingClientRect();
181
227
 
182
228
  const scale = rect.width / zRect.width;
@@ -194,25 +240,42 @@ const captionHtml =
194
240
  }"
195
241
  >
196
242
  <div
197
- class="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-(--rd-code-surface)"
243
+ class="overflow-hidden rounded-[11px]"
198
244
  >
199
245
  <img
200
- {...imageAttrs}
201
- x-ref="img"
246
+ {...lightImageAttrs}
247
+ x-ref="lightImg"
202
248
  title={title}
203
249
  class:list={[
204
250
  "h-auto my-0! block transition-opacity",
251
+ hasDarkSource && "dark:hidden",
205
252
  zoomEnabled && "cursor-zoom-in",
206
253
  hasCustomImageWidth ? "max-w-full" : "w-full",
207
254
  ]}
208
255
  :class="showZoomed ? 'opacity-0 duration-0' : 'opacity-100 duration-300'"
209
256
  @click={zoomEnabled ? "zoom()" : undefined}
210
257
  />
258
+ {
259
+ darkImageAttrs && (
260
+ <img
261
+ {...darkImageAttrs}
262
+ x-ref="darkImg"
263
+ title={title}
264
+ class:list={[
265
+ "h-auto my-0! hidden transition-opacity dark:block",
266
+ zoomEnabled && "cursor-zoom-in",
267
+ hasCustomImageWidth ? "max-w-full" : "w-full",
268
+ ]}
269
+ :class="showZoomed ? 'opacity-0 duration-0' : 'opacity-100 duration-300'"
270
+ @click={zoomEnabled ? "zoom()" : undefined}
271
+ />
272
+ )
273
+ }
211
274
  </div>
212
275
  {
213
- title && (
214
- <figcaption class="mt-1! xs:mt-1.5! text-center text-xs! xs:text-sm! text-neutral-500 dark:text-neutral-400 font-medium whitespace-pre-line leading-relaxed px-2">
215
- <span set:html={captionHtml} />
276
+ hasCaption && (
277
+ <figcaption class="prose-rules mt-1! xs:mt-1.5! max-w-none! *:max-w-none! text-center text-xs! xs:text-sm! text-neutral-500 dark:text-neutral-400 leading-relaxed px-2">
278
+ <Fragment set:html={slotCaptionHtml} />
216
279
  </figcaption>
217
280
  )
218
281
  }
@@ -243,6 +306,7 @@ const captionHtml =
243
306
  <img
244
307
  {...zoomAttrs}
245
308
  x-ref="zoomedImg"
309
+ :src="zoomSrc || $el.getAttribute('src')"
246
310
  class="relative z-10 max-w-full max-h-full object-contain rounded-2xl shadow-none will-change-transform pointer-events-none"
247
311
  :style="style"
248
312
  />
@@ -21,14 +21,14 @@ validateProps(
21
21
  <div
22
22
  class:list={[
23
23
  "relative pl-10 step-item pb-4 last:pb-0 space-y-4",
24
- "before:absolute before:left-[10.5px] before:top-8 before:bottom-0 before:w-px before:bg-linear-[transparent,var(--color-neutral-200)_10%,var(--color-neutral-200)_90%,transparent] dark:before:bg-linear-[transparent,var(--color-neutral-700)_10%,var(--color-neutral-700)_90%,transparent]",
24
+ "before:mask-b-from-90% before:absolute before:left-[13.25px] before:top-[28px] before:bottom-0 before:w-[1.5px] before:bg-neutral-900/8 dark:before:bg-neutral-50/8",
25
25
  ]}
26
26
  data-step-panel
27
27
  >
28
28
  <div
29
29
  class:list={[
30
30
  "flex items-center gap-1.5 not-prose",
31
- "step-number before:size-6 before:bg-linear-to-b before:from-neutral-900/80 before:to-neutral-900 dark:before:from-neutral-100 dark:before:to-neutral-200 before:rounded-full before:text-white before:flex before:items-center before:justify-center before:text-xs before:font-bold dark:before:font-extrabold before:absolute before:left-px before:top-[3px] before:shadow-sm dark:before:bg-neutral-200 dark:before:text-neutral-900",
31
+ "step-number before:leading-none before:size-7 before:bg-neutral-900/6 dark:before:bg-neutral-50/6 before:rounded-full before:text-neutral-700 dark:before:text-neutral-100 before:flex before:items-center before:justify-center before:text-[13px] before:font-medium before:absolute before:left-0 before:top-0",
32
32
  ]}
33
33
  >
34
34
  <h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
@@ -30,12 +30,33 @@ if (labels.length === 0) {
30
30
  previousTab: null,
31
31
  transitionDirection: 1,
32
32
  isTransitioning: false,
33
- transitionDurationMs: 300,
33
+ transitionDurationMs: 400,
34
+ transitionEasing: 'cubic-bezier(0.22, 1, 0.36, 1)',
34
35
  transitionTimeout: null,
35
36
  containerHeight: 'auto',
36
37
  markerStyle: { left: null, width: null },
37
38
  resizeHandler: null,
39
+ readMotionTokens() {
40
+ const styles = window.getComputedStyle(document.documentElement);
41
+ const configuredDurationMs = Number.parseFloat(
42
+ styles.getPropertyValue('--rd-panel-transition-duration-ms'),
43
+ );
44
+ this.transitionDurationMs = Number.isFinite(configuredDurationMs)
45
+ ? configuredDurationMs
46
+ : this.transitionDurationMs;
47
+ this.transitionEasing =
48
+ styles.getPropertyValue('--rd-panel-transition-easing').trim() ||
49
+ this.transitionEasing;
50
+
51
+ if (
52
+ typeof window.matchMedia === 'function' &&
53
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
54
+ ) {
55
+ this.transitionDurationMs = 0;
56
+ }
57
+ },
38
58
  init() {
59
+ this.readMotionTokens();
39
60
  this.resizeHandler = () => {
40
61
  this.updateMarker(this.activeTab);
41
62
  this.updateHeight(this.isTransitioning);
@@ -68,7 +89,7 @@ if (labels.length === 0) {
68
89
  this.isTransitioning = true;
69
90
  this.activeTab = index;
70
91
  this.updateMarker(this.activeTab);
71
- this.updateHeight(true);
92
+ this.updateHeight();
72
93
 
73
94
  this.transitionTimeout = window.setTimeout(() => {
74
95
  this.isTransitioning = false;
@@ -87,7 +108,7 @@ if (labels.length === 0) {
87
108
  }
88
109
  },
89
110
  getPanelStyle(index) {
90
- const base = 'position: absolute; inset: 0;';
111
+ const base = 'position: absolute; top: 0; left: 0; right: 0; width: 100%;';
91
112
 
92
113
  const isActive = index === this.activeTab;
93
114
  const isPrevious = this.isTransitioning && index === this.previousTab;
@@ -105,29 +126,23 @@ if (labels.length === 0) {
105
126
  this.transitionDirection === 1
106
127
  ? 'rd-tabs-slide-in-from-right'
107
128
  : 'rd-tabs-slide-in-from-left';
108
- return `${base} opacity: 1; pointer-events: auto; visibility: visible; z-index: 2; animation: ${animationName} ${this.transitionDurationMs}ms ease-in-out both;`;
129
+ return `${base} opacity: 1; pointer-events: auto; visibility: visible; z-index: 2; animation: ${animationName} ${this.transitionDurationMs}ms ${this.transitionEasing} both;`;
109
130
  }
110
131
 
111
132
  const animationName =
112
133
  this.transitionDirection === 1
113
134
  ? 'rd-tabs-slide-out-to-left'
114
135
  : 'rd-tabs-slide-out-to-right';
115
- return `${base} opacity: 1; pointer-events: none; visibility: visible; z-index: 1; animation: ${animationName} ${this.transitionDurationMs}ms ease-in-out both;`;
136
+ return `${base} opacity: 1; pointer-events: none; visibility: visible; z-index: 1; animation: ${animationName} ${this.transitionDurationMs}ms ${this.transitionEasing} both;`;
116
137
  },
117
- updateHeight(includePrevious = false) {
138
+ updateHeight() {
118
139
  this.$nextTick(() => {
119
140
  const activeSlide = this.$refs['content-' + this.activeTab];
120
141
  if (!activeSlide) return;
142
+ const activeContent = activeSlide.querySelector('[data-rd-tabs-panel-content]');
143
+ const measuredElement = activeContent || activeSlide;
121
144
 
122
- let nextHeight = activeSlide.scrollHeight;
123
- if (includePrevious && this.previousTab !== null) {
124
- const previousSlide = this.$refs['content-' + this.previousTab];
125
- if (previousSlide) {
126
- nextHeight = Math.max(nextHeight, previousSlide.scrollHeight);
127
- }
128
- }
129
-
130
- this.containerHeight = nextHeight + 'px';
145
+ this.containerHeight = measuredElement.scrollHeight + 'px';
131
146
  });
132
147
  }
133
148
  }"
@@ -167,19 +182,28 @@ class="rd-prose-block">
167
182
  </ul>
168
183
 
169
184
  <div
170
- class="relative mt-4 overflow-hidden transition-[height] duration-300 ease-in-out"
171
- :style="'height: ' + containerHeight"
185
+ class="relative mt-4 overflow-visible transition-[height] motion-reduce:transition-none"
186
+ :style="'height: ' + containerHeight + '; transition-duration: ' + transitionDurationMs + 'ms; transition-timing-function: ' + transitionEasing + ';'"
172
187
  >
173
- { tabContents.map((content, index) => (
174
- <div
175
- {...(index !== 0 ? { "x-cloak": true } : {})}
176
- x-ref={`content-${index}`}
177
- class="prose-rules w-full max-w-none! *:max-w-none!"
178
- :style={`getPanelStyle(${index})`}
179
- style={index === 0 ? 'position: relative;' : ''}
180
- set:html={content}
181
- />
182
- )) }
188
+ <div class="relative -mx-4 h-full overflow-x-clip overflow-y-visible">
189
+ <div class="relative mx-4 h-full">
190
+ { tabContents.map((content, index) => (
191
+ <div
192
+ {...(index !== 0 ? { "x-cloak": true } : {})}
193
+ x-ref={`content-${index}`}
194
+ class="w-full"
195
+ :style={`getPanelStyle(${index})`}
196
+ style={index === 0 ? 'position: relative;' : ''}
197
+ >
198
+ <div
199
+ data-rd-tabs-panel-content
200
+ class="prose-rules w-full max-w-none! *:max-w-none!"
201
+ set:html={content}
202
+ />
203
+ </div>
204
+ )) }
205
+ </div>
206
+ </div>
183
207
  </div>
184
208
  </div>
185
209
 
@@ -187,36 +211,44 @@ class="rd-prose-block">
187
211
  @keyframes rd-tabs-slide-in-from-right {
188
212
  from {
189
213
  transform: translateX(100%);
214
+ opacity: 0;
190
215
  }
191
216
  to {
192
217
  transform: translateX(0);
218
+ opacity: 1;
193
219
  }
194
220
  }
195
221
 
196
222
  @keyframes rd-tabs-slide-in-from-left {
197
223
  from {
198
224
  transform: translateX(-100%);
225
+ opacity: 0;
199
226
  }
200
227
  to {
201
228
  transform: translateX(0);
229
+ opacity: 1;
202
230
  }
203
231
  }
204
232
 
205
233
  @keyframes rd-tabs-slide-out-to-left {
206
234
  from {
207
235
  transform: translateX(0);
236
+ opacity: 1;
208
237
  }
209
238
  to {
210
239
  transform: translateX(-100%);
240
+ opacity: 0;
211
241
  }
212
242
  }
213
243
 
214
244
  @keyframes rd-tabs-slide-out-to-right {
215
245
  from {
216
246
  transform: translateX(0);
247
+ opacity: 1;
217
248
  }
218
249
  to {
219
250
  transform: translateX(100%);
251
+ opacity: 0;
220
252
  }
221
253
  }
222
254
  </style>
@@ -7,6 +7,12 @@ import {
7
7
  type ThemedToken,
8
8
  } from "shiki";
9
9
  import { DEFAULT_FILE, getIconForFile } from "vscode-icons-js";
10
+ import { getConfig } from "../validation";
11
+ import {
12
+ DEFAULT_SHIKI_DARK_THEME,
13
+ DEFAULT_SHIKI_LIGHT_THEME,
14
+ type CodeSyntaxThemeByMode,
15
+ } from "./shiki-theme-config";
10
16
 
11
17
  export const DEFAULT_CODE_BLOCK_LANGUAGE = "plaintext";
12
18
 
@@ -137,9 +143,8 @@ const CODE_BLOCK_LANGUAGE_ICON_FILE_BY_VALUE: Record<string, string> = {
137
143
  yaml: "file_type_yaml_official.svg",
138
144
  };
139
145
 
140
- const SHIKI_LIGHT_THEME = "github-light";
141
- const SHIKI_DARK_THEME = "github-dark";
142
- const SHIKI_THEMES = [SHIKI_LIGHT_THEME, SHIKI_DARK_THEME] as const;
146
+ const SHIKI_LIGHT_LINE_HIGHLIGHT_FALLBACK = "#f6f8fa";
147
+ const SHIKI_DARK_LINE_HIGHLIGHT_FALLBACK = "#2b3036";
143
148
  const BUNDLED_LANGUAGE_SET = new Set(Object.keys(bundledLanguages));
144
149
  const LANGUAGE_RUNTIME_DEPENDENCIES: Record<string, string[]> = {
145
150
  // MDX tokenization relies on TSX grammar injections for JSX-style tags.
@@ -159,6 +164,7 @@ let iconFileNameSetPromise: Promise<Set<string>> | null = null;
159
164
  let highlighterPromise:
160
165
  | Promise<Awaited<ReturnType<typeof getSingletonHighlighter>>>
161
166
  | null = null;
167
+ let highlighterThemeKey = "";
162
168
  const loadedLanguageSet = new Set<string>([DEFAULT_CODE_BLOCK_LANGUAGE]);
163
169
  const languageLoadPromiseByName = new Map<string, Promise<void>>();
164
170
 
@@ -371,10 +377,30 @@ function namespaceSvgIds(svg: string, namespace: string): string {
371
377
  return namespacedSvg;
372
378
  }
373
379
 
374
- async function getHighlighter() {
375
- if (!highlighterPromise) {
380
+ async function getConfiguredCodeSyntaxThemes(): Promise<CodeSyntaxThemeByMode> {
381
+ const config = await getConfig();
382
+ const configuredSyntaxTheme = config.theme?.code?.syntaxTheme;
383
+
384
+ if (typeof configuredSyntaxTheme === "string") {
385
+ return {
386
+ light: configuredSyntaxTheme,
387
+ dark: configuredSyntaxTheme,
388
+ };
389
+ }
390
+
391
+ return {
392
+ light: configuredSyntaxTheme?.light ?? DEFAULT_SHIKI_LIGHT_THEME,
393
+ dark: configuredSyntaxTheme?.dark ?? DEFAULT_SHIKI_DARK_THEME,
394
+ };
395
+ }
396
+
397
+ async function getHighlighter(syntaxThemes: CodeSyntaxThemeByMode) {
398
+ const nextThemeKey = `${syntaxThemes.light}\u0000${syntaxThemes.dark}`;
399
+
400
+ if (!highlighterPromise || highlighterThemeKey !== nextThemeKey) {
401
+ highlighterThemeKey = nextThemeKey;
376
402
  highlighterPromise = getSingletonHighlighter({
377
- themes: [...SHIKI_THEMES],
403
+ themes: Array.from(new Set([syntaxThemes.light, syntaxThemes.dark])),
378
404
  langs: [DEFAULT_CODE_BLOCK_LANGUAGE],
379
405
  });
380
406
  }
@@ -382,6 +408,40 @@ async function getHighlighter() {
382
408
  return highlighterPromise;
383
409
  }
384
410
 
411
+ function getThemeColor(
412
+ highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
413
+ themeName: string,
414
+ colorName: string,
415
+ fallback: string,
416
+ ): string {
417
+ const colorValue = highlighter.getTheme(themeName)?.colors?.[colorName];
418
+ return typeof colorValue === "string" && colorValue.trim().length > 0
419
+ ? colorValue.trim()
420
+ : fallback;
421
+ }
422
+
423
+ function getCodeThemeColors(
424
+ highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
425
+ syntaxThemes: CodeSyntaxThemeByMode,
426
+ ): CodeThemeColors {
427
+ return {
428
+ lineHighlightBackground: {
429
+ light: getThemeColor(
430
+ highlighter,
431
+ syntaxThemes.light,
432
+ "editor.lineHighlightBackground",
433
+ SHIKI_LIGHT_LINE_HIGHLIGHT_FALLBACK,
434
+ ),
435
+ dark: getThemeColor(
436
+ highlighter,
437
+ syntaxThemes.dark,
438
+ "editor.lineHighlightBackground",
439
+ SHIKI_DARK_LINE_HIGHLIGHT_FALLBACK,
440
+ ),
441
+ },
442
+ };
443
+ }
444
+
385
445
  function resolveLoadableLanguage(
386
446
  highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
387
447
  normalizedLanguage: string,
@@ -496,8 +556,11 @@ export async function getCodeLineTokens({
496
556
  }): Promise<{
497
557
  normalizedLanguage: string;
498
558
  lines: CodeLineToken[][];
559
+ themeColors: CodeThemeColors;
499
560
  }> {
500
- const highlighter = await getHighlighter();
561
+ const syntaxThemes = await getConfiguredCodeSyntaxThemes();
562
+ const highlighter = await getHighlighter(syntaxThemes);
563
+ const themeColors = getCodeThemeColors(highlighter, syntaxThemes);
501
564
  const normalizedLanguage = normalizeCodeLanguageValue(language);
502
565
  const loadableLanguage = resolveLoadableLanguage(
503
566
  highlighter,
@@ -525,26 +588,41 @@ export async function getCodeLineTokens({
525
588
  }
526
589
 
527
590
  try {
528
- const themedTokenLines = getThemedTokenLines(highlighter, code, targetLanguage);
591
+ const themedTokenLines = getThemedTokenLines(
592
+ highlighter,
593
+ code,
594
+ targetLanguage,
595
+ syntaxThemes,
596
+ );
529
597
 
530
598
  return {
531
599
  normalizedLanguage: targetLanguage,
532
600
  lines: mergeTokenLines(themedTokenLines.light, themedTokenLines.dark),
601
+ themeColors,
533
602
  };
534
603
  } catch {
535
604
  const themedTokenLines = getThemedTokenLines(
536
605
  highlighter,
537
606
  code,
538
607
  DEFAULT_CODE_BLOCK_LANGUAGE,
608
+ syntaxThemes,
539
609
  );
540
610
 
541
611
  return {
542
612
  normalizedLanguage: DEFAULT_CODE_BLOCK_LANGUAGE,
543
613
  lines: mergeTokenLines(themedTokenLines.light, themedTokenLines.dark),
614
+ themeColors,
544
615
  };
545
616
  }
546
617
  }
547
618
 
619
+ export type CodeThemeColors = {
620
+ lineHighlightBackground: {
621
+ light: string;
622
+ dark: string;
623
+ };
624
+ };
625
+
548
626
  export type CodeLineToken = {
549
627
  content: string;
550
628
  color?: string;
@@ -560,6 +638,7 @@ function getThemedTokenLines(
560
638
  highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
561
639
  code: string,
562
640
  lang: string,
641
+ syntaxThemes: CodeSyntaxThemeByMode,
563
642
  ): {
564
643
  light: ThemedToken[][];
565
644
  dark: ThemedToken[][];
@@ -567,11 +646,11 @@ function getThemedTokenLines(
567
646
  const shikiLanguage = lang as keyof typeof bundledLanguages;
568
647
  const lightTokenResult = highlighter.codeToTokens(code, {
569
648
  lang: shikiLanguage,
570
- theme: SHIKI_LIGHT_THEME,
649
+ theme: syntaxThemes.light,
571
650
  });
572
651
  const darkTokenResult = highlighter.codeToTokens(code, {
573
652
  lang: shikiLanguage,
574
- theme: SHIKI_DARK_THEME,
653
+ theme: syntaxThemes.dark,
575
654
  });
576
655
 
577
656
  return {
@@ -0,0 +1,16 @@
1
+ import { bundledThemes } from "shiki";
2
+
3
+ export const DEFAULT_SHIKI_LIGHT_THEME = "github-light";
4
+ export const DEFAULT_SHIKI_DARK_THEME = "github-dark";
5
+
6
+ export type CodeSyntaxThemeByMode = {
7
+ light: string;
8
+ dark: string;
9
+ };
10
+
11
+ export const SHIKI_BUNDLED_THEME_NAMES = Object.keys(bundledThemes).sort();
12
+ const SHIKI_BUNDLED_THEME_NAME_SET = new Set(SHIKI_BUNDLED_THEME_NAMES);
13
+
14
+ export function isBundledShikiThemeName(value: string): boolean {
15
+ return SHIKI_BUNDLED_THEME_NAME_SET.has(value);
16
+ }