radiant-docs 0.1.37 → 0.1.39
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/package.json +1 -1
- package/template/astro.config.mjs +2 -0
- package/template/src/components/Footer.astro +1 -1
- package/template/src/components/Header.astro +8 -8
- package/template/src/components/OpenApiPage.astro +18 -18
- package/template/src/components/Search.astro +18 -18
- package/template/src/components/Sidebar.astro +4 -2
- package/template/src/components/SidebarDropdown.astro +82 -79
- package/template/src/components/SidebarSegmented.astro +5 -5
- package/template/src/components/TableOfContents.astro +24 -15
- package/template/src/components/ThemeSwitcher.astro +15 -8
- package/template/src/components/chat/AskAiWidget.tsx +4 -3
- package/template/src/components/endpoint/PlaygroundBar.astro +3 -3
- package/template/src/components/endpoint/PlaygroundButton.astro +2 -2
- package/template/src/components/endpoint/PlaygroundField.astro +53 -53
- package/template/src/components/endpoint/PlaygroundForm.astro +38 -22
- package/template/src/components/endpoint/RequestSnippets.astro +54 -21
- package/template/src/components/endpoint/ResponseDisplay.astro +24 -24
- package/template/src/components/endpoint/ResponseFieldTree.astro +12 -12
- package/template/src/components/endpoint/ResponseFields.astro +19 -19
- package/template/src/components/endpoint/ResponseSnippets.astro +66 -29
- package/template/src/components/ui/CodeTabEdge.astro +6 -4
- package/template/src/components/ui/Field.astro +7 -7
- package/template/src/components/ui/demo/Demo.astro +1 -1
- package/template/src/components/user/Accordion.astro +3 -3
- package/template/src/components/user/Callout.astro +8 -8
- package/template/src/components/user/CodeBlock.astro +63 -25
- package/template/src/components/user/CodeGroup.astro +259 -22
- package/template/src/components/user/ComponentPreviewBlock.astro +36 -10
- package/template/src/components/user/Image.astro +2 -2
- package/template/src/components/user/Step.astro +4 -4
- package/template/src/components/user/Tab.astro +1 -1
- package/template/src/components/user/Tabs.astro +142 -42
- package/template/src/layouts/Layout.astro +1 -1
- package/template/src/lib/code/code-block.ts +150 -15
- package/template/src/lib/mdx/remark-resolve-internal-links.ts +639 -0
- package/template/src/pages/404.astro +44 -0
- package/template/src/styles/global.css +51 -19
|
@@ -101,7 +101,7 @@ validateProps(
|
|
|
101
101
|
|
|
102
102
|
<figure
|
|
103
103
|
class:list={[
|
|
104
|
-
"p-1.5 pb-1 xs:pb-1.5 group border border-neutral-200
|
|
104
|
+
"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",
|
|
105
105
|
hasCustomImageWidth ? "w-fit max-w-full mx-auto" : "w-full",
|
|
106
106
|
]}
|
|
107
107
|
x-data="{
|
|
@@ -204,7 +204,7 @@ validateProps(
|
|
|
204
204
|
}"
|
|
205
205
|
>
|
|
206
206
|
<div
|
|
207
|
-
class="overflow-hidden rounded-xl border border-neutral-200
|
|
207
|
+
class="overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-(--rd-code-surface)"
|
|
208
208
|
>
|
|
209
209
|
<img
|
|
210
210
|
{...attrs}
|
|
@@ -14,24 +14,24 @@ validateProps(
|
|
|
14
14
|
{
|
|
15
15
|
title: { required: true, type: "string" },
|
|
16
16
|
},
|
|
17
|
-
Astro.url.pathname
|
|
17
|
+
Astro.url.pathname,
|
|
18
18
|
);
|
|
19
19
|
---
|
|
20
20
|
|
|
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]",
|
|
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]",
|
|
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:bg-neutral-900 before:
|
|
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",
|
|
32
32
|
]}
|
|
33
33
|
>
|
|
34
|
-
<h3 class="text-lg font-semibold text-neutral-900">
|
|
34
|
+
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
|
35
35
|
{title}
|
|
36
36
|
</h3>
|
|
37
37
|
</div>
|
|
@@ -3,16 +3,18 @@ import Icon from '../ui/Icon.astro';
|
|
|
3
3
|
|
|
4
4
|
const html = await Astro.slots.render("default");
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const tabRegex = /<section\b([^>]*)\bdata-label="([^"]*)"([^>]*)>(.*?)<\/section>/gs;
|
|
7
|
+
const dataIconRegex = /\bdata-icon="([^"]*)"/;
|
|
8
8
|
let labels = [];
|
|
9
9
|
let icons = [];
|
|
10
|
+
let tabContents = [];
|
|
10
11
|
let match;
|
|
11
|
-
while ((match =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
icons.push(
|
|
12
|
+
while ((match = tabRegex.exec(html)) !== null) {
|
|
13
|
+
const openingAttributes = `${match[1]} ${match[3]}`;
|
|
14
|
+
const iconMatch = openingAttributes.match(dataIconRegex);
|
|
15
|
+
labels.push(match[2]);
|
|
16
|
+
icons.push(iconMatch?.[1] ?? "");
|
|
17
|
+
tabContents.push(match[4]);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
if (labels.length === 0) {
|
|
@@ -21,28 +23,59 @@ if (labels.length === 0) {
|
|
|
21
23
|
`[USER_ERROR]: <Tabs>: Must contain at least two <Tab> children (in ${pagePath}.mdx)`
|
|
22
24
|
);
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
const tabRegex = /<section[^>]*data-label="[^"]*"[^>]*>(.*?)<\/section>/gs;
|
|
26
|
-
let tabContents = [];
|
|
27
|
-
let contentMatch;
|
|
28
|
-
while ((contentMatch = tabRegex.exec(html)) !== null) {
|
|
29
|
-
tabContents.push(contentMatch[1]);
|
|
30
|
-
}
|
|
31
26
|
---
|
|
32
27
|
|
|
33
28
|
<div x-data="{
|
|
34
29
|
activeTab: 0,
|
|
30
|
+
previousTab: null,
|
|
31
|
+
transitionDirection: 1,
|
|
32
|
+
isTransitioning: false,
|
|
33
|
+
transitionDurationMs: 300,
|
|
34
|
+
transitionTimeout: null,
|
|
35
35
|
containerHeight: 'auto',
|
|
36
36
|
markerStyle: { left: null, width: null },
|
|
37
|
+
resizeHandler: null,
|
|
37
38
|
init() {
|
|
39
|
+
this.resizeHandler = () => {
|
|
40
|
+
this.updateMarker(this.activeTab);
|
|
41
|
+
this.updateHeight(this.isTransitioning);
|
|
42
|
+
};
|
|
43
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
44
|
+
|
|
38
45
|
this.$nextTick(() => {
|
|
39
46
|
this.updateMarker(this.activeTab);
|
|
40
47
|
this.updateHeight();
|
|
41
48
|
});
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
},
|
|
50
|
+
destroy() {
|
|
51
|
+
if (this.resizeHandler) {
|
|
52
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
53
|
+
}
|
|
54
|
+
if (this.transitionTimeout) {
|
|
55
|
+
window.clearTimeout(this.transitionTimeout);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
selectTab(index) {
|
|
59
|
+
if (index === this.activeTab) return;
|
|
60
|
+
|
|
61
|
+
if (this.transitionTimeout) {
|
|
62
|
+
window.clearTimeout(this.transitionTimeout);
|
|
63
|
+
this.transitionTimeout = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.previousTab = this.activeTab;
|
|
67
|
+
this.transitionDirection = index > this.activeTab ? 1 : -1;
|
|
68
|
+
this.isTransitioning = true;
|
|
69
|
+
this.activeTab = index;
|
|
70
|
+
this.updateMarker(this.activeTab);
|
|
71
|
+
this.updateHeight(true);
|
|
72
|
+
|
|
73
|
+
this.transitionTimeout = window.setTimeout(() => {
|
|
74
|
+
this.isTransitioning = false;
|
|
75
|
+
this.previousTab = null;
|
|
76
|
+
this.transitionTimeout = null;
|
|
44
77
|
this.updateHeight();
|
|
45
|
-
});
|
|
78
|
+
}, this.transitionDurationMs);
|
|
46
79
|
},
|
|
47
80
|
updateMarker(index) {
|
|
48
81
|
const el = this.$refs['tab-' + index];
|
|
@@ -53,23 +86,57 @@ while ((contentMatch = tabRegex.exec(html)) !== null) {
|
|
|
53
86
|
};
|
|
54
87
|
}
|
|
55
88
|
},
|
|
56
|
-
|
|
89
|
+
getPanelStyle(index) {
|
|
90
|
+
const base = 'position: absolute; inset: 0;';
|
|
91
|
+
|
|
92
|
+
const isActive = index === this.activeTab;
|
|
93
|
+
const isPrevious = this.isTransitioning && index === this.previousTab;
|
|
94
|
+
|
|
95
|
+
if (!isActive && !isPrevious) {
|
|
96
|
+
return `${base} opacity: 0; pointer-events: none; visibility: hidden; z-index: 0;`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!this.isTransitioning) {
|
|
100
|
+
return 'position: relative; transform: translateX(0); opacity: 1; pointer-events: auto; visibility: visible; z-index: 1;';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isActive) {
|
|
104
|
+
const animationName =
|
|
105
|
+
this.transitionDirection === 1
|
|
106
|
+
? 'rd-tabs-slide-in-from-right'
|
|
107
|
+
: '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;`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const animationName =
|
|
112
|
+
this.transitionDirection === 1
|
|
113
|
+
? 'rd-tabs-slide-out-to-left'
|
|
114
|
+
: '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;`;
|
|
116
|
+
},
|
|
117
|
+
updateHeight(includePrevious = false) {
|
|
57
118
|
this.$nextTick(() => {
|
|
58
|
-
// We look for the internal wrapper or the content div specifically
|
|
59
119
|
const activeSlide = this.$refs['content-' + this.activeTab];
|
|
60
|
-
if (activeSlide)
|
|
61
|
-
|
|
62
|
-
|
|
120
|
+
if (!activeSlide) return;
|
|
121
|
+
|
|
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
|
+
}
|
|
63
128
|
}
|
|
129
|
+
|
|
130
|
+
this.containerHeight = nextHeight + 'px';
|
|
64
131
|
});
|
|
65
132
|
}
|
|
66
133
|
}"
|
|
67
134
|
class="my-5">
|
|
68
135
|
<ul
|
|
69
|
-
class="relative isolate not-prose flex border border-neutral-
|
|
136
|
+
class="relative isolate not-prose flex w-full max-w-full min-w-0 rounded-lg border border-neutral-100 bg-neutral-100/80 p-[3px] dark:border-none dark:bg-neutral-800/50"
|
|
70
137
|
>
|
|
71
138
|
<div
|
|
72
|
-
class="absolute top-[3px] bottom-[3px]
|
|
139
|
+
class="absolute top-[3px] bottom-[3px] -z-10 flex items-center justify-center rounded-md bg-white shadow-sm transition-all duration-300 ease-out dark:bg-neutral-700/30 dark:border dark:border-neutral-700/40 dark:shadow-black/30."
|
|
73
140
|
style="left: 3px;"
|
|
74
141
|
:style="markerStyle.width ? `left: ${markerStyle.left}; width: ${markerStyle.width}` : ''"
|
|
75
142
|
>
|
|
@@ -79,44 +146,77 @@ class="my-5">
|
|
|
79
146
|
</div>
|
|
80
147
|
|
|
81
148
|
{ labels.map((label, index) => (
|
|
82
|
-
<li>
|
|
149
|
+
<li class="min-w-0 flex-1">
|
|
83
150
|
<button
|
|
84
151
|
type="button"
|
|
85
152
|
x-ref={`tab-${index}`}
|
|
86
|
-
@click={`
|
|
87
|
-
class="relative
|
|
153
|
+
@click={`selectTab(${index})`}
|
|
154
|
+
class="relative flex h-[32px] w-full min-w-0 max-w-full cursor-pointer items-center gap-2 px-3 text-sm font-medium transition-colors duration-200"
|
|
88
155
|
style={index === 0 ? "" : ""}
|
|
89
|
-
class:list={[index === 0 ? "text-
|
|
156
|
+
class:list={[index === 0 ? "text-foreground" : "text-muted-foreground"]}
|
|
90
157
|
:class={`{
|
|
91
|
-
'text-
|
|
92
|
-
'text-
|
|
158
|
+
'text-foreground': activeTab === ${index},
|
|
159
|
+
'text-muted-foreground hover:text-foreground': activeTab !== ${index}
|
|
93
160
|
}`}
|
|
94
161
|
>
|
|
95
162
|
{icons[index] && <Icon name={icons[index]} class="size-4 shrink-0" />}
|
|
96
|
-
{label}
|
|
163
|
+
<span class="min-w-0 flex-1 truncate" title={label}>{label}</span>
|
|
97
164
|
</button>
|
|
98
165
|
</li>
|
|
99
166
|
)) }
|
|
100
167
|
</ul>
|
|
101
168
|
|
|
102
169
|
<div
|
|
103
|
-
class="mt-4 overflow-hidden transition-[height] duration-300 ease-in-out"
|
|
170
|
+
class="relative mt-4 overflow-hidden transition-[height] duration-300 ease-in-out"
|
|
104
171
|
:style="'height: ' + containerHeight"
|
|
105
172
|
>
|
|
106
|
-
<div
|
|
107
|
-
class="flex items-start transition-transform duration-300 ease-in-out"
|
|
108
|
-
:style="'transform: translateX(-' + (activeTab * 100) + '%)'"
|
|
109
|
-
>
|
|
110
173
|
{ tabContents.map((content, index) => (
|
|
111
|
-
// We add a ref here so we can measure the height
|
|
112
174
|
<div
|
|
175
|
+
{...(index !== 0 ? { "x-cloak": true } : {})}
|
|
113
176
|
x-ref={`content-${index}`}
|
|
114
|
-
class="w-full
|
|
115
|
-
:style={`
|
|
116
|
-
style={index === 0 ? '
|
|
177
|
+
class="w-full"
|
|
178
|
+
:style={`getPanelStyle(${index})`}
|
|
179
|
+
style={index === 0 ? 'position: relative;' : ''}
|
|
117
180
|
set:html={content}
|
|
118
181
|
/>
|
|
119
182
|
)) }
|
|
120
|
-
</div>
|
|
121
183
|
</div>
|
|
122
|
-
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<style>
|
|
187
|
+
@keyframes rd-tabs-slide-in-from-right {
|
|
188
|
+
from {
|
|
189
|
+
transform: translateX(100%);
|
|
190
|
+
}
|
|
191
|
+
to {
|
|
192
|
+
transform: translateX(0);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@keyframes rd-tabs-slide-in-from-left {
|
|
197
|
+
from {
|
|
198
|
+
transform: translateX(-100%);
|
|
199
|
+
}
|
|
200
|
+
to {
|
|
201
|
+
transform: translateX(0);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@keyframes rd-tabs-slide-out-to-left {
|
|
206
|
+
from {
|
|
207
|
+
transform: translateX(0);
|
|
208
|
+
}
|
|
209
|
+
to {
|
|
210
|
+
transform: translateX(-100%);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@keyframes rd-tabs-slide-out-to-right {
|
|
215
|
+
from {
|
|
216
|
+
transform: translateX(0);
|
|
217
|
+
}
|
|
218
|
+
to {
|
|
219
|
+
transform: translateX(100%);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
</style>
|
|
@@ -180,7 +180,7 @@ if (isDev && hasAskAiDevConfig) {
|
|
|
180
180
|
class="fixed top-1 inset-x-1 h-16 -z-10 bg-background-dark"
|
|
181
181
|
data-vaul-scale-chrome
|
|
182
182
|
>
|
|
183
|
-
<div class="bg-
|
|
183
|
+
<div class="bg-background w-full h-full rounded-t-2xl"></div>
|
|
184
184
|
</div>
|
|
185
185
|
<div
|
|
186
186
|
class="fixed top-[63px] z-30 w-[5px] right-0 bottom-0 bg-background-dark border-l border-l-border"
|
|
@@ -137,7 +137,9 @@ const CODE_BLOCK_LANGUAGE_ICON_FILE_BY_VALUE: Record<string, string> = {
|
|
|
137
137
|
yaml: "file_type_yaml_official.svg",
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
const
|
|
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;
|
|
141
143
|
const BUNDLED_LANGUAGE_SET = new Set(Object.keys(bundledLanguages));
|
|
142
144
|
const LANGUAGE_RUNTIME_DEPENDENCIES: Record<string, string[]> = {
|
|
143
145
|
// MDX tokenization relies on TSX grammar injections for JSX-style tags.
|
|
@@ -228,7 +230,7 @@ function pickIconFromFileName(fileName: string): string | null {
|
|
|
228
230
|
const candidates = uniqueValues([trimmedFileName, trimmedFileName.toLowerCase()]);
|
|
229
231
|
for (const candidate of candidates) {
|
|
230
232
|
const iconFileName = getIconForFile(candidate);
|
|
231
|
-
if (iconFileName !== DEFAULT_FILE) return iconFileName;
|
|
233
|
+
if (iconFileName && iconFileName !== DEFAULT_FILE) return iconFileName;
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
return null;
|
|
@@ -372,7 +374,7 @@ function namespaceSvgIds(svg: string, namespace: string): string {
|
|
|
372
374
|
async function getHighlighter() {
|
|
373
375
|
if (!highlighterPromise) {
|
|
374
376
|
highlighterPromise = getSingletonHighlighter({
|
|
375
|
-
themes: [
|
|
377
|
+
themes: [...SHIKI_THEMES],
|
|
376
378
|
langs: [DEFAULT_CODE_BLOCK_LANGUAGE],
|
|
377
379
|
});
|
|
378
380
|
}
|
|
@@ -414,7 +416,7 @@ async function ensureLanguageLoaded(
|
|
|
414
416
|
}
|
|
415
417
|
|
|
416
418
|
const loadPromise = highlighter
|
|
417
|
-
.loadLanguage(language)
|
|
419
|
+
.loadLanguage(language as keyof typeof bundledLanguages)
|
|
418
420
|
.then(() => {
|
|
419
421
|
loadedLanguageSet.add(language);
|
|
420
422
|
})
|
|
@@ -493,7 +495,7 @@ export async function getCodeLineTokens({
|
|
|
493
495
|
language: string;
|
|
494
496
|
}): Promise<{
|
|
495
497
|
normalizedLanguage: string;
|
|
496
|
-
lines:
|
|
498
|
+
lines: CodeLineToken[][];
|
|
497
499
|
}> {
|
|
498
500
|
const highlighter = await getHighlighter();
|
|
499
501
|
const normalizedLanguage = normalizeCodeLanguageValue(language);
|
|
@@ -523,24 +525,157 @@ export async function getCodeLineTokens({
|
|
|
523
525
|
}
|
|
524
526
|
|
|
525
527
|
try {
|
|
526
|
-
const
|
|
527
|
-
lang: targetLanguage,
|
|
528
|
-
theme: SHIKI_THEME,
|
|
529
|
-
});
|
|
528
|
+
const themedTokenLines = getThemedTokenLines(highlighter, code, targetLanguage);
|
|
530
529
|
|
|
531
530
|
return {
|
|
532
531
|
normalizedLanguage: targetLanguage,
|
|
533
|
-
lines:
|
|
532
|
+
lines: mergeTokenLines(themedTokenLines.light, themedTokenLines.dark),
|
|
534
533
|
};
|
|
535
534
|
} catch {
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
535
|
+
const themedTokenLines = getThemedTokenLines(
|
|
536
|
+
highlighter,
|
|
537
|
+
code,
|
|
538
|
+
DEFAULT_CODE_BLOCK_LANGUAGE,
|
|
539
|
+
);
|
|
540
540
|
|
|
541
541
|
return {
|
|
542
542
|
normalizedLanguage: DEFAULT_CODE_BLOCK_LANGUAGE,
|
|
543
|
-
lines:
|
|
543
|
+
lines: mergeTokenLines(themedTokenLines.light, themedTokenLines.dark),
|
|
544
544
|
};
|
|
545
545
|
}
|
|
546
546
|
}
|
|
547
|
+
|
|
548
|
+
export type CodeLineToken = {
|
|
549
|
+
content: string;
|
|
550
|
+
color?: string;
|
|
551
|
+
darkColor?: string;
|
|
552
|
+
bgColor?: string;
|
|
553
|
+
darkBgColor?: string;
|
|
554
|
+
fontStyle?: number;
|
|
555
|
+
htmlStyle?: Record<string, string>;
|
|
556
|
+
darkHtmlStyle?: Record<string, string>;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
function getThemedTokenLines(
|
|
560
|
+
highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
|
|
561
|
+
code: string,
|
|
562
|
+
lang: string,
|
|
563
|
+
): {
|
|
564
|
+
light: ThemedToken[][];
|
|
565
|
+
dark: ThemedToken[][];
|
|
566
|
+
} {
|
|
567
|
+
const shikiLanguage = lang as keyof typeof bundledLanguages;
|
|
568
|
+
const lightTokenResult = highlighter.codeToTokens(code, {
|
|
569
|
+
lang: shikiLanguage,
|
|
570
|
+
theme: SHIKI_LIGHT_THEME,
|
|
571
|
+
});
|
|
572
|
+
const darkTokenResult = highlighter.codeToTokens(code, {
|
|
573
|
+
lang: shikiLanguage,
|
|
574
|
+
theme: SHIKI_DARK_THEME,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
light: lightTokenResult.tokens,
|
|
579
|
+
dark: darkTokenResult.tokens,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function buildLightOnlyTokenLine(lightLine: ThemedToken[]): CodeLineToken[] {
|
|
584
|
+
return lightLine.map((token) => ({
|
|
585
|
+
content: token.content,
|
|
586
|
+
color: token.color,
|
|
587
|
+
bgColor: token.bgColor,
|
|
588
|
+
fontStyle: token.fontStyle,
|
|
589
|
+
htmlStyle: token.htmlStyle,
|
|
590
|
+
}));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function mergeTokenLineByContent(
|
|
594
|
+
lightLine: ThemedToken[],
|
|
595
|
+
darkLine: ThemedToken[],
|
|
596
|
+
): CodeLineToken[] | null {
|
|
597
|
+
const lightContent = lightLine.map((token) => token.content).join("");
|
|
598
|
+
const darkContent = darkLine.map((token) => token.content).join("");
|
|
599
|
+
|
|
600
|
+
// When themes tokenize the same text with different boundaries (common for markdown),
|
|
601
|
+
// align on content segments so we can still apply dark token colors reliably.
|
|
602
|
+
if (lightContent !== darkContent) return null;
|
|
603
|
+
if (lightContent.length === 0) return [];
|
|
604
|
+
|
|
605
|
+
const mergedLine: CodeLineToken[] = [];
|
|
606
|
+
let lightTokenIndex = 0;
|
|
607
|
+
let darkTokenIndex = 0;
|
|
608
|
+
let lightTokenOffset = 0;
|
|
609
|
+
let darkTokenOffset = 0;
|
|
610
|
+
|
|
611
|
+
while (
|
|
612
|
+
lightTokenIndex < lightLine.length &&
|
|
613
|
+
darkTokenIndex < darkLine.length
|
|
614
|
+
) {
|
|
615
|
+
const lightToken = lightLine[lightTokenIndex];
|
|
616
|
+
const darkToken = darkLine[darkTokenIndex];
|
|
617
|
+
|
|
618
|
+
if (lightTokenOffset >= lightToken.content.length) {
|
|
619
|
+
lightTokenIndex += 1;
|
|
620
|
+
lightTokenOffset = 0;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (darkTokenOffset >= darkToken.content.length) {
|
|
625
|
+
darkTokenIndex += 1;
|
|
626
|
+
darkTokenOffset = 0;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const lightRemainingLength = lightToken.content.length - lightTokenOffset;
|
|
631
|
+
const darkRemainingLength = darkToken.content.length - darkTokenOffset;
|
|
632
|
+
const segmentLength = Math.min(lightRemainingLength, darkRemainingLength);
|
|
633
|
+
if (segmentLength <= 0) break;
|
|
634
|
+
|
|
635
|
+
const lightSegment = lightToken.content.slice(
|
|
636
|
+
lightTokenOffset,
|
|
637
|
+
lightTokenOffset + segmentLength,
|
|
638
|
+
);
|
|
639
|
+
const darkSegment = darkToken.content.slice(
|
|
640
|
+
darkTokenOffset,
|
|
641
|
+
darkTokenOffset + segmentLength,
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
if (lightSegment !== darkSegment) return null;
|
|
645
|
+
|
|
646
|
+
mergedLine.push({
|
|
647
|
+
content: lightSegment,
|
|
648
|
+
color: lightToken.color,
|
|
649
|
+
darkColor: darkToken.color,
|
|
650
|
+
bgColor: lightToken.bgColor,
|
|
651
|
+
darkBgColor: darkToken.bgColor,
|
|
652
|
+
fontStyle: lightToken.fontStyle,
|
|
653
|
+
htmlStyle: lightToken.htmlStyle,
|
|
654
|
+
darkHtmlStyle: darkToken.htmlStyle,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
lightTokenOffset += segmentLength;
|
|
658
|
+
darkTokenOffset += segmentLength;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const mergedContent = mergedLine.map((token) => token.content).join("");
|
|
662
|
+
return mergedContent === lightContent ? mergedLine : null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function mergeTokenLines(
|
|
666
|
+
lightTokenLines: ThemedToken[][],
|
|
667
|
+
darkTokenLines: ThemedToken[][],
|
|
668
|
+
): CodeLineToken[][] {
|
|
669
|
+
const lineCount = Math.max(lightTokenLines.length, darkTokenLines.length);
|
|
670
|
+
const mergedLines: CodeLineToken[][] = [];
|
|
671
|
+
|
|
672
|
+
for (let lineIndex = 0; lineIndex < lineCount; lineIndex += 1) {
|
|
673
|
+
const lightLine = lightTokenLines[lineIndex] ?? [];
|
|
674
|
+
const darkLine = darkTokenLines[lineIndex] ?? [];
|
|
675
|
+
|
|
676
|
+
const mergedLine = mergeTokenLineByContent(lightLine, darkLine);
|
|
677
|
+
mergedLines.push(mergedLine ?? buildLightOnlyTokenLine(lightLine));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return mergedLines;
|
|
681
|
+
}
|