@clidey/dory 0.47.0 → 0.49.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 +15 -19
- package/package.json +35 -29
- package/scripts/render-routes.mjs +45 -14
- package/src/components/routes.tsx +19 -22
- package/src/components/store.ts +5 -5
- package/src/config/mdx.js +7 -3
- package/src/config/mdx.ts +6 -2
- package/src/entry-server.tsx +53 -20
- package/src/main.tsx +9 -1
- package/src/mdx/code.tsx +8 -5
- package/src/mdx/fence.tsx +10 -2
- package/src/mdx/icon.tsx +21 -14
- package/src/mdx/mermaid.tsx +10 -1
- package/src/mdx/processor.test.ts +47 -0
- package/src/mdx/step.tsx +3 -3
- package/src/plugins/analytics-injector.ts +7 -5
- package/src/plugins/docs-assets.ts +5 -4
- package/src/plugins/frontmatter-dev-server.ts +8 -159
- package/src/plugins/frontmatter-generator.ts +37 -129
- 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 +113 -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 +90 -59
- 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 +66 -47
- 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.ts +11 -7
- package/scripts/generate-frontmatter-json.js +0 -154
- package/scripts/generate-llm-txt.js +0 -115
|
@@ -19,8 +19,8 @@ export default function Routes() {
|
|
|
19
19
|
const route = useMemo(() => {
|
|
20
20
|
let targetPath = pathname;
|
|
21
21
|
if (pathname === '/') {
|
|
22
|
-
const ssrRoute = typeof window !== 'undefined'
|
|
23
|
-
targetPath = ssrRoute || ALL_NAVIGATION[0]
|
|
22
|
+
const ssrRoute = typeof window !== 'undefined' ? window.__DORY_ROUTE__ : undefined;
|
|
23
|
+
targetPath = ssrRoute || ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href || pathname;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const entry = Object.entries(ALL_PAGES).find(([path]) => path === targetPath);
|
|
@@ -33,26 +33,23 @@ export default function Routes() {
|
|
|
33
33
|
};
|
|
34
34
|
}, [pathname]);
|
|
35
35
|
|
|
36
|
-
// 404
|
|
37
|
-
if (!route) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
36
|
+
// 404 — no MDX module for this path (even if it appears in navigation)
|
|
37
|
+
if (!route && pathname !== '/') {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex flex-col items-center justify-center min-h-[50vh] p-8">
|
|
40
|
+
<h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
|
|
41
|
+
<h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
|
|
42
|
+
<p className="text-gray-600 dark:text-gray-400 mb-6 text-center max-w-md">
|
|
43
|
+
The page <code className="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-sm">{pathname}</code> doesn't exist.
|
|
44
|
+
</p>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => navigate(ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href ?? '/')}
|
|
47
|
+
className="px-4 py-2 text-sm font-medium rounded-md bg-[var(--brand-primary)] text-white hover:opacity-90 transition-opacity"
|
|
48
|
+
>
|
|
49
|
+
Go to Home
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
return (
|
package/src/components/store.ts
CHANGED
|
@@ -68,13 +68,13 @@ export function isFrontmatterReady() { return preloadedFrontMatter !== null; }
|
|
|
68
68
|
let preloadedFrontMatter: Record<string, any>[] | null = null;
|
|
69
69
|
|
|
70
70
|
// Check for inlined frontmatter from SSR (available synchronously, no fetch needed)
|
|
71
|
-
if (typeof window !== 'undefined' &&
|
|
72
|
-
preloadedFrontMatter =
|
|
73
|
-
completeFrontMatter = [...preloadedFrontMatter
|
|
74
|
-
for (const fm of preloadedFrontMatter
|
|
71
|
+
if (typeof window !== 'undefined' && window.__DORY_FRONTMATTER__) {
|
|
72
|
+
preloadedFrontMatter = window.__DORY_FRONTMATTER__;
|
|
73
|
+
completeFrontMatter = [...preloadedFrontMatter];
|
|
74
|
+
for (const fm of preloadedFrontMatter) {
|
|
75
75
|
updateNavigationTitle(fm.path, fm.title);
|
|
76
76
|
}
|
|
77
|
-
delete
|
|
77
|
+
delete window.__DORY_FRONTMATTER__;
|
|
78
78
|
// Build search index in the background after the page has fully rendered
|
|
79
79
|
setTimeout(() => addPreloadedContentToSearch(), 2000);
|
|
80
80
|
}
|
package/src/config/mdx.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import remarkGfm from 'remark-gfm';
|
|
2
|
+
import rehypeSlug from 'rehype-slug';
|
|
2
3
|
import { remarkSafeVars } from '../plugins/sanitize.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Shared MDX configuration used by
|
|
6
|
-
* and CLI build:file command
|
|
6
|
+
* Shared MDX configuration used by the main build (vite.config.ts),
|
|
7
|
+
* the SSR build (vite.config.ssr.ts), and the CLI build:file command
|
|
8
|
+
* (bin/dory.js). rehype-slug adds stable ids to headings so anchors
|
|
9
|
+
* match between SSR output and client hydration.
|
|
7
10
|
*/
|
|
8
11
|
export const getMdxConfig = (isDevelopment = false) => ({
|
|
9
12
|
providerImportSource: '@mdx-js/preact',
|
|
10
13
|
remarkPlugins: [remarkGfm, remarkSafeVars],
|
|
14
|
+
rehypePlugins: [rehypeSlug],
|
|
11
15
|
development: isDevelopment,
|
|
12
|
-
});
|
|
16
|
+
});
|
package/src/config/mdx.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import remarkGfm from 'remark-gfm';
|
|
2
|
+
import rehypeSlug from 'rehype-slug';
|
|
2
3
|
import { remarkSafeVars } from '../plugins/sanitize';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Shared MDX configuration used by
|
|
6
|
-
* and CLI build:file command
|
|
6
|
+
* Shared MDX configuration used by the main build (vite.config.ts),
|
|
7
|
+
* the SSR build (vite.config.ssr.ts), and the CLI build:file command
|
|
8
|
+
* (bin/dory.js). rehype-slug adds stable ids to headings so anchors
|
|
9
|
+
* match between SSR output and client hydration.
|
|
7
10
|
*/
|
|
8
11
|
export const getMdxConfig = (isDevelopment: boolean = false) => ({
|
|
9
12
|
providerImportSource: '@mdx-js/preact',
|
|
10
13
|
remarkPlugins: [remarkGfm, remarkSafeVars],
|
|
14
|
+
rehypePlugins: [rehypeSlug],
|
|
11
15
|
development: isDevelopment,
|
|
12
16
|
});
|
package/src/entry-server.tsx
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import type { ComponentType } from 'preact';
|
|
1
2
|
import { renderToString } from 'preact-render-to-string';
|
|
2
3
|
import { Router } from 'wouter-preact';
|
|
3
4
|
import { MDXProvider } from '@mdx-js/preact';
|
|
4
5
|
import { ThemeProvider } from '@clidey/ux';
|
|
5
6
|
import * as mdxComponents from './mdx/mdx';
|
|
6
|
-
import docsConfig from '../docs/dory.json';
|
|
7
|
+
import docsConfig from '../docs/dory.json' with { type: 'json' };
|
|
8
|
+
import type { DoryConfig } from './types/config';
|
|
7
9
|
|
|
8
|
-
const config = docsConfig as
|
|
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
|
+
}
|
|
9
16
|
|
|
10
17
|
function pathFromFilename(filename: string): string {
|
|
11
18
|
return filename
|
|
@@ -20,12 +27,27 @@ const allPages = Object.fromEntries(
|
|
|
20
27
|
Object.entries(pages).map(([path, loader]) => [pathFromFilename(path), loader])
|
|
21
28
|
);
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
interface NavPage {
|
|
31
|
+
title: string;
|
|
32
|
+
href: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface NavGroup {
|
|
36
|
+
title: string;
|
|
37
|
+
pages: NavPage[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface NavTab {
|
|
41
|
+
title: string;
|
|
42
|
+
groups: NavGroup[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildNavigation(frontmatter?: Record<string, any>[]): NavTab[] {
|
|
46
|
+
return navigationTabs.map(tab => ({
|
|
25
47
|
title: tab.tab,
|
|
26
|
-
groups: tab.groups.map(
|
|
48
|
+
groups: tab.groups.map(group => ({
|
|
27
49
|
title: group.group,
|
|
28
|
-
pages: group.pages.map(
|
|
50
|
+
pages: group.pages.map(page => {
|
|
29
51
|
const href = `/${page}`;
|
|
30
52
|
const fm = frontmatter?.find(f => f.path === href);
|
|
31
53
|
return {
|
|
@@ -42,32 +64,32 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
42
64
|
if (!loader) return '';
|
|
43
65
|
|
|
44
66
|
try {
|
|
45
|
-
const module = await loader() as
|
|
67
|
+
const module = await loader() as { default: ComponentType };
|
|
46
68
|
const Content = module.default;
|
|
47
69
|
|
|
48
70
|
const navigation = buildNavigation(frontmatter);
|
|
49
|
-
const pageFm = frontmatter?.find(
|
|
71
|
+
const pageFm = frontmatter?.find(fm => fm.path === routePath);
|
|
50
72
|
|
|
51
73
|
// Find group/page titles (Layout uses swapped names: "title: group, group: title")
|
|
52
74
|
let groupTitle = '';
|
|
53
|
-
const currentTab = navigation.find(
|
|
54
|
-
tab.groups.some(
|
|
55
|
-
group.pages.some(
|
|
75
|
+
const currentTab = navigation.find(tab =>
|
|
76
|
+
tab.groups.some(group =>
|
|
77
|
+
group.pages.some(page => page.href === routePath)
|
|
56
78
|
)
|
|
57
79
|
);
|
|
58
80
|
if (currentTab) {
|
|
59
81
|
for (const group of currentTab.groups) {
|
|
60
|
-
if (group.pages.some(
|
|
82
|
+
if (group.pages.some(p => p.href === routePath)) {
|
|
61
83
|
groupTitle = group.title;
|
|
62
84
|
}
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
// Prev/next
|
|
67
|
-
const allLinks = navigation.flatMap(
|
|
68
|
-
tab.groups.flatMap(
|
|
89
|
+
const allLinks = navigation.flatMap(tab =>
|
|
90
|
+
tab.groups.flatMap(group => group.pages)
|
|
69
91
|
);
|
|
70
|
-
const linkIndex = allLinks.findIndex(
|
|
92
|
+
const linkIndex = allLinks.findIndex(link => link.href === routePath);
|
|
71
93
|
const prevPage = linkIndex > 0 ? allLinks[linkIndex - 1] : null;
|
|
72
94
|
const nextPage = linkIndex < allLinks.length - 1 ? allLinks[linkIndex + 1] : null;
|
|
73
95
|
|
|
@@ -76,17 +98,28 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
76
98
|
<Router ssrPath={routePath}>
|
|
77
99
|
<MDXProvider components={mdxComponents}>
|
|
78
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. */}
|
|
79
108
|
{/* Header — matches src/ui/header.tsx structure */}
|
|
80
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">
|
|
81
110
|
<div class="relative flex justify-between gap-6 sm:gap-8 md:grow items-center w-full px-4">
|
|
82
111
|
<div class="flex items-center grow gap-2">
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
)}
|
|
85
118
|
<h1 class="text-base lg:text-2xl font-bold">{config.name}</h1>
|
|
86
119
|
</div>
|
|
87
120
|
<nav class="hidden md:block">
|
|
88
121
|
<ul role="list" class="flex items-center gap-8">
|
|
89
|
-
{navigation.map(
|
|
122
|
+
{navigation.map(tab => {
|
|
90
123
|
const isActive = tab === currentTab;
|
|
91
124
|
return (
|
|
92
125
|
<li key={tab.title}>
|
|
@@ -117,11 +150,11 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
117
150
|
{currentTab && (
|
|
118
151
|
<li>
|
|
119
152
|
<h2 class="font-display font-medium text-xs">{currentTab.title}</h2>
|
|
120
|
-
{currentTab.groups.map(
|
|
153
|
+
{currentTab.groups.map(group => (
|
|
121
154
|
<div key={group.title}>
|
|
122
155
|
<h3 class="mt-4 text-sm font-semibold">{group.title}</h3>
|
|
123
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">
|
|
124
|
-
{group.pages.map(
|
|
157
|
+
{group.pages.map(page => (
|
|
125
158
|
<li key={page.href} class="relative">
|
|
126
159
|
<a
|
|
127
160
|
href={page.href}
|
package/src/main.tsx
CHANGED
|
@@ -5,9 +5,14 @@ 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'
|
|
10
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;
|
|
11
16
|
|
|
12
17
|
// Start frontmatter loading immediately — if inlined from SSR, this is a no-op.
|
|
13
18
|
preloadFrontmatter()
|
|
@@ -15,14 +20,17 @@ preloadFrontmatter()
|
|
|
15
20
|
// Load fonts and colors before rendering the app
|
|
16
21
|
loadFonts()
|
|
17
22
|
loadColors()
|
|
23
|
+
loadTheme()
|
|
18
24
|
|
|
19
25
|
const AppWithMDX = withMDX(App)
|
|
20
26
|
|
|
21
27
|
const appRoot = document.getElementById('app')!
|
|
22
28
|
const hasSSRContent = appRoot.children.length > 0
|
|
23
29
|
|
|
30
|
+
const defaultTheme = config.theme?.mode === 'dark' ? 'dark' : config.theme?.mode === 'light' ? 'light' : 'system';
|
|
31
|
+
|
|
24
32
|
const tree = (
|
|
25
|
-
<ThemeProvider storageKey="@clidey/dory/theme">
|
|
33
|
+
<ThemeProvider defaultTheme={defaultTheme} storageKey="@clidey/dory/theme">
|
|
26
34
|
<Toaster />
|
|
27
35
|
<ErrorBoundary>
|
|
28
36
|
<AppWithMDX />
|
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/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>
|
|
@@ -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;
|
|
@@ -37,8 +37,9 @@ export function docsAssetsPlugin(): Plugin {
|
|
|
37
37
|
const shouldSkip = (name: string) => {
|
|
38
38
|
if (skipPatterns.has(name)) return true;
|
|
39
39
|
const ext = extname(name).toLowerCase();
|
|
40
|
-
// Skip
|
|
41
|
-
|
|
40
|
+
// Skip Markdown and JSON files (they're processed by Vite).
|
|
41
|
+
// MDX sources ARE copied so /route.mdx URLs work (Open MDX / LLM prompts).
|
|
42
|
+
if (ext === '.md' || ext === '.json') return true;
|
|
42
43
|
return false;
|
|
43
44
|
};
|
|
44
45
|
|
|
@@ -60,8 +61,8 @@ export function docsAssetsPlugin(): Plugin {
|
|
|
60
61
|
copyAssets(srcPath, destPath, itemRelativePath);
|
|
61
62
|
} else if (stats.isFile()) {
|
|
62
63
|
const ext = extname(item).toLowerCase();
|
|
63
|
-
// Copy
|
|
64
|
-
if (assetExtensions.has(ext)) {
|
|
64
|
+
// Copy asset files, plus source .mdx files at their route paths
|
|
65
|
+
if (assetExtensions.has(ext) || ext === '.mdx') {
|
|
65
66
|
cpSync(srcPath, destPath, { force: true });
|
|
66
67
|
console.log(`[docs-assets] Copied ${itemRelativePath}`);
|
|
67
68
|
}
|