@clidey/dory 0.30.0 → 0.31.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.
Files changed (74) hide show
  1. package/bin/dist/dory.js +1 -8
  2. package/bin/dist/dory.js.map +1 -1
  3. package/index.html +54 -0
  4. package/package.json +12 -4
  5. package/src/app.css +5 -0
  6. package/src/app.tsx +13 -0
  7. package/src/components/dropdown.tsx +53 -0
  8. package/src/components/hooks.tsx +36 -0
  9. package/src/components/loading.tsx +21 -0
  10. package/src/components/mdx-provider.tsx +13 -0
  11. package/src/components/navigation.tsx +58 -0
  12. package/src/components/notification.tsx +13 -0
  13. package/src/components/routes.tsx +92 -0
  14. package/src/components/search-index.ts +43 -0
  15. package/src/components/search.tsx +448 -0
  16. package/src/components/store.ts +216 -0
  17. package/src/config/mdx.js +12 -0
  18. package/src/config/mdx.ts +12 -0
  19. package/src/embed/loader.ts +200 -0
  20. package/src/embed/types.ts +54 -0
  21. package/src/embed/widget-iframe.ts +302 -0
  22. package/src/main.tsx +22 -0
  23. package/src/mdx/accordion.tsx +41 -0
  24. package/src/mdx/api-playground.tsx +659 -0
  25. package/src/mdx/api.tsx +354 -0
  26. package/src/mdx/async-api.tsx +353 -0
  27. package/src/mdx/callout.tsx +64 -0
  28. package/src/mdx/card.tsx +54 -0
  29. package/src/mdx/code.tsx +289 -0
  30. package/src/mdx/expandable.tsx +36 -0
  31. package/src/mdx/fence.tsx +74 -0
  32. package/src/mdx/icon.tsx +73 -0
  33. package/src/mdx/latex.tsx +26 -0
  34. package/src/mdx/list.tsx +7 -0
  35. package/src/mdx/mdx.tsx +47 -0
  36. package/src/mdx/mermaid.tsx +147 -0
  37. package/src/mdx/open-api.tsx +311 -0
  38. package/src/mdx/processor.ts +174 -0
  39. package/src/mdx/source.tsx +58 -0
  40. package/src/mdx/step.tsx +51 -0
  41. package/src/mdx/table.tsx +20 -0
  42. package/src/mdx/tag.tsx +53 -0
  43. package/src/mdx/text.tsx +99 -0
  44. package/src/mdx/websocket-playground.tsx +824 -0
  45. package/src/plugins/docs-assets.ts +79 -0
  46. package/src/plugins/frontmatter-dev-server.ts +158 -0
  47. package/src/plugins/frontmatter-generator.ts +157 -0
  48. package/src/plugins/html-files-middleware.ts +108 -0
  49. package/src/plugins/html-metadata-injector.ts +76 -0
  50. package/src/plugins/llm-txt-dev-server.ts +183 -0
  51. package/src/plugins/llm-txt-generator.ts +186 -0
  52. package/src/plugins/sanitize.ts +163 -0
  53. package/src/plugins/unified-mdx-plugin.ts +35 -0
  54. package/src/sdk/index.ts +89 -0
  55. package/src/sdk/react.tsx +139 -0
  56. package/src/sdk/vue.ts +107 -0
  57. package/src/theme.scss +72 -0
  58. package/src/types/config.ts +89 -0
  59. package/src/ui/header.tsx +96 -0
  60. package/src/ui/layout.tsx +237 -0
  61. package/src/ui/mobile-navigation.tsx +72 -0
  62. package/src/ui/prev-next-link.tsx +74 -0
  63. package/src/ui/table-of-content.tsx +167 -0
  64. package/src/utils/colors.ts +54 -0
  65. package/src/utils/fonts.ts +23 -0
  66. package/src/utils/functions.ts +12 -0
  67. package/src/vite-env.d.ts +1 -0
  68. package/tsconfig.app.json +33 -0
  69. package/tsconfig.bin.json +25 -0
  70. package/tsconfig.json +7 -0
  71. package/tsconfig.node.json +26 -0
  72. package/vite.config.embed-loader.ts +28 -0
  73. package/vite.config.embed-widget.ts +31 -0
  74. package/vite.config.ts +45 -0
@@ -0,0 +1,147 @@
1
+ import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
2
+ import classNames from 'classnames'
3
+ import { useEffect, useMemo, useRef, useState } from 'preact/compat'
4
+
5
+ function fixNodeLabelsWithSpecialChars(content: string): string {
6
+ // Pattern to match node definitions with brackets containing special characters
7
+ // Matches: NodeId[text content] where text content has special chars and isn't already quoted
8
+ const nodePattern = /(\w+)\[([^\]"]*[(),\s][^\]"]*)\]/g;
9
+
10
+ return content.replace(nodePattern, (match, nodeId, labelContent) => {
11
+ // Only wrap in quotes if not already quoted and contains special characters
12
+ if (!labelContent.startsWith('"') || !labelContent.endsWith('"')) {
13
+ return `${nodeId}["${labelContent}"]`;
14
+ }
15
+ return match;
16
+ });
17
+ }
18
+
19
+ export function MermaidRenderer({ content }: { content: string }) {
20
+ const mermaidRef = useRef<HTMLDivElement>(null)
21
+ const [isFullscreen, setIsFullscreen] = useState(false)
22
+ const [isLoaded, setIsLoaded] = useState(false)
23
+ const [error, setError] = useState<string | null>(null)
24
+ const [isValid, setIsValid] = useState<boolean | null>(null)
25
+
26
+ const diagramId = useMemo(() => `mermaid-${btoa(content).replace(/[^a-zA-Z0-9]/g, '').substring(0, 10)}`, [content])
27
+
28
+ useEffect(() => {
29
+ let mounted = true
30
+
31
+ const renderMermaid = async () => {
32
+ try {
33
+ const mermaid = (await import('mermaid')).default
34
+
35
+ if (!mounted || !mermaidRef.current) return
36
+
37
+ mermaid.initialize({
38
+ startOnLoad: true,
39
+ theme: 'base',
40
+ themeVariables: {
41
+ fontSize: '14px',
42
+ nodePadding: 20,
43
+ notePadding: 15,
44
+ padding: 10,
45
+ nodeBorderRadius: 8,
46
+ },
47
+ flowchart: {
48
+ nodeSpacing: 50,
49
+ rankSpacing: 80,
50
+ padding: 20,
51
+ useMaxWidth: true,
52
+ },
53
+ })
54
+
55
+ if (mermaidRef.current) {
56
+ mermaidRef.current.innerHTML = ''
57
+ }
58
+
59
+ const contentWithoutStyle = content
60
+ .replace(/^\s*style .*$/gm, '')
61
+ .replace(/^\s*classDef .*$/gm, '')
62
+ .replace(/^\s*class .*$/gm, '');
63
+
64
+ // Fix node labels with special characters by wrapping them in quotes
65
+ const processedContent = fixNodeLabelsWithSpecialChars(contentWithoutStyle);
66
+
67
+ // Validate the mermaid syntax first
68
+ try {
69
+ await mermaid.parse(processedContent)
70
+ setIsValid(true)
71
+ } catch (parseError) {
72
+ setIsValid(false)
73
+ if (mounted) {
74
+ setError(parseError instanceof Error ? parseError.message : 'Invalid diagram syntax')
75
+ }
76
+ return
77
+ }
78
+
79
+ const { svg } = await mermaid.render(diagramId, processedContent);
80
+
81
+ if (mounted && mermaidRef.current) {
82
+ mermaidRef.current.innerHTML = svg
83
+ setIsLoaded(true)
84
+ }
85
+ } catch (err) {
86
+ console.error('Mermaid rendering error:', err)
87
+ if (mounted) {
88
+ setIsValid(false)
89
+ setError(err instanceof Error ? err.message : 'Failed to render diagram')
90
+ }
91
+ }
92
+ }
93
+
94
+ renderMermaid()
95
+
96
+ return () => {
97
+ mounted = false
98
+ }
99
+ }, [content, diagramId])
100
+
101
+ const toggleFullscreen = () => {
102
+ setIsFullscreen(!isFullscreen)
103
+ }
104
+
105
+ if (error || isValid === false) {
106
+ return (
107
+ <div className="mermaid-wrapper my-8">
108
+ <div className="flex flex-col items-center justify-center py-12 px-6 text-center bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20 border-2 border-dashed border-red-300 dark:border-red-700 rounded-lg">
109
+ <div className="w-16 h-16 mb-4 flex items-center justify-center bg-red-100 dark:bg-red-900/30 rounded-full">
110
+ <ExclamationTriangleIcon className="w-8 h-8 text-red-500" />
111
+ </div>
112
+ <h3 className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
113
+ Diagram Syntax Error
114
+ </h3>
115
+ <p className="text-red-600 dark:text-red-400 mb-4 max-w-md">
116
+ The Mermaid diagram contains invalid syntax and cannot be rendered.
117
+ </p>
118
+ <div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md">
119
+ <p className="text-sm text-red-700 dark:text-red-300 font-mono break-all">
120
+ {error}
121
+ </p>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ )
126
+ }
127
+
128
+ return (
129
+ <div className="mermaid-wrapper my-8">
130
+ {!isLoaded && (
131
+ <div className="flex items-center justify-center py-8 text-gray-400">
132
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
133
+ <span className="ml-2">Loading diagram...</span>
134
+ </div>
135
+ )}
136
+ <div
137
+ ref={mermaidRef}
138
+ className={classNames('mermaid cursor-pointer transition-all duration-300 rounded-lg p-4 bg-white', {
139
+ 'fixed inset-0 z-50 bg-black/90 flex items-center justify-center backdrop-blur-sm': isFullscreen,
140
+ 'opacity-0': !isLoaded,
141
+ 'opacity-100': isLoaded,
142
+ })}
143
+ onClick={toggleFullscreen}
144
+ />
145
+ </div>
146
+ )
147
+ }
@@ -0,0 +1,311 @@
1
+ import { useState } from "preact/hooks"
2
+ import { APIGroup } from "./api"
3
+ import { APIPlayground, PlayButton, type Server } from "./api-playground"
4
+ import { Fence } from "./fence"
5
+ import { Tag } from "./tag"
6
+ import { Col, Row } from "./text"
7
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, Button } from "@clidey/ux"
8
+
9
+ export function Properties({ children }: { children: React.ReactNode }) {
10
+ return (
11
+ <div className="my-2">
12
+ <ul
13
+ role="list"
14
+ className="m-0 max-w-[calc(var(--container-lg)-(--spacing(8)))] list-none divide-y divide-zinc-900/5 p-0 dark:divide-white/10"
15
+ >
16
+ {children}
17
+ </ul>
18
+ </div>
19
+ )
20
+ }
21
+
22
+ export function Property({
23
+ name,
24
+ children,
25
+ type,
26
+ required = false,
27
+ }: {
28
+ name: string
29
+ children: React.ReactNode
30
+ type?: string
31
+ required?: boolean
32
+ }) {
33
+ return (
34
+ <div className="m-0 px-0 first:pt-0 last:pb-0">
35
+ <div className="my-4 flex flex-wrap items-center gap-x-3 gap-2">
36
+ <div className="flex gap-2 items-center grow">
37
+ <Tag className="px-4 py-1">{name}</Tag>
38
+ <p className="font-mono text-xs text-zinc-400 dark:text-zinc-500">
39
+ {type}
40
+ </p>
41
+ <div className="flex grow justify-end ">
42
+ {required && <span className="text-rose-400 ml-1 text-xs">Required</span>}
43
+ </div>
44
+ </div>
45
+ <p className="w-full flex-none [&>:first-child]:mt-0 [&>:last-child]:mb-0 text-xs text-muted-foreground">
46
+ {children}
47
+ </p>
48
+ </div>
49
+ </div>
50
+ )
51
+ }
52
+
53
+ interface ParamFieldProps {
54
+ body: string;
55
+ type: string;
56
+ default?: string | number;
57
+ required?: boolean;
58
+ children: React.ReactNode;
59
+ }
60
+
61
+ export function ParamField({ body, type, default: defaultValue, required, children }: ParamFieldProps) {
62
+ return (
63
+ <Property name={body} type={type} required={required}>
64
+ {children}
65
+ {defaultValue != null && (
66
+ <span className="text-gray-500 ml-1">(default: {defaultValue})</span>
67
+ )}
68
+ </Property>
69
+ );
70
+ }
71
+
72
+ interface ResponseFieldProps {
73
+ name: string;
74
+ type: string;
75
+ required?: boolean;
76
+ children: React.ReactNode;
77
+ }
78
+
79
+ export function ResponseField({ name, type, required, children }: ResponseFieldProps) {
80
+ return (
81
+ <Property name={name} type={type} required={required}>
82
+ {children}
83
+ </Property>
84
+ );
85
+ }
86
+
87
+ interface Schema {
88
+ $ref?: string;
89
+ type?: string;
90
+ items?: Schema;
91
+ properties?: Record<string, Schema>;
92
+ required?: boolean;
93
+ description?: string;
94
+ default?: string | number;
95
+ }
96
+
97
+ interface Components {
98
+ schemas?: Record<string, Schema>;
99
+ }
100
+
101
+ interface Parameter {
102
+ name: string;
103
+ schema?: Schema;
104
+ required?: boolean;
105
+ description?: string;
106
+ }
107
+
108
+ interface Content {
109
+ schema?: Schema;
110
+ }
111
+
112
+ interface RequestBody {
113
+ description?: string;
114
+ content?: {
115
+ 'application/json'?: Content;
116
+ };
117
+ }
118
+
119
+ interface Response {
120
+ content?: {
121
+ 'application/json'?: Content;
122
+ };
123
+ }
124
+
125
+ interface PathMethod {
126
+ description?: string;
127
+ parameters?: Parameter[];
128
+ requestBody?: RequestBody;
129
+ responses: Record<string, Response>;
130
+ }
131
+
132
+ export interface OpenAPIJson {
133
+ paths: Record<string, Record<string, PathMethod>>;
134
+ components: Components;
135
+ servers?: Server[];
136
+ }
137
+
138
+ function resolveSchema(schema: Schema | undefined, components: Components): Schema {
139
+ if (!schema) return {};
140
+
141
+ if (schema.$ref) {
142
+ const refPath = schema.$ref.replace(/^#\/components\/schemas\//, '');
143
+ return resolveSchema(components.schemas?.[refPath], components);
144
+ }
145
+
146
+ if (schema.type === 'array' && schema.items) {
147
+ return {
148
+ ...schema,
149
+ items: resolveSchema(schema.items, components)
150
+ };
151
+ }
152
+
153
+ if (schema.properties) {
154
+ const resolvedProps: Record<string, Schema> = {};
155
+ for (const [key, value] of Object.entries(schema.properties)) {
156
+ resolvedProps[key] = resolveSchema(value, components);
157
+ }
158
+ return {
159
+ ...schema,
160
+ properties: resolvedProps
161
+ };
162
+ }
163
+
164
+ return schema;
165
+ }
166
+
167
+ export function OpenAPI({ openAPIJson, method: selectedMethod, path: selectedPath }: { openAPIJson: OpenAPIJson, method: string, path: string }) {
168
+ const { paths, components } = openAPIJson;
169
+ const [showPlayground, setShowPlayground] = useState(false);
170
+
171
+ const handleShowPlayground = () => {
172
+ setShowPlayground(true);
173
+ }
174
+
175
+ return (
176
+ <div className="max-w-none w-full">
177
+ <Sheet open={showPlayground} onOpenChange={setShowPlayground} modal={true}>
178
+ <SheetContent side="bottom" className="h-[90vh] w-full overflow-y-auto p-8">
179
+ <SheetHeader className="p-0">
180
+ <SheetTitle>API Playground</SheetTitle>
181
+ </SheetHeader>
182
+ <APIPlayground
183
+ method={selectedMethod}
184
+ url={selectedPath}
185
+ servers={openAPIJson.servers}
186
+ />
187
+ </SheetContent>
188
+ </Sheet>
189
+ {Object.entries(paths).map(([path, methods]) => {
190
+ if (path !== selectedPath) {
191
+ return null;
192
+ }
193
+
194
+ return (
195
+ <div key={path} className="my-8">
196
+ {Object.entries(methods).map(([method, details]) => {
197
+ const { description, parameters, requestBody, responses } = details;
198
+ const methodUpper = method.toUpperCase();
199
+
200
+ if (methodUpper !== selectedMethod) {
201
+ return null;
202
+ }
203
+
204
+ return (
205
+ <Row key={`${method}-${path}`} cols={2} className="gap-0">
206
+ <Col>
207
+ <h2 className="flex items-center mt-4">
208
+ <Tag variant="medium" className="mr-2 py-1 px-4 rounded-lg">{methodUpper}</Tag>
209
+ <span className="ml-2 font-mono">{path}</span>
210
+ </h2>
211
+
212
+ {description && <p>{description}</p>}
213
+
214
+ {parameters && parameters.length > 0 && (
215
+ <div className="mt-4">
216
+ <h3 className="font-bold">Parameters</h3>
217
+ <Properties>
218
+ {parameters.map((param) => (
219
+ <ParamField
220
+ key={param.name}
221
+ body={param.name}
222
+ type={param.schema?.type ?? 'string'}
223
+ required={param.required}
224
+ default={param.schema?.default}
225
+ >
226
+ {param.description}
227
+ </ParamField>
228
+ ))}
229
+ </Properties>
230
+ </div>
231
+ )}
232
+ {requestBody && (
233
+ <div className="mt-4">
234
+ <h3 className="font-bold">Request Body</h3>
235
+ <p>{requestBody.description}</p>
236
+ {requestBody.content?.['application/json']?.schema && (
237
+ (() => {
238
+ const resolvedSchema = resolveSchema(requestBody.content['application/json'].schema, components);
239
+ return (
240
+ <Properties>
241
+ {Object.entries(resolvedSchema.properties || {}).map(([propName, prop]) => (
242
+ <ParamField
243
+ key={propName}
244
+ body={propName}
245
+ type={prop.type ?? 'string'}
246
+ required={prop.required}
247
+ default={prop.default}
248
+ >
249
+ {prop.description}
250
+ </ParamField>
251
+ ))}
252
+ </Properties>
253
+ );
254
+ })()
255
+ )}
256
+ </div>
257
+ )}
258
+ {components?.schemas && (
259
+ <div>
260
+ <h3 className="font-bold">Schemas</h3>
261
+ {Object.entries(components.schemas).filter(([, schema]) => schema.properties).map(([name, schema]) => (
262
+ <div key={name} className="mt-8">
263
+ <h4>{name}</h4>
264
+ <Properties>
265
+ {Object.entries(schema.properties || {}).map(([propName, prop]) => (
266
+ <ResponseField
267
+ key={propName}
268
+ name={propName}
269
+ type={prop.type ?? 'string'}
270
+ required={prop.required}
271
+ >
272
+ {prop.description}
273
+ </ResponseField>
274
+ ))}
275
+ </Properties>
276
+ </div>
277
+ ))}
278
+ </div>
279
+ )}
280
+ </Col>
281
+ <Col sticky>
282
+ <Button
283
+ onClick={handleShowPlayground}
284
+ className='ml-auto mt-3 flex items-center gap-2'>
285
+ Try
286
+ <PlayButton className="h-4 w-4" />
287
+ </Button>
288
+ <APIGroup title="Request" tag={methodUpper} label={path}>
289
+ {Object.entries(responses).map(([code, response]) => (
290
+ <div key={code} title={code}>
291
+ {response.content?.['application/json']?.schema && (() => {
292
+ const resolvedSchema = resolveSchema(response.content['application/json'].schema, components);
293
+ return (
294
+ <Fence language="json">
295
+ {JSON.stringify(resolvedSchema, null, 2)}
296
+ </Fence>
297
+ );
298
+ })()}
299
+ </div>
300
+ ))}
301
+ </APIGroup>
302
+ </Col>
303
+ </Row>
304
+ )
305
+ })}
306
+ </div>
307
+ )
308
+ })}
309
+ </div>
310
+ )
311
+ }
@@ -0,0 +1,174 @@
1
+ import { compile } from '@mdx-js/mdx';
2
+ import { preprocessMdxTags } from '../plugins/sanitize';
3
+ import { getMdxConfig } from '../config/mdx';
4
+
5
+ export interface ProcessMdxOptions {
6
+ isDevelopment?: boolean;
7
+ fileName?: string;
8
+ }
9
+
10
+ /**
11
+ * SINGLE SOURCE OF TRUTH for MDX preprocessing.
12
+ *
13
+ * This processor is used by:
14
+ * - CLI verify:content command (bin/dory.ts) - full processing + compilation
15
+ * - Vite build via unified-mdx-plugin.ts - preprocessing only
16
+ *
17
+ * ALL MDX processing steps should be added here to ensure consistency.
18
+ */
19
+ export async function preprocessMdxContent(
20
+ content: string,
21
+ fileName: string = 'content.mdx'
22
+ ): Promise<{
23
+ success: boolean;
24
+ processedContent?: string;
25
+ error?: Error;
26
+ }> {
27
+ try {
28
+ // Step 1: Apply preprocessor (handles MDX tags, code blocks, etc.)
29
+ const preprocessor = preprocessMdxTags();
30
+ let processedContent = content;
31
+
32
+ if (preprocessor.transform) {
33
+ const result = preprocessor.transform(content, fileName);
34
+ if (result && typeof result === 'object' && result.code) {
35
+ processedContent = result.code;
36
+ } else if (typeof result === 'string') {
37
+ processedContent = result;
38
+ }
39
+ }
40
+
41
+ // Step 2: Convert URLs in angle brackets to markdown links
42
+ // Handles cases like <https://example.com> which MDX would interpret as JSX
43
+ // But preserves them inside code blocks
44
+
45
+ // Process line by line to handle code blocks correctly
46
+ const lines = processedContent.split('\n');
47
+ let inCodeBlock = false;
48
+
49
+ processedContent = lines.map(line => {
50
+ // Check if we're entering or leaving a code block (with optional indentation)
51
+ if (line.trim().startsWith('```')) {
52
+ inCodeBlock = !inCodeBlock;
53
+ return line;
54
+ }
55
+
56
+ // Don't process lines inside code blocks
57
+ if (inCodeBlock) {
58
+ return line;
59
+ }
60
+
61
+ // For lines outside code blocks, preserve inline code
62
+ // More robust regex that handles inline code with potential spaces
63
+ // Matches: `...` including multi-line content
64
+ let result = '';
65
+ let lastIndex = 0;
66
+
67
+ // Find all inline code spans in the line
68
+ const inlineCodeRegex = /`([^`]+)`/g;
69
+ let match;
70
+
71
+ while ((match = inlineCodeRegex.exec(line)) !== null) {
72
+ // Process text before the inline code
73
+ const beforeCode = line.substring(lastIndex, match.index);
74
+ result += beforeCode.replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
75
+
76
+ // Keep inline code as-is
77
+ result += match[0];
78
+
79
+ lastIndex = match.index + match[0].length;
80
+ }
81
+
82
+ // Process remaining text after last inline code (or entire line if no inline code)
83
+ const remainingText = line.substring(lastIndex);
84
+ result += remainingText.replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
85
+
86
+ return result;
87
+ }).join('\n');
88
+
89
+ // Step 3: Add any future preprocessing steps here
90
+ // Example: processedContent = await yourNewPreprocessor(processedContent);
91
+ // Example: processedContent = removeHTMLComments(processedContent);
92
+ // Example: processedContent = expandCustomShortcodes(processedContent);
93
+ // ALL preprocessing added here will automatically apply to:
94
+ // - CLI commands (verify:content)
95
+ // - Vite build (pnpm run build)
96
+ // - Vite dev server (pnpm run dev)
97
+
98
+ return {
99
+ success: true,
100
+ processedContent
101
+ };
102
+ } catch (error) {
103
+ return {
104
+ success: false,
105
+ error: error as Error
106
+ };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Process and compile MDX content through the full pipeline.
112
+ * Used by CLI commands that need the compiled output.
113
+ */
114
+ export async function processMdxContent(
115
+ content: string,
116
+ options: ProcessMdxOptions = {}
117
+ ): Promise<{
118
+ success: boolean;
119
+ compiled?: any;
120
+ error?: Error;
121
+ processedContent?: string;
122
+ }> {
123
+ const { isDevelopment = false, fileName = 'content.mdx' } = options;
124
+
125
+ try {
126
+ // Step 1: Preprocess
127
+ const preprocessResult = await preprocessMdxContent(content, fileName);
128
+ if (!preprocessResult.success) {
129
+ return {
130
+ success: false,
131
+ error: preprocessResult.error
132
+ };
133
+ }
134
+
135
+ const processedContent = preprocessResult.processedContent || content;
136
+
137
+ // Step 2: Compile MDX with the same config as main build
138
+ const mdxConfig = getMdxConfig(isDevelopment);
139
+ const compiled = await compile(processedContent, {
140
+ ...mdxConfig,
141
+ });
142
+
143
+ return {
144
+ success: true,
145
+ compiled,
146
+ processedContent
147
+ };
148
+ } catch (error) {
149
+ return {
150
+ success: false,
151
+ error: error as Error
152
+ };
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Verify MDX content can be compiled without errors.
158
+ * Uses the same processing pipeline as processMdxContent.
159
+ */
160
+ export async function verifyMdxContent(
161
+ content: string,
162
+ fileName?: string
163
+ ): Promise<{ valid: boolean; error?: Error }> {
164
+ const result = await processMdxContent(content, { fileName });
165
+
166
+ if (result.success) {
167
+ return { valid: true };
168
+ } else {
169
+ return {
170
+ valid: false,
171
+ error: result.error
172
+ };
173
+ }
174
+ }
@@ -0,0 +1,58 @@
1
+ type PathRange = {
2
+ path: string;
3
+ range?: string;
4
+ };
5
+
6
+ type SourceProps = {
7
+ url: string;
8
+ branch?: string;
9
+ paths: PathRange[];
10
+ };
11
+
12
+ export const Source = ({ url, branch = "main", paths }: SourceProps) => {
13
+ const createSourceUrl = (path: string, range?: string) => {
14
+ if (!range) {
15
+ if (url.includes('github.com')) {
16
+ return `${url}/blob/${branch}/${path}`;
17
+ } else if (url.includes('bitbucket.org')) {
18
+ return `${url}/src/${branch}/${path}`;
19
+ } else if (url.includes('gitlab.com')) {
20
+ return `${url}/-/blob/${branch}/${path}`;
21
+ }
22
+ return url;
23
+ }
24
+
25
+ const [start, end] = range.split('-');
26
+
27
+ if (url.includes('github.com')) {
28
+ return `${url}/blob/${branch}/${path}#L${start}${end ? `-L${end}` : ''}`;
29
+ } else if (url.includes('bitbucket.org')) {
30
+ return `${url}/src/${branch}/${path}#lines-${start}${end ? `:${end}` : ''}`;
31
+ } else if (url.includes('gitlab.com')) {
32
+ return `${url}/-/blob/${branch}/${path}#L${range.replace('-', '-')}`;
33
+ }
34
+
35
+ return url;
36
+ };
37
+
38
+ return (
39
+ <div>
40
+ <p className="text-sm text-slate-900 dark:text-white">Sources:</p>
41
+ <ul className="list-none pl-0 flex flex-wrap gap-2 mt-2">
42
+ {paths.map(({ path, range }) => (
43
+ <li key={path + (range || '')}>
44
+ <a
45
+ href={createSourceUrl(path, range)}
46
+ target="_blank"
47
+ rel="noopener noreferrer"
48
+ className="rounded px-3 py-1 font-mono text-sm text-slate-700 dark:text-slate-400 hover:opacity-80 transition-colors border border-slate-200 dark:border-slate-700"
49
+ >
50
+ {path}
51
+ {range && <span className="ml-2 opacity-70">{range}</span>}
52
+ </a>
53
+ </li>
54
+ ))}
55
+ </ul>
56
+ </div>
57
+ );
58
+ };