@clidey/dory 0.46.0 → 0.48.0
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/bin/dist/dory.js +103 -45
- package/bin/dist/dory.js.map +1 -1
- package/index.html +11 -15
- package/package.json +35 -29
- package/scripts/render-routes.mjs +55 -15
- package/src/components/routes.tsx +38 -93
- package/src/components/search.tsx +1 -0
- package/src/components/store.ts +64 -88
- package/src/config/mdx.js +7 -3
- package/src/config/mdx.ts +6 -2
- package/src/entry-server.tsx +189 -66
- package/src/main.tsx +27 -5
- package/src/mdx/api-playground.tsx +2 -0
- package/src/mdx/code.tsx +8 -5
- package/src/mdx/fence.tsx +10 -2
- package/src/mdx/icon.tsx +21 -14
- package/src/mdx/latex.tsx +5 -0
- package/src/mdx/mdx.tsx +19 -11
- package/src/mdx/mermaid.tsx +10 -1
- package/src/mdx/processor.test.ts +47 -0
- package/src/mdx/step.tsx +3 -3
- package/src/mdx/websocket-playground.tsx +2 -0
- package/src/plugins/analytics-injector.ts +7 -5
- package/src/plugins/docs-assets.ts +5 -4
- package/src/plugins/frontmatter-dev-server.ts +23 -137
- package/src/plugins/frontmatter-generator.ts +64 -111
- package/src/plugins/html-files-middleware.test.ts +21 -0
- package/src/plugins/html-files-middleware.ts +27 -5
- package/src/plugins/html-metadata-injector.ts +94 -53
- package/src/plugins/llm-txt-dev-server.ts +25 -178
- package/src/plugins/llm-txt-generator.ts +94 -133
- package/src/plugins/prerender.ts +156 -50
- package/src/plugins/robots-generator.ts +15 -16
- package/src/plugins/sanitize.test.ts +104 -0
- package/src/plugins/sanitize.ts +91 -60
- package/src/plugins/shared.ts +139 -0
- package/src/plugins/sitemap-generator.ts +58 -44
- package/src/theme.scss +138 -15
- package/src/types/config.ts +42 -2
- package/src/types/globals.d.ts +7 -0
- package/src/ui/header.tsx +12 -4
- package/src/ui/layout.tsx +72 -52
- package/src/ui/table-of-content.tsx +19 -2
- package/src/utils/colors.ts +38 -18
- package/src/utils/fonts.ts +55 -12
- package/src/utils/theme.ts +29 -0
- package/vite.config.embed-loader.ts +3 -11
- package/vite.config.embed-widget.ts +3 -10
- package/vite.config.ssr.ts +2 -1
- package/vite.config.ts +13 -8
- package/scripts/generate-frontmatter-json.js +0 -154
- package/scripts/generate-llm-txt.js +0 -115
package/src/entry-server.tsx
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { ComponentType } from 'preact';
|
|
2
|
+
import { renderToString } from 'preact-render-to-string';
|
|
3
|
+
import { Router } from 'wouter-preact';
|
|
4
|
+
import { MDXProvider } from '@mdx-js/preact';
|
|
5
|
+
import { ThemeProvider } from '@clidey/ux';
|
|
4
6
|
import * as mdxComponents from './mdx/mdx';
|
|
7
|
+
import docsConfig from '../docs/dory.json' with { type: 'json' };
|
|
8
|
+
import type { DoryConfig } from './types/config';
|
|
9
|
+
|
|
10
|
+
const config = docsConfig as DoryConfig;
|
|
11
|
+
|
|
12
|
+
const navigationTabs = config.navigation?.tabs ?? [];
|
|
13
|
+
if (!config.navigation?.tabs) {
|
|
14
|
+
console.warn('dory.json has no navigation.tabs — SSR navigation will be empty.');
|
|
15
|
+
}
|
|
5
16
|
|
|
6
17
|
function pathFromFilename(filename: string): string {
|
|
7
18
|
return filename
|
|
@@ -16,88 +27,200 @@ const allPages = Object.fromEntries(
|
|
|
16
27
|
Object.entries(pages).map(([path, loader]) => [pathFromFilename(path), loader])
|
|
17
28
|
);
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// for crawlers — syntax highlighting is a visual enhancement.
|
|
23
|
-
function SSRPre(props: any) {
|
|
24
|
-
const child = props.children?.props;
|
|
25
|
-
const className = child?.className || '';
|
|
26
|
-
const code = child?.children || '';
|
|
27
|
-
return (
|
|
28
|
-
<pre class="overflow-x-auto p-4 text-xs">
|
|
29
|
-
<code class={className}>{code}</code>
|
|
30
|
-
</pre>
|
|
31
|
-
);
|
|
30
|
+
interface NavPage {
|
|
31
|
+
title: string;
|
|
32
|
+
href: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div class="my-4">
|
|
39
|
-
<code>{children}</code>
|
|
40
|
-
</div>
|
|
41
|
-
);
|
|
35
|
+
interface NavGroup {
|
|
36
|
+
title: string;
|
|
37
|
+
pages: NavPage[];
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
function SSRTable(props: any) {
|
|
48
|
-
return (
|
|
49
|
-
<div class="overflow-x-auto rounded-lg my-8">
|
|
50
|
-
<table class="table-auto border-collapse min-w-full" {...props} />
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
function SSRTh(props: any) {
|
|
55
|
-
return <th class="px-4 py-2 text-left text-sm font-medium" {...props} />;
|
|
56
|
-
}
|
|
57
|
-
function SSRTd(props: any) {
|
|
58
|
-
return <td class="px-4 py-2 text-sm" {...props} />;
|
|
40
|
+
interface NavTab {
|
|
41
|
+
title: string;
|
|
42
|
+
groups: NavGroup[];
|
|
59
43
|
}
|
|
60
44
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
45
|
+
function buildNavigation(frontmatter?: Record<string, any>[]): NavTab[] {
|
|
46
|
+
return navigationTabs.map(tab => ({
|
|
47
|
+
title: tab.tab,
|
|
48
|
+
groups: tab.groups.map(group => ({
|
|
49
|
+
title: group.group,
|
|
50
|
+
pages: group.pages.map(page => {
|
|
51
|
+
const href = `/${page}`;
|
|
52
|
+
const fm = frontmatter?.find(f => f.path === href);
|
|
53
|
+
return {
|
|
54
|
+
title: fm?.title || page.charAt(0).toUpperCase() + page.slice(1).replace(/-/g, ' '),
|
|
55
|
+
href,
|
|
56
|
+
};
|
|
57
|
+
})
|
|
58
|
+
}))
|
|
59
|
+
}));
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
...mdxComponents,
|
|
70
|
-
pre: SSRPre,
|
|
71
|
-
Latex: SSRLatex,
|
|
72
|
-
table: SSRTable,
|
|
73
|
-
th: SSRTh,
|
|
74
|
-
td: SSRTd,
|
|
75
|
-
APIPlayground: SSRPlaceholder,
|
|
76
|
-
WebSocketPlayground: SSRPlaceholder,
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export async function render(routePath: string): Promise<string> {
|
|
62
|
+
export async function render(routePath: string, frontmatter?: Record<string, any>[]): Promise<string> {
|
|
80
63
|
const loader = allPages[routePath];
|
|
81
64
|
if (!loader) return '';
|
|
82
65
|
|
|
83
66
|
try {
|
|
84
|
-
const module = await loader() as
|
|
67
|
+
const module = await loader() as { default: ComponentType };
|
|
85
68
|
const Content = module.default;
|
|
86
69
|
|
|
70
|
+
const navigation = buildNavigation(frontmatter);
|
|
71
|
+
const pageFm = frontmatter?.find(fm => fm.path === routePath);
|
|
72
|
+
|
|
73
|
+
// Find group/page titles (Layout uses swapped names: "title: group, group: title")
|
|
74
|
+
let groupTitle = '';
|
|
75
|
+
const currentTab = navigation.find(tab =>
|
|
76
|
+
tab.groups.some(group =>
|
|
77
|
+
group.pages.some(page => page.href === routePath)
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
if (currentTab) {
|
|
81
|
+
for (const group of currentTab.groups) {
|
|
82
|
+
if (group.pages.some(p => p.href === routePath)) {
|
|
83
|
+
groupTitle = group.title;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Prev/next
|
|
89
|
+
const allLinks = navigation.flatMap(tab =>
|
|
90
|
+
tab.groups.flatMap(group => group.pages)
|
|
91
|
+
);
|
|
92
|
+
const linkIndex = allLinks.findIndex(link => link.href === routePath);
|
|
93
|
+
const prevPage = linkIndex > 0 ? allLinks[linkIndex - 1] : null;
|
|
94
|
+
const nextPage = linkIndex < allLinks.length - 1 ? allLinks[linkIndex + 1] : null;
|
|
95
|
+
|
|
87
96
|
return renderToString(
|
|
97
|
+
<ThemeProvider>
|
|
88
98
|
<Router ssrPath={routePath}>
|
|
89
|
-
<MDXProvider components={
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
<MDXProvider components={mdxComponents}>
|
|
100
|
+
<div class="flex w-full flex-col">
|
|
101
|
+
{/* SYNC NOTE: the static shell below (header chrome, sidebar nav skeleton,
|
|
102
|
+
main/article wrappers) hand-mirrors the client components:
|
|
103
|
+
- <header> block -> src/ui/header.tsx (Header component JSX)
|
|
104
|
+
- #container/nav -> src/ui/layout.tsx (non-landing return) + src/components/navigation.tsx
|
|
105
|
+
When editing structure or classes in either place, update the counterpart,
|
|
106
|
+
or hydration will mismatch. Client-only widgets (Search, ModeToggle,
|
|
107
|
+
MobileNav) are intentionally NOT rendered here — see placeholder below. */}
|
|
108
|
+
{/* Header — matches src/ui/header.tsx structure */}
|
|
109
|
+
<header class="sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-gray-900/5 sm:px-6 lg:px-8 dark:shadow-none dark:bg-transparent">
|
|
110
|
+
<div class="relative flex justify-between gap-6 sm:gap-8 md:grow items-center w-full px-4">
|
|
111
|
+
<div class="flex items-center grow gap-2">
|
|
112
|
+
{config.logo && (
|
|
113
|
+
<>
|
|
114
|
+
<img src={`/${config.logo.light}`} alt="logo" class="w-8 hidden dark:block" />
|
|
115
|
+
<img src={`/${config.logo.dark}`} alt="logo" class="w-8 block dark:hidden" />
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
<h1 class="text-base lg:text-2xl font-bold">{config.name}</h1>
|
|
119
|
+
</div>
|
|
120
|
+
<nav class="hidden md:block">
|
|
121
|
+
<ul role="list" class="flex items-center gap-8">
|
|
122
|
+
{navigation.map(tab => {
|
|
123
|
+
const isActive = tab === currentTab;
|
|
124
|
+
return (
|
|
125
|
+
<li key={tab.title}>
|
|
126
|
+
<a
|
|
127
|
+
href={tab.groups[0].pages[0].href}
|
|
128
|
+
class={`text-xs transition hover:opacity-80${isActive ? ' text-brand-foreground' : ''}`}
|
|
129
|
+
>
|
|
130
|
+
{tab.title}
|
|
131
|
+
</a>
|
|
132
|
+
</li>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</ul>
|
|
136
|
+
</nav>
|
|
137
|
+
<div class="hidden md:block md:h-5 md:w-px md:bg-zinc-900/10 md:dark:bg-white/15" />
|
|
138
|
+
{/* Search, ModeToggle, MobileNav — rendered client-side during hydration.
|
|
139
|
+
Static placeholders cause ref mismatches with Algolia autocomplete. */}
|
|
140
|
+
<div class="flex items-center gap-3" data-testid="header-actions" />
|
|
141
|
+
</div>
|
|
142
|
+
</header>
|
|
143
|
+
|
|
144
|
+
{/* Container — matches src/ui/layout.tsx structure */}
|
|
145
|
+
<div id="container" class="relative flex w-full max-w-8xl sm:px-2 lg:px-8">
|
|
146
|
+
<div class="hidden lg:relative lg:block lg:flex-none">
|
|
147
|
+
<div class="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-x-hidden overflow-y-auto py-16 pr-8 pl-0.5 xl:w-72 xl:pr-16">
|
|
148
|
+
<nav class="dory-navigation text-base lg:text-sm">
|
|
149
|
+
<ul role="list" class="space-y-9">
|
|
150
|
+
{currentTab && (
|
|
151
|
+
<li>
|
|
152
|
+
<h2 class="font-display font-medium text-xs">{currentTab.title}</h2>
|
|
153
|
+
{currentTab.groups.map(group => (
|
|
154
|
+
<div key={group.title}>
|
|
155
|
+
<h3 class="mt-4 text-sm font-semibold">{group.title}</h3>
|
|
156
|
+
<ul role="list" class="mt-2 space-y-2 border-l-2 border-slate-100 lg:mt-4 lg:space-y-4 lg:border-slate-200 dark:border-slate-800">
|
|
157
|
+
{group.pages.map(page => (
|
|
158
|
+
<li key={page.href} class="relative">
|
|
159
|
+
<a
|
|
160
|
+
href={page.href}
|
|
161
|
+
class={routePath === page.href
|
|
162
|
+
? 'block w-full pl-3.5 font-semibold text-brand-foreground before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full'
|
|
163
|
+
: 'block w-full pl-3.5 text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300 before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full'}
|
|
164
|
+
>
|
|
165
|
+
{page.title}
|
|
166
|
+
</a>
|
|
167
|
+
</li>
|
|
168
|
+
))}
|
|
169
|
+
</ul>
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
172
|
+
</li>
|
|
173
|
+
)}
|
|
174
|
+
</ul>
|
|
175
|
+
</nav>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<main class="flex grow max-w-full px-4 py-16 sm:px-6 lg:px-8">
|
|
180
|
+
<div class="min-w-0 w-full">
|
|
181
|
+
<article class="h-full flex flex-1 flex-col">
|
|
182
|
+
<div class="flex justify-between items-start">
|
|
183
|
+
<header class="mb-8">
|
|
184
|
+
{groupTitle && <h1 class="font-display text-lg tracking-tight">{groupTitle}</h1>}
|
|
185
|
+
{pageFm?.title && <p class="text-4xl tracking-tight">{pageFm.title}</p>}
|
|
186
|
+
</header>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="flex-1 min-h-[calc(100vh-2rem)]">
|
|
189
|
+
{/* Matches App > div.antialiased > div.flex > Routes > Content */}
|
|
190
|
+
<div class="h-full antialiased">
|
|
191
|
+
<div class="flex min-h-full">
|
|
192
|
+
<Content />
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
{(prevPage || nextPage) && (
|
|
197
|
+
<div class="mt-12 pt-6 flex border-t border-black/20 dark:border-white/20">
|
|
198
|
+
{prevPage && (
|
|
199
|
+
<div>
|
|
200
|
+
<p class="font-display text-sm font-medium">Previous</p>
|
|
201
|
+
<div class="mt-1">
|
|
202
|
+
<a href={prevPage.href} class="text-base font-semibold hover:opacity-80">{prevPage.title}</a>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
{nextPage && (
|
|
207
|
+
<div class="ml-auto text-right">
|
|
208
|
+
<p class="font-display text-sm font-medium">Next</p>
|
|
209
|
+
<div class="mt-1">
|
|
210
|
+
<a href={nextPage.href} class="text-base font-semibold hover:opacity-80">{nextPage.title}</a>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</article>
|
|
217
|
+
</div>
|
|
218
|
+
</main>
|
|
219
|
+
</div>
|
|
96
220
|
</div>
|
|
97
|
-
</main>
|
|
98
|
-
</div>
|
|
99
221
|
</MDXProvider>
|
|
100
222
|
</Router>
|
|
223
|
+
</ThemeProvider>
|
|
101
224
|
);
|
|
102
225
|
} catch (error) {
|
|
103
226
|
console.warn(`SSR render failed for ${routePath}:`, error);
|
package/src/main.tsx
CHANGED
|
@@ -1,25 +1,47 @@
|
|
|
1
|
-
import { render } from 'preact'
|
|
1
|
+
import { render, hydrate } from 'preact'
|
|
2
2
|
import '@clidey/ux/styles.css';
|
|
3
3
|
import { App } from './app.tsx'
|
|
4
4
|
import { withMDX } from './components/mdx-provider.tsx'
|
|
5
5
|
import { ErrorBoundary } from './components/error-boundary.tsx'
|
|
6
6
|
import { loadFonts } from './utils/fonts'
|
|
7
7
|
import { loadColors } from './utils/colors'
|
|
8
|
+
import { loadTheme } from './utils/theme'
|
|
8
9
|
import { Toaster } from '@clidey/ux'
|
|
9
10
|
import { ThemeProvider } from '@clidey/ux'
|
|
11
|
+
import { preloadFrontmatter } from './components/store'
|
|
12
|
+
import docsConfig from '../docs/dory.json' with { type: 'json' };
|
|
13
|
+
import type { DoryConfig } from './types/config';
|
|
14
|
+
|
|
15
|
+
const config = docsConfig as DoryConfig;
|
|
16
|
+
|
|
17
|
+
// Start frontmatter loading immediately — if inlined from SSR, this is a no-op.
|
|
18
|
+
preloadFrontmatter()
|
|
10
19
|
|
|
11
20
|
// Load fonts and colors before rendering the app
|
|
12
21
|
loadFonts()
|
|
13
22
|
loadColors()
|
|
23
|
+
loadTheme()
|
|
14
24
|
|
|
15
25
|
const AppWithMDX = withMDX(App)
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
const appRoot = document.getElementById('app')!
|
|
28
|
+
const hasSSRContent = appRoot.children.length > 0
|
|
29
|
+
|
|
30
|
+
const defaultTheme = config.theme?.mode === 'dark' ? 'dark' : config.theme?.mode === 'light' ? 'light' : 'system';
|
|
31
|
+
|
|
32
|
+
const tree = (
|
|
33
|
+
<ThemeProvider defaultTheme={defaultTheme} storageKey="@clidey/dory/theme">
|
|
19
34
|
<Toaster />
|
|
20
35
|
<ErrorBoundary>
|
|
21
36
|
<AppWithMDX />
|
|
22
37
|
</ErrorBoundary>
|
|
23
|
-
</ThemeProvider
|
|
24
|
-
document.getElementById('app')!
|
|
38
|
+
</ThemeProvider>
|
|
25
39
|
)
|
|
40
|
+
|
|
41
|
+
// If SSR content exists, hydrate (attach JS to existing DOM).
|
|
42
|
+
// Otherwise, render from scratch (dev mode, non-SSR pages).
|
|
43
|
+
if (hasSSRContent) {
|
|
44
|
+
hydrate(tree, appRoot)
|
|
45
|
+
} else {
|
|
46
|
+
render(tree, appRoot)
|
|
47
|
+
}
|
|
@@ -323,6 +323,8 @@ export function APIPlayground({
|
|
|
323
323
|
authType = 'none',
|
|
324
324
|
servers = []
|
|
325
325
|
}: APIPlaygroundProps) {
|
|
326
|
+
if (typeof window === 'undefined') return null
|
|
327
|
+
|
|
326
328
|
const [selectedTab, setSelectedTab] = useState(0)
|
|
327
329
|
const [isLoading, setIsLoading] = useState(false)
|
|
328
330
|
const [response, setResponse] = useState<ResponseData | null>(null)
|
package/src/mdx/code.tsx
CHANGED
|
@@ -115,7 +115,7 @@ function CodePanelHeader({ tag, label }: { tag?: string; label?: string }) {
|
|
|
115
115
|
if (!tag && !label) return null
|
|
116
116
|
|
|
117
117
|
return (
|
|
118
|
-
<div className="flex h-9 items-center gap-2 border-
|
|
118
|
+
<div className="flex h-9 items-center gap-2 border-b border-t-transparent px-4" style={{ borderColor: 'var(--border)' }}>
|
|
119
119
|
{tag && (
|
|
120
120
|
<div className="dark flex">
|
|
121
121
|
<Tag variant="small">{tag}</Tag>
|
|
@@ -155,7 +155,7 @@ function CodePanel({
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
return (
|
|
158
|
-
<div className="group
|
|
158
|
+
<div className="group">
|
|
159
159
|
<CodePanelHeader tag={tag} label={label} />
|
|
160
160
|
<div className="relative">
|
|
161
161
|
<pre className="overflow-x-auto p-4 text-xs">{children}</pre>
|
|
@@ -257,7 +257,7 @@ export function CodeGroup({
|
|
|
257
257
|
return (
|
|
258
258
|
<CodeGroupContext.Provider value={true}>
|
|
259
259
|
{hasTabs ? (
|
|
260
|
-
<Card className="
|
|
260
|
+
<Card className="p-0 border-black/10 bg-none overflow-hidden mb-4">
|
|
261
261
|
<TabGroup
|
|
262
262
|
as="div"
|
|
263
263
|
selectedIndex={selectedIndex}
|
|
@@ -270,7 +270,7 @@ export function CodeGroup({
|
|
|
270
270
|
</TabGroup>
|
|
271
271
|
</Card>
|
|
272
272
|
) : (
|
|
273
|
-
<Card className="
|
|
273
|
+
<Card className="p-0 border-black/10 bg-none overflow-hidden mb-4">
|
|
274
274
|
<div className="not-prose">
|
|
275
275
|
{header}
|
|
276
276
|
{panels}
|
|
@@ -285,5 +285,8 @@ export function Code({
|
|
|
285
285
|
children,
|
|
286
286
|
...props
|
|
287
287
|
}: React.ComponentPropsWithoutRef<'code'>) {
|
|
288
|
-
|
|
288
|
+
const isBlock = typeof props.className === 'string' && props.className.includes('language-');
|
|
289
|
+
return <code className={classNames(props.className, {
|
|
290
|
+
'dory-mdx-code rounded-md py-0.5 text-sm px-1.5': !isBlock,
|
|
291
|
+
})} {...props}>{children}</code>
|
|
289
292
|
}
|
package/src/mdx/fence.tsx
CHANGED
|
@@ -6,6 +6,14 @@ import { Loading } from '../components/loading'
|
|
|
6
6
|
import { useTheme } from '@clidey/ux'
|
|
7
7
|
import classNames from 'classnames'
|
|
8
8
|
|
|
9
|
+
function dedent(code: string): string {
|
|
10
|
+
const lines = code.replace(/^\n+/, '').replace(/\s+$/, '').split('\n');
|
|
11
|
+
const indents = lines.filter(l => l.trim().length > 0).map(l => l.match(/^(\s*)/)?.[1].length ?? 0);
|
|
12
|
+
const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
|
|
13
|
+
if (minIndent === 0) return lines.join('\n');
|
|
14
|
+
return lines.map(l => l.slice(minIndent)).join('\n');
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
|
|
10
18
|
export function Fence({
|
|
11
19
|
children,
|
|
@@ -31,12 +39,12 @@ export function Fence({
|
|
|
31
39
|
|
|
32
40
|
return (
|
|
33
41
|
<Highlight
|
|
34
|
-
code={children
|
|
42
|
+
code={dedent(children)}
|
|
35
43
|
language={supportedLanguage}
|
|
36
44
|
theme={isDark ? themes.vsDark : themes.vsLight}>
|
|
37
45
|
{({ className, style, tokens, getTokenProps }) => (
|
|
38
46
|
<pre>
|
|
39
|
-
<Code className={classNames(className, "whitespace-pre-wrap")} style={style} title={supportedLanguage}>
|
|
47
|
+
<Code className={classNames(className, "whitespace-pre-wrap")} style={{ ...style, backgroundColor: 'transparent' }} title={supportedLanguage}>
|
|
40
48
|
{tokens.map((line, lineIndex) => (
|
|
41
49
|
<Fragment key={lineIndex}>
|
|
42
50
|
{line
|
package/src/mdx/icon.tsx
CHANGED
|
@@ -51,21 +51,28 @@ export function Icon({
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// --- Try Font Awesome ---
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
54
|
+
// Fall back to the brands set: Lucide 1.x removed all brand icons
|
|
55
|
+
// (github, twitter, npm, ...), which live only in FA brands.
|
|
56
|
+
const iconKey = `fa${pascalCase}`
|
|
57
|
+
const setsToTry: Array<keyof typeof iconSets> =
|
|
58
|
+
iconType === 'brands' ? ['brands'] : [iconType, 'brands']
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
for (const setName of setsToTry) {
|
|
61
|
+
const faSet = iconSets[setName] ?? solidIcons
|
|
62
|
+
const faIcon = faSet[iconKey as keyof typeof faSet]
|
|
63
|
+
|
|
64
|
+
if (faIcon && typeof faIcon === 'object' && 'iconName' in faIcon) {
|
|
65
|
+
library.add(faIcon)
|
|
66
|
+
return (
|
|
67
|
+
<FontAwesomeIcon
|
|
68
|
+
icon={[prefixes[setName] || 'fas', faIcon.iconName] as any}
|
|
69
|
+
className={classNames("stroke-black dark:stroke-white", className)}
|
|
70
|
+
color={color as any}
|
|
71
|
+
size={size as any}
|
|
72
|
+
{...rest as any}
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
console.warn(`⚠️ Icon not found: "${icon}" (converted to ${iconKey}) in ${iconType}`)
|
package/src/mdx/latex.tsx
CHANGED
|
@@ -16,6 +16,11 @@ interface LatexProps {
|
|
|
16
16
|
|
|
17
17
|
export function Latex({ children }: LatexProps) {
|
|
18
18
|
const expression = extractText(children).trim();
|
|
19
|
+
|
|
20
|
+
if (typeof window === 'undefined') {
|
|
21
|
+
return <div className="my-4"><code>{expression}</code></div>
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
20
25
|
const [error, setError] = useState<string | null>(null)
|
|
21
26
|
const [isLoading, setIsLoading] = useState(true)
|
package/src/mdx/mdx.tsx
CHANGED
|
@@ -22,20 +22,28 @@ export { UnorderedList as ul, OrderedList as ol, ListItem as li } from './list';
|
|
|
22
22
|
export { AsyncAPI } from './async-api';
|
|
23
23
|
export { Source } from './source';
|
|
24
24
|
|
|
25
|
-
// Lazy-loaded heavy components — only downloaded when a page uses them
|
|
25
|
+
// Lazy-loaded heavy components — only downloaded when a page uses them.
|
|
26
|
+
// SSR guard at this level prevents Suspense from rendering in Node.js
|
|
27
|
+
// (renderToString can't resolve lazy components).
|
|
26
28
|
const LazyAPIPlayground = lazy(() => import('./api-playground').then(m => ({ default: m.APIPlayground })));
|
|
27
|
-
export const APIPlayground = (props: any) =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
export const APIPlayground = (props: any) => {
|
|
30
|
+
if (typeof window === 'undefined') return null;
|
|
31
|
+
return (
|
|
32
|
+
<Suspense fallback={<Loading />}>
|
|
33
|
+
<LazyAPIPlayground {...props} />
|
|
34
|
+
</Suspense>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
32
37
|
|
|
33
38
|
const LazyWebSocketPlayground = lazy(() => import('./websocket-playground').then(m => ({ default: m.WebSocketPlayground })));
|
|
34
|
-
export const WebSocketPlayground = (props: any) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
export const WebSocketPlayground = (props: any) => {
|
|
40
|
+
if (typeof window === 'undefined') return null;
|
|
41
|
+
return (
|
|
42
|
+
<Suspense fallback={<Loading />}>
|
|
43
|
+
<LazyWebSocketPlayground {...props} />
|
|
44
|
+
</Suspense>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
39
47
|
|
|
40
48
|
export const wrapper = ({ children }: { children: preact.ComponentChildren }) => {
|
|
41
49
|
const ref = useRef<HTMLDivElement>(null);
|
package/src/mdx/mermaid.tsx
CHANGED
|
@@ -16,6 +16,15 @@ function fixNodeLabelsWithSpecialChars(content: string): string {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Deterministic djb2 hash — btoa throws on non-Latin-1 content (e.g. "→", CJK, emoji)
|
|
20
|
+
function hashContent(content: string): string {
|
|
21
|
+
let hash = 5381;
|
|
22
|
+
for (let i = 0; i < content.length; i++) {
|
|
23
|
+
hash = ((hash << 5) + hash + content.charCodeAt(i)) >>> 0;
|
|
24
|
+
}
|
|
25
|
+
return hash.toString(16);
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
export function MermaidRenderer({ content }: { content: string }) {
|
|
20
29
|
const mermaidRef = useRef<HTMLDivElement>(null)
|
|
21
30
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
@@ -23,7 +32,7 @@ export function MermaidRenderer({ content }: { content: string }) {
|
|
|
23
32
|
const [error, setError] = useState<string | null>(null)
|
|
24
33
|
const [isValid, setIsValid] = useState<boolean | null>(null)
|
|
25
34
|
|
|
26
|
-
const diagramId = useMemo(() => `mermaid-${
|
|
35
|
+
const diagramId = useMemo(() => `mermaid-${hashContent(content)}`, [content])
|
|
27
36
|
|
|
28
37
|
useEffect(() => {
|
|
29
38
|
let mounted = true
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { preprocessMdxContent } from './processor';
|
|
3
|
+
|
|
4
|
+
async function run(input: string): Promise<string> {
|
|
5
|
+
const result = await preprocessMdxContent(input);
|
|
6
|
+
expect(result.success).toBe(true);
|
|
7
|
+
return result.processedContent!;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('preprocessMdxContent', () => {
|
|
11
|
+
it('strips YAML frontmatter', async () => {
|
|
12
|
+
const output = await run('---\ntitle: Hello\n---\n# Heading\n');
|
|
13
|
+
expect(output).not.toContain('title: Hello');
|
|
14
|
+
expect(output).toContain('# Heading');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('converts URLs in angle brackets to markdown links', async () => {
|
|
18
|
+
const output = await run('Visit <https://example.com/docs> now');
|
|
19
|
+
expect(output).toBe('Visit [https://example.com/docs](https://example.com/docs) now');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('preserves angle-bracket URLs inside fenced code blocks', async () => {
|
|
23
|
+
const input = '```\ncurl <https://example.com>\n```\n';
|
|
24
|
+
expect(await run(input)).toBe(input);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('preserves angle-bracket URLs inside inline code', async () => {
|
|
28
|
+
const input = 'run `curl <https://example.com>` locally';
|
|
29
|
+
expect(await run(input)).toBe(input);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('tracks code-block state across multiple fences', async () => {
|
|
33
|
+
const input = [
|
|
34
|
+
'```',
|
|
35
|
+
'<https://in-block.com>',
|
|
36
|
+
'```',
|
|
37
|
+
'<https://outside.com>',
|
|
38
|
+
'```',
|
|
39
|
+
'<https://in-block-2.com>',
|
|
40
|
+
'```',
|
|
41
|
+
].join('\n');
|
|
42
|
+
const output = await run(input);
|
|
43
|
+
expect(output).toContain('<https://in-block.com>');
|
|
44
|
+
expect(output).toContain('<https://in-block-2.com>');
|
|
45
|
+
expect(output).toContain('[https://outside.com](https://outside.com)');
|
|
46
|
+
});
|
|
47
|
+
});
|
package/src/mdx/step.tsx
CHANGED
|
@@ -16,11 +16,11 @@ export function Steps({ children }: { children: preact.ComponentChildren }) {
|
|
|
16
16
|
},
|
|
17
17
|
'relative'
|
|
18
18
|
)}>
|
|
19
|
-
<div aria-hidden="true" className="absolute top-10 left-4 h-full w-[0.5px]
|
|
19
|
+
<div aria-hidden="true" className="absolute top-10 left-4 h-full w-[0.5px]" style={{ top: '40px', height: 'calc(100% - 44px)', backgroundColor: 'color-mix(in oklch, var(--brand-foreground) 30%, transparent)' }} />
|
|
20
20
|
<div className="group relative flex items-start">
|
|
21
21
|
<span className="flex h-9 items-center">
|
|
22
|
-
<span className="relative z-10 flex size-8 items-center justify-center rounded-full
|
|
23
|
-
<span className="text-sm font-medium
|
|
22
|
+
<span className="relative z-10 flex size-8 items-center justify-center rounded-full border" style={{ borderColor: 'var(--brand-foreground)', backgroundColor: 'color-mix(in oklch, var(--brand-foreground) 12%, transparent)' }}>
|
|
23
|
+
<span className="text-sm font-medium" style={{ color: 'var(--brand-foreground)' }}>
|
|
24
24
|
{stepIdx + 1}
|
|
25
25
|
</span>
|
|
26
26
|
</span>
|
|
@@ -750,6 +750,8 @@ function WebSocketPlaygroundInternal({
|
|
|
750
750
|
|
|
751
751
|
// Main exported component with Sheet wrapper
|
|
752
752
|
export function WebSocketPlayground(props: WebSocketPlaygroundProps) {
|
|
753
|
+
if (typeof window === 'undefined') return null
|
|
754
|
+
|
|
753
755
|
const [showPlayground, setShowPlayground] = useState(false)
|
|
754
756
|
|
|
755
757
|
const handleShowPlayground = () => {
|
|
@@ -17,19 +17,21 @@ export function analyticsInjector(): Plugin {
|
|
|
17
17
|
const config = JSON.parse(readFileSync(doryJsonPath, 'utf-8'));
|
|
18
18
|
|
|
19
19
|
if (config.analytics?.posthog?.token) {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// JSON.stringify so config-controlled values are safely escaped
|
|
21
|
+
// when inlined into the script (quotes, </script>, etc.)
|
|
22
|
+
const token = JSON.stringify(String(config.analytics.posthog.token)).replace(/</g, '\\u003c');
|
|
23
|
+
const apiHost = JSON.stringify(String(config.analytics.posthog.apiHost || 'https://us.i.posthog.com')).replace(/</g, '\\u003c');
|
|
22
24
|
|
|
23
25
|
const script = `
|
|
24
26
|
<script>
|
|
25
27
|
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture identify setPersonProperties group resetGroups reset get_distinct_id getGroups get_session_id opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
26
|
-
posthog.init(
|
|
27
|
-
api_host:
|
|
28
|
+
posthog.init(${token}, {
|
|
29
|
+
api_host: ${apiHost},
|
|
28
30
|
person_profiles: 'identified_only',
|
|
29
31
|
})
|
|
30
32
|
</script>`;
|
|
31
33
|
|
|
32
|
-
return html.replace('</head>', `${script}\n </head>`);
|
|
34
|
+
return html.replace('</head>', () => `${script}\n </head>`);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
return html;
|