@clidey/dory 1.0.8 → 1.10.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 (210) hide show
  1. package/README.md +59 -164
  2. package/bin/chat-server.ts +270 -0
  3. package/bin/dist/analytics-server.d.ts +11 -0
  4. package/bin/dist/analytics-server.d.ts.map +1 -0
  5. package/bin/dist/analytics-server.js +121 -0
  6. package/bin/dist/analytics-server.js.map +1 -0
  7. package/bin/dist/automation.d.ts +35 -0
  8. package/bin/dist/automation.d.ts.map +1 -0
  9. package/bin/dist/automation.js +259 -0
  10. package/bin/dist/automation.js.map +1 -0
  11. package/bin/dist/chat-server.d.ts +8 -0
  12. package/bin/dist/chat-server.d.ts.map +1 -0
  13. package/bin/dist/chat-server.js +322 -0
  14. package/bin/dist/chat-server.js.map +1 -0
  15. package/bin/dist/check.d.ts +16 -0
  16. package/bin/dist/check.d.ts.map +1 -0
  17. package/bin/dist/check.js +247 -0
  18. package/bin/dist/check.js.map +1 -0
  19. package/bin/dist/dependency-overlay.d.ts +7 -0
  20. package/bin/dist/dependency-overlay.d.ts.map +1 -0
  21. package/bin/dist/dependency-overlay.js +137 -0
  22. package/bin/dist/dependency-overlay.js.map +1 -0
  23. package/bin/dist/dory.js +254 -402
  24. package/bin/dist/dory.js.map +1 -1
  25. package/bin/dist/i18n-init.d.ts +6 -0
  26. package/bin/dist/i18n-init.d.ts.map +1 -0
  27. package/bin/dist/i18n-init.js +56 -0
  28. package/bin/dist/i18n-init.js.map +1 -0
  29. package/bin/dist/mcp-http.d.ts +8 -0
  30. package/bin/dist/mcp-http.d.ts.map +1 -0
  31. package/bin/dist/mcp-http.js +72 -0
  32. package/bin/dist/mcp-http.js.map +1 -0
  33. package/bin/dist/mcp.d.ts +2 -0
  34. package/bin/dist/mcp.d.ts.map +1 -0
  35. package/bin/dist/mcp.js +81 -0
  36. package/bin/dist/mcp.js.map +1 -0
  37. package/bin/dist/preview-entry.d.ts +2 -0
  38. package/bin/dist/preview-entry.d.ts.map +1 -0
  39. package/bin/dist/preview-entry.js +6 -0
  40. package/bin/dist/preview-entry.js.map +1 -0
  41. package/bin/dist/preview-server.d.ts +5 -0
  42. package/bin/dist/preview-server.d.ts.map +1 -0
  43. package/bin/dist/preview-server.js +146 -0
  44. package/bin/dist/preview-server.js.map +1 -0
  45. package/bin/dist/preview-static.d.ts +7 -0
  46. package/bin/dist/preview-static.d.ts.map +1 -0
  47. package/bin/dist/preview-static.js +70 -0
  48. package/bin/dist/preview-static.js.map +1 -0
  49. package/bin/dist/project-config.d.ts +22 -0
  50. package/bin/dist/project-config.d.ts.map +1 -0
  51. package/bin/dist/project-config.js +44 -0
  52. package/bin/dist/project-config.js.map +1 -0
  53. package/dist/index.html +57 -0
  54. package/dist/runtime/dory-runtime.css +2 -0
  55. package/dist/runtime/dory-runtime.html +14 -0
  56. package/dist/runtime/dory-runtime.js +5490 -0
  57. package/docs/api-playground-demo.mdx +119 -0
  58. package/docs/api-reference/asyncapi.json +324 -0
  59. package/docs/api-reference/openapi.json +366 -0
  60. package/docs/api-reference/operations/create.mdx +5 -0
  61. package/docs/api-reference/operations/delete.mdx +5 -0
  62. package/docs/api-reference/operations/get.mdx +5 -0
  63. package/docs/api-reference/operations/update.mdx +5 -0
  64. package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
  65. package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
  66. package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
  67. package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
  68. package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
  69. package/docs/assets/dory.jpeg +0 -0
  70. package/docs/components/accordion.mdx +145 -0
  71. package/docs/components/callouts.mdx +134 -0
  72. package/docs/components/cards.mdx +153 -0
  73. package/docs/components/code-blocks.mdx +281 -0
  74. package/docs/components/expandable.mdx +189 -0
  75. package/docs/components/icons.mdx +71 -0
  76. package/docs/components/images.mdx +82 -0
  77. package/docs/components/latex.mdx +148 -0
  78. package/docs/components/layout.mdx +178 -0
  79. package/docs/components/lists.mdx +134 -0
  80. package/docs/components/mermaid.mdx +149 -0
  81. package/docs/components/overview.mdx +81 -0
  82. package/docs/components/source.mdx +130 -0
  83. package/docs/components/steps.mdx +122 -0
  84. package/docs/components/tables.mdx +114 -0
  85. package/docs/components/tags.mdx +179 -0
  86. package/docs/components/typography.mdx +185 -0
  87. package/docs/configuration/analytics.mdx +45 -0
  88. package/docs/configuration/chat.mdx +59 -0
  89. package/docs/configuration/dory-json.mdx +278 -0
  90. package/docs/configuration/embed.mdx +73 -0
  91. package/docs/configuration/fonts.mdx +86 -0
  92. package/docs/configuration/internationalization.mdx +61 -0
  93. package/docs/configuration/navigation.mdx +217 -0
  94. package/docs/configuration/theming.mdx +166 -0
  95. package/docs/contributing.mdx +47 -0
  96. package/docs/dory.json +136 -0
  97. package/docs/embed-example.html +176 -0
  98. package/docs/favicon.svg +18 -0
  99. package/docs/features.mdx +440 -0
  100. package/docs/getting-started/automation.mdx +76 -0
  101. package/docs/getting-started/configuration.mdx +95 -0
  102. package/docs/getting-started/custom-components.mdx +60 -0
  103. package/docs/getting-started/installation.mdx +88 -0
  104. package/docs/getting-started/quick-start.mdx +53 -0
  105. package/docs/introduction.mdx +50 -0
  106. package/docs/logo/logo.svg +18 -0
  107. package/docs/playgrounds/api-playground.mdx +122 -0
  108. package/docs/playgrounds/websocket-playground.mdx +98 -0
  109. package/docs/social-card.png +0 -0
  110. package/docs/social-card.svg +17 -0
  111. package/docs/websocket-playground-demo.mdx +135 -0
  112. package/package.json +23 -17
  113. package/scripts/verify-component-examples.mjs +126 -0
  114. package/scripts/verify-runtime-parity.mjs +147 -0
  115. package/src/app.css +22 -1
  116. package/src/assets/fonts/HankenGrotesk.ttf +0 -0
  117. package/src/assets/fonts/OFL.txt +94 -0
  118. package/src/components/chat-assistant.tsx +229 -0
  119. package/src/components/dropdown.tsx +3 -3
  120. package/src/components/error-boundary.tsx +2 -2
  121. package/src/components/hooks.tsx +20 -1
  122. package/src/components/navigation.tsx +82 -51
  123. package/src/components/notification.tsx +2 -3
  124. package/src/components/routes.tsx +17 -19
  125. package/src/components/search-index.test.ts +15 -0
  126. package/src/components/search-index.ts +49 -48
  127. package/src/components/search.tsx +208 -104
  128. package/src/components/store.ts +27 -9
  129. package/src/config/mdx-common.ts +11 -0
  130. package/src/config/mdx.ts +10 -11
  131. package/src/embed/loader.ts +0 -3
  132. package/src/embed/types.ts +0 -2
  133. package/src/embed/widget-iframe.ts +19 -21
  134. package/src/embedded-runtime/config.ts +138 -0
  135. package/src/embedded-runtime/index.tsx +680 -0
  136. package/src/embedded-runtime/runtime.html +14 -0
  137. package/src/entry-server.tsx +46 -124
  138. package/src/main.tsx +17 -5
  139. package/src/mdx/accordion.tsx +3 -3
  140. package/src/mdx/api-playground.tsx +18 -16
  141. package/src/mdx/async-api.tsx +2 -2
  142. package/src/mdx/card.tsx +3 -2
  143. package/src/mdx/code.tsx +11 -9
  144. package/src/mdx/fence.tsx +6 -5
  145. package/src/mdx/icon-grid.tsx +4 -1
  146. package/src/mdx/icon-names.test.ts +15 -0
  147. package/src/mdx/icon-names.ts +60 -0
  148. package/src/mdx/icon-source-variants.ts +102 -0
  149. package/src/mdx/icon.tsx +12 -73
  150. package/src/mdx/latex.test.ts +38 -0
  151. package/src/mdx/latex.tsx +42 -64
  152. package/src/mdx/mdx.tsx +1 -1
  153. package/src/mdx/mermaid.tsx +12 -6
  154. package/src/mdx/open-api.tsx +130 -74
  155. package/src/mdx/preprocess.test.ts +16 -0
  156. package/src/mdx/preprocess.ts +61 -0
  157. package/src/mdx/processor.test.ts +135 -37
  158. package/src/mdx/processor.ts +54 -92
  159. package/src/mdx/source.tsx +5 -4
  160. package/src/mdx/specimen.tsx +57 -43
  161. package/src/mdx/tabs.tsx +32 -21
  162. package/src/mdx/text.tsx +11 -11
  163. package/src/mdx/try-it-panel.tsx +284 -0
  164. package/src/mdx/type-scale.tsx +7 -7
  165. package/src/mdx/websocket-playground.tsx +21 -14
  166. package/src/plugins/analytics-injector.ts +8 -0
  167. package/src/plugins/chat-dev-server.ts +32 -0
  168. package/src/plugins/docs-assets.test.ts +25 -0
  169. package/src/plugins/docs-assets.ts +6 -2
  170. package/src/plugins/frontmatter-dev-server.ts +2 -2
  171. package/src/plugins/frontmatter-generator.ts +1 -1
  172. package/src/plugins/html-files-middleware.test.ts +21 -1
  173. package/src/plugins/html-files-middleware.ts +26 -6
  174. package/src/plugins/html-metadata-injector.ts +24 -4
  175. package/src/plugins/llm-txt-dev-server.ts +2 -2
  176. package/src/plugins/llm-txt-generator.ts +21 -4
  177. package/src/plugins/prerender.ts +47 -20
  178. package/src/plugins/robots-generator.ts +1 -1
  179. package/src/plugins/sanitize.ts +6 -5
  180. package/src/plugins/shared.ts +6 -18
  181. package/src/plugins/sitemap-generator.ts +1 -1
  182. package/src/plugins/social-preview.test.ts +71 -0
  183. package/src/plugins/social-preview.ts +87 -0
  184. package/src/plugins/unified-mdx-plugin.ts +1 -1
  185. package/src/theme.scss +630 -207
  186. package/src/types/config.ts +53 -1
  187. package/src/ui/breadcrumb.tsx +17 -0
  188. package/src/ui/docs-header-view.tsx +32 -0
  189. package/src/ui/docs-navigation-view.tsx +69 -0
  190. package/src/ui/docs-page-frame.tsx +50 -0
  191. package/src/ui/dory-mark.tsx +9 -0
  192. package/src/ui/header.tsx +33 -90
  193. package/src/ui/layout.tsx +99 -153
  194. package/src/ui/mobile-navigation.tsx +10 -8
  195. package/src/ui/page-actions.tsx +67 -0
  196. package/src/ui/page-feedback.tsx +50 -0
  197. package/src/ui/page-footer.tsx +23 -0
  198. package/src/ui/prev-next-link.tsx +15 -69
  199. package/src/ui/prev-next-view.tsx +27 -0
  200. package/src/ui/table-of-content-view.tsx +167 -0
  201. package/src/ui/table-of-content.tsx +25 -183
  202. package/src/utils/analytics.test.ts +36 -0
  203. package/src/utils/analytics.ts +35 -0
  204. package/src/utils/colors.ts +7 -0
  205. package/src/utils/content-scope.test.ts +26 -0
  206. package/src/utils/content-scope.ts +39 -0
  207. package/src/utils/search-content.ts +12 -0
  208. package/src/utils/theme.ts +1 -7
  209. package/vite.config.ssr.ts +4 -4
  210. package/vite.config.ts +22 -16
@@ -1,12 +1,13 @@
1
- import { Button, Sheet, SheetContent, SheetHeader, SheetTitle } from "@clidey/ux"
1
+ import { CatalogIcon, Button } from "@clidey/ux"
2
2
  import classNames from "classnames"
3
- import { Check, ChevronRight, Copy, Play } from "lucide-react"
3
+
4
4
  import { h } from "preact"
5
5
  import { useState } from "preact/hooks"
6
- import { APIPlayground, type Server } from "./api-playground"
6
+ import type { Server } from "./api-playground"
7
7
  import { CodeFrame, CodeGroup } from "./code"
8
8
  import { Fence, Pre } from "./fence"
9
9
  import { Tag } from "./tag"
10
+ import { TryItPanel, type TryItField } from "./try-it-panel"
10
11
 
11
12
  // ---------------------------------------------------------------------------
12
13
  // Field list used by the API pages and exposed to MDX as Properties/Property.
@@ -178,6 +179,11 @@ function exampleOf(schema: Schema, depth = 0): unknown {
178
179
  }
179
180
  }
180
181
 
182
+ /** Short type for a form field: `enum` rather than `enum<string>`. */
183
+ function fieldType(schema: Schema): string {
184
+ return schema.enum ? 'enum' : schema.type ?? 'object'
185
+ }
186
+
181
187
  function describeDefault(schema: Schema, description?: string): string | undefined {
182
188
  if (description) return description
183
189
  if (schema.description) return schema.description
@@ -223,17 +229,17 @@ function ChildAttributes({ schema, components }: { schema: Schema; components: C
223
229
  const count = Object.keys(schema.properties ?? {}).length
224
230
  if (count === 0) return null
225
231
  return (
226
- <div className="mt-3">
232
+ <div className="mt-3 overflow-hidden rounded-lg border border-[var(--border)]">
227
233
  <button
228
234
  type="button"
229
235
  onClick={() => setOpen((v) => !v)}
230
236
  aria-expanded={open}
231
- className="flex w-full items-center gap-2 rounded-lg border border-[var(--border)] bg-muted px-4 py-2.5 text-left text-sm font-medium text-foreground"
237
+ className="flex w-full items-center gap-2 bg-muted px-4 py-2.5 text-left text-sm font-medium text-foreground"
232
238
  >
233
- <ChevronRight aria-hidden="true" className={classNames('h-3.5 w-3.5 transition-transform', { 'rotate-90': open })} />
239
+ <CatalogIcon name="chevron-right" aria-hidden="true" className={classNames('h-3.5 w-3.5 transition-transform', { 'rotate-90': open })} />
234
240
  {open ? 'Hide' : 'Show'} {count} child attribute{count === 1 ? '' : 's'}
235
241
  </button>
236
- {open && <div className="pl-4"><SchemaFields schema={schema} components={components} /></div>}
242
+ {open && <div className="px-4"><SchemaFields schema={schema} components={components} /></div>}
237
243
  </div>
238
244
  )
239
245
  }
@@ -247,7 +253,7 @@ function CopyUrl({ value }: { value: string }) {
247
253
  onClick={() => navigator.clipboard.writeText(value).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1000) })}
248
254
  className="rounded-md p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
249
255
  >
250
- {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
256
+ {copied ? <CatalogIcon name="check" className="h-3.5 w-3.5" /> : <CatalogIcon name="copy" className="h-3.5 w-3.5" />}
251
257
  </button>
252
258
  )
253
259
  }
@@ -299,6 +305,32 @@ function ResponseTabs({ responses, components }: { responses: PathMethod['respon
299
305
  )
300
306
  }
301
307
 
308
+ // Longest line of the cURL `-d` body before it continues on the next line.
309
+ const CURL_BODY_WIDTH = 44
310
+
311
+ /** `-d` lines for cURL, breaking a flat JSON object across lines so it reads without wrapping. */
312
+ function curlBody(body: unknown): string[] {
313
+ const json = JSON.stringify(body)
314
+ if (body === null || typeof body !== 'object' || Array.isArray(body) || json.length <= CURL_BODY_WIDTH) {
315
+ return [` -d '${json}'`]
316
+ }
317
+ const lines: string[] = []
318
+ let line = ''
319
+ for (const [key, value] of Object.entries(body)) {
320
+ const entry = `${JSON.stringify(key)}:${JSON.stringify(value)}`
321
+ if (line && line.length + entry.length + 1 > CURL_BODY_WIDTH) {
322
+ lines.push(`${line},`)
323
+ line = entry
324
+ } else {
325
+ line = line ? `${line},${entry}` : entry
326
+ }
327
+ }
328
+ lines.push(line)
329
+ return lines.map((text, index) =>
330
+ `${index === 0 ? " -d '{" : ' '}${text}${index === lines.length - 1 ? "}'" : ''}`
331
+ )
332
+ }
333
+
302
334
  /** cURL / JavaScript / Python samples generated from the operation. */
303
335
  function RequestSamples({ method, url, headers, body }: { method: string; url: string; headers: Record<string, string>; body?: unknown }) {
304
336
  const headerLines = Object.entries(headers)
@@ -306,7 +338,7 @@ function RequestSamples({ method, url, headers, body }: { method: string; url: s
306
338
  const curl = [
307
339
  `curl -X ${method} ${url} \\`,
308
340
  ...headerLines.map(([key, value]) => ` -H "${key}: ${value}" \\`),
309
- ...(json ? [` -d '${json}'`] : []),
341
+ ...(json ? curlBody(body) : []),
310
342
  ].join('\n').replace(/ \\$/, '')
311
343
  const js = [
312
344
  `const response = await fetch("${url}", {`,
@@ -384,20 +416,39 @@ export function OpenAPI({ openAPIJson, method: selectedMethod, path: selectedPat
384
416
  }
385
417
  if (bodySchema) headers['Content-Type'] = 'application/json'
386
418
 
419
+ const parameterFields: TryItField[] = parameters.flatMap((param) => {
420
+ if (param.in === 'cookie') return []
421
+ const schema = resolveSchema(param.schema, components)
422
+ const value = schema.example ?? schema.default
423
+ return [{ name: param.name, type: fieldType(schema), value: value === undefined ? '' : String(value), in: param.in ?? 'query' }]
424
+ })
425
+ const bodyFields: TryItField[] = Object.entries(bodySchema?.properties ?? {}).map(([name, raw]) => {
426
+ const schema = resolveSchema(raw, components)
427
+ const value = exampleOf(schema)
428
+ return { name, type: fieldType(schema), value: typeof value === 'string' ? value : JSON.stringify(value) }
429
+ })
430
+ const authInputs: TryItField[] = schemes.flatMap((scheme) => {
431
+ const header = authHeader(scheme)
432
+ if (!header) return []
433
+ return [{ name: header[0], type: 'header', value: header[1].replace(/\$\w+$/, '') }]
434
+ })
435
+
387
436
  return (
388
437
  <div className="dory-api max-w-none w-full">
389
- <Sheet open={showPlayground} onOpenChange={setShowPlayground} modal={true}>
390
- <SheetContent side="bottom" className="h-[90vh] w-full overflow-y-auto p-8">
391
- <SheetHeader className="p-0">
392
- <SheetTitle>API Playground</SheetTitle>
393
- </SheetHeader>
394
- <APIPlayground method={selectedMethod} url={selectedPath} servers={openAPIJson.servers} />
395
- </SheetContent>
396
- </Sheet>
397
-
398
- {operation.description && <p className="dory-lead -mt-4 mb-6">{operation.description}</p>}
399
-
400
- <div className="not-prose flex items-center gap-3 rounded-lg border border-[var(--border)] bg-card py-2 pl-3 pr-2">
438
+ <TryItPanel
439
+ open={showPlayground}
440
+ onOpenChange={setShowPlayground}
441
+ method={selectedMethod}
442
+ path={selectedPath}
443
+ servers={openAPIJson.servers ?? []}
444
+ parameters={parameterFields}
445
+ body={bodyFields}
446
+ auth={authInputs}
447
+ />
448
+
449
+ {operation.description && <p className="dory-lead -mt-4! mb-6">{operation.description}</p>}
450
+
451
+ <div className="not-prose flex max-w-[45rem] items-center gap-3 rounded-lg border border-[var(--border)] bg-card py-2 pl-3 pr-2">
401
452
  <Tag>{selectedMethod}</Tag>
402
453
  <span className="min-w-0 flex-1 truncate font-mono text-sm">
403
454
  <span className="text-muted-foreground">{serverUrl}</span>
@@ -405,63 +456,68 @@ export function OpenAPI({ openAPIJson, method: selectedMethod, path: selectedPat
405
456
  </span>
406
457
  <CopyUrl value={fullUrl} />
407
458
  <Button onClick={() => setShowPlayground(true)} className="flex items-center gap-1.5 bg-[var(--primary)] text-white hover:bg-[var(--primary)]/90">
408
- <Play className="h-3 w-3 fill-current" />
459
+ <CatalogIcon name="play" className="h-3 w-3 fill-current" />
409
460
  Try it
410
461
  </Button>
411
462
  </div>
412
463
 
413
- {schemes.length > 0 && (
414
- <>
415
- <SectionTitle>Authorization</SectionTitle>
416
- <Properties>
417
- {schemes.map((scheme, index) => {
418
- const header = authHeader(scheme)
419
- const name = header?.[0] ?? scheme.name ?? scheme.type
420
- const location = scheme.type === 'apiKey' ? scheme.in ?? 'header' : 'header'
421
- return (
422
- <Property key={index} name={name} type={`${location} · string`} required>
423
- {scheme.description ?? (scheme.type === 'http' ? `${scheme.scheme === 'basic' ? 'Basic' : 'Bearer'} token.` : 'API key.')}
424
- </Property>
425
- )
426
- })}
427
- </Properties>
428
- </>
429
- )}
430
-
431
- {parameters.length > 0 && (
432
- <>
433
- <SectionTitle>Parameters</SectionTitle>
434
- <Properties>
435
- {parameters.map((param) => {
436
- const schema = resolveSchema(param.schema, components)
437
- return (
438
- <Property key={`${param.in}-${param.name}`} name={param.name} type={`${param.in ?? 'query'} · ${typeLabel(schema)}`} required={param.required} defaultValue={schema.default}>
439
- {describeDefault(schema, param.description)}
440
- </Property>
441
- )
442
- })}
443
- </Properties>
444
- </>
445
- )}
446
-
447
- {bodySchema && (
448
- <>
449
- <SectionTitle note="application/json">Body</SectionTitle>
450
- {operation.requestBody?.description && <p className="mt-2 text-sm text-muted-foreground">{operation.requestBody.description}</p>}
451
- <SchemaFields schema={bodySchema} components={components} />
452
- </>
453
- )}
454
-
455
- {responseSchema && (
456
- <>
457
- <SectionTitle>Response</SectionTitle>
458
- <SchemaFields schema={responseSchema} components={components} />
459
- </>
460
- )}
461
-
462
- <div className="mt-8">
463
- <RequestSamples method={selectedMethod} url={fullUrl} headers={headers} body={bodySchema ? exampleOf(bodySchema) : undefined} />
464
- <ResponseTabs responses={operation.responses} components={components} />
464
+ <div className="grid gap-x-12 xl:grid-cols-[minmax(0,1fr)_29rem]">
465
+ <div className="min-w-0">
466
+ {schemes.length > 0 && (
467
+ <>
468
+ <SectionTitle>Authorization</SectionTitle>
469
+ <Properties>
470
+ {schemes.map((scheme, index) => {
471
+ const header = authHeader(scheme)
472
+ const name = header?.[0] ?? scheme.name ?? scheme.type
473
+ const location = scheme.type === 'apiKey' ? scheme.in ?? 'header' : 'header'
474
+ return (
475
+ <Property key={index} name={name} type={`${location} · string`} required>
476
+ {scheme.description ?? (scheme.type === 'http' ? `${scheme.scheme === 'basic' ? 'Basic' : 'Bearer'} token.` : 'API key.')}
477
+ </Property>
478
+ )
479
+ })}
480
+ </Properties>
481
+ </>
482
+ )}
483
+
484
+ {parameters.length > 0 && (
485
+ <>
486
+ <SectionTitle>Parameters</SectionTitle>
487
+ <Properties>
488
+ {parameters.map((param) => {
489
+ const schema = resolveSchema(param.schema, components)
490
+ return (
491
+ <Property key={`${param.in}-${param.name}`} name={param.name} type={`${param.in ?? 'query'} · ${typeLabel(schema)}`} required={param.required} defaultValue={schema.default}>
492
+ {describeDefault(schema, param.description)}
493
+ </Property>
494
+ )
495
+ })}
496
+ </Properties>
497
+ </>
498
+ )}
499
+
500
+ {bodySchema && (
501
+ <>
502
+ <SectionTitle note="application/json">Body</SectionTitle>
503
+ {operation.requestBody?.description && <p className="mt-2 text-sm text-muted-foreground">{operation.requestBody.description}</p>}
504
+ <SchemaFields schema={bodySchema} components={components} />
505
+ </>
506
+ )}
507
+
508
+ {responseSchema && (
509
+ <>
510
+ <SectionTitle>Response</SectionTitle>
511
+ <SchemaFields schema={responseSchema} components={components} />
512
+ </>
513
+ )}
514
+ </div>
515
+
516
+ {/* Samples sit beside the fields on wide screens and follow the reader down the page. */}
517
+ <aside className="mt-10 min-w-0 xl:sticky xl:top-20 xl:self-start">
518
+ <RequestSamples method={selectedMethod} url={fullUrl} headers={headers} body={bodySchema ? exampleOf(bodySchema) : undefined} />
519
+ <ResponseTabs responses={operation.responses} components={components} />
520
+ </aside>
465
521
  </div>
466
522
  </div>
467
523
  )
@@ -0,0 +1,16 @@
1
+ import { expect, test } from 'vitest';
2
+ import { preprocessMdxSource } from './preprocess';
3
+
4
+ test('page title is rendered by the page frame without repeating in MDX', () => {
5
+ const result = preprocessMdxSource('---\ntitle: "Installation"\n---\n# Installation\n\n## Setup\nInstall it.');
6
+ expect(result).not.toMatch(/^# Installation$/m);
7
+ expect(result).toContain('## Setup');
8
+ expect(result).toContain('Install it.');
9
+ });
10
+
11
+ test('keeps code examples, distinct headings and pages without a frontmatter title', () => {
12
+ const result = preprocessMdxSource('---\ntitle: Guide\n---\n```md\n# Guide\n```\n# A different heading');
13
+ expect(result).toContain('```md\n# Guide\n```');
14
+ expect(result).toContain('# A different heading');
15
+ expect(preprocessMdxSource('# Guide')).toContain('# Guide');
16
+ });
@@ -0,0 +1,61 @@
1
+ import { preprocessMdxTags } from '../plugins/sanitize.ts';
2
+
3
+ /** Browser-safe preprocessing shared by the static build and embedded runtime. */
4
+ export function preprocessMdxSource(content: string, fileName = 'content.mdx', importedComponents: readonly string[] = []): string {
5
+ let processed = content;
6
+ const frontmatter = processed.match(/^---\n([\s\S]*?)\n---\n?/);
7
+ if (frontmatter) processed = processed.slice(frontmatter[0].length);
8
+
9
+ // The page frame already renders the frontmatter title. Remove only its
10
+ // matching Markdown heading; headings in examples and other titles survive.
11
+ const rawTitle = frontmatter?.[1].match(/^title:\s*(.+?)\s*$/m)?.[1];
12
+ let title = rawTitle;
13
+ if (rawTitle?.startsWith('"')) {
14
+ try { title = JSON.parse(rawTitle) as string; } catch { /* Keep unfamiliar YAML unchanged. */ }
15
+ } else if (rawTitle?.startsWith("'") && rawTitle.endsWith("'")) {
16
+ title = rawTitle.slice(1, -1).replace(/''/g, "'");
17
+ }
18
+ if (title) {
19
+ let fence = '';
20
+ let removed = false;
21
+ processed = processed.split('\n').filter(line => {
22
+ const delimiter = line.trim().match(/^(`{3,}|~{3,})/);
23
+ if (delimiter) {
24
+ if (!fence) fence = delimiter[1];
25
+ else if (delimiter[1][0] === fence[0] && delimiter[1].length >= fence.length) fence = '';
26
+ return true;
27
+ }
28
+ if (fence || removed) return true;
29
+ const heading = line.match(/^#\s+(.+?)\s*#*\s*$/);
30
+ if (heading?.[1] !== title) return true;
31
+ removed = true;
32
+ return false;
33
+ }).join('\n');
34
+ }
35
+
36
+ const strayYaml = processed.match(/^```yaml\n([\s\S]*?)\n---\n?/);
37
+ if (strayYaml) processed = processed.slice(strayYaml[0].length);
38
+
39
+ const transformed = preprocessMdxTags(importedComponents).transform(processed, fileName);
40
+ processed = typeof transformed === 'string' ? transformed : transformed?.code ?? processed;
41
+
42
+ let inCodeBlock = false;
43
+ return processed.split('\n').map(line => {
44
+ if (line.trim().startsWith('```')) {
45
+ inCodeBlock = !inCodeBlock;
46
+ return line;
47
+ }
48
+ if (inCodeBlock) return line;
49
+
50
+ let output = '';
51
+ let lastIndex = 0;
52
+ const inlineCode = /`([^`]+)`/g;
53
+ let match: RegExpExecArray | null;
54
+ while ((match = inlineCode.exec(line)) !== null) {
55
+ output += line.slice(lastIndex, match.index).replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
56
+ output += match[0];
57
+ lastIndex = match.index + match[0].length;
58
+ }
59
+ return output + line.slice(lastIndex).replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
60
+ }).join('\n');
61
+ }
@@ -1,47 +1,145 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { preprocessMdxContent } from './processor';
1
+ import { describe, it, expect, test } from 'vitest';
2
+ import { preprocessMdxContent, processMdxContent } from './processor';
3
+ import { preprocessMdxSource } from './preprocess';
3
4
 
4
- async function run(input: string): Promise<string> {
5
- const result = await preprocessMdxContent(input);
5
+ const source = `import Demo, { Button as DemoButton } from './example.tsx';
6
+ import * as Controls from './controls.tsx';
7
+
8
+ <Demo />
9
+ <DemoButton>Try it</DemoButton>
10
+ <Controls.Toggle />
11
+ `;
12
+
13
+ test('trusted static MDX preserves default, aliased and namespace component imports', async () => {
14
+ const result = await processMdxContent(source);
15
+ expect(result.success).toBe(true);
16
+ expect(result.processedContent).toContain('<Demo />');
17
+ expect(result.processedContent).toContain('<DemoButton>Try it</DemoButton>');
18
+ expect(result.processedContent).toContain('<Controls.Toggle />');
19
+ expect(result.processedContent).not.toContain('`<Demo');
20
+ });
21
+
22
+ test('imports shown in code fences do not authorize unknown JSX components', async () => {
23
+ const result = await preprocessMdxContent("```tsx\nimport Demo from './example.tsx';\n```\n\n<Demo />");
6
24
  expect(result.success).toBe(true);
7
- return result.processedContent!;
8
- }
25
+ expect(result.processedContent).toContain('`<Demo />`');
26
+ });
27
+
28
+ test('unimported components keep the existing text behavior in static MDX', async () => {
29
+ const result = await preprocessMdxContent("import Demo from './example.tsx';\n\n<Demo />\n<Unregistered />");
30
+ expect(result.processedContent).toContain('<Demo />');
31
+ expect(result.processedContent).toContain('`<Unregistered />`');
32
+ });
33
+
34
+ test('embedded preprocessing does not trust component names from MDX imports', () => {
35
+ const result = preprocessMdxSource(source);
36
+ expect(result).toContain('`<Demo />`');
37
+ expect(result).toContain('`<Controls.Toggle />`');
38
+ });
39
+
40
+ test('malformed import syntax reports a compile error instead of bypassing preprocessing', async () => {
41
+ const result = await preprocessMdxContent('import { Broken from "./example.tsx";\n\n<Broken />');
42
+ expect(result.success).toBe(false);
43
+ expect(result.error).toBeInstanceOf(Error);
44
+ });
45
+
46
+
47
+ test('imported components with props coexist with nested JSX and prose placeholders', async () => {
48
+ const result = await processMdxContent(`import Demo, { Child } from './example.tsx';
49
+
50
+ <Demo start={0}><Child /></Demo>
51
+
52
+ Replace <token> with your key.
53
+ `);
54
+ expect(result.success).toBe(true);
55
+ expect(result.processedContent).toContain('<Demo start={0}><Child /></Demo>');
56
+ });
57
+
58
+ test('multiline imports work while tilde-fenced imports stay inert', async () => {
59
+ const result = await processMdxContent(`import {
60
+ Button as DemoButton,
61
+ } from './example.tsx';
62
+
63
+ <DemoButton>Try it</DemoButton>
9
64
 
10
- describe('preprocessMdxContent', () => {
65
+ ~~~tsx
66
+ import Unregistered from './other.tsx';
67
+ ~~~
68
+
69
+ <Unregistered />
70
+ `);
71
+ expect(result.success).toBe(true);
72
+ expect(result.processedContent).toContain('<DemoButton>Try it</DemoButton>');
73
+ expect(result.processedContent).toContain('`<Unregistered />`');
74
+ });
75
+
76
+
77
+ describe('existing preprocessing behavior', () => {
11
78
  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');
79
+ const result = await preprocessMdxContent('---\ntitle: Hello\n---\n# Heading\n');
80
+ expect(result.success).toBe(true);
81
+ expect(result.processedContent).not.toContain('title: Hello');
82
+ expect(result.processedContent).toContain('# Heading');
15
83
  });
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');
84
+ it('converts angle bracket URLs while preserving fenced and inline code', async () => {
85
+ const result = await preprocessMdxContent('Visit <https://example.com/docs> now');
86
+ expect(result.processedContent).toBe('Visit [https://example.com/docs](https://example.com/docs) now');
87
+ const fenced = '```\ncurl <https://example.com>\n```\n';
88
+ expect((await preprocessMdxContent(fenced)).processedContent).toBe(fenced);
89
+ const inline = 'Use `<https://example.com>` here';
90
+ expect((await preprocessMdxContent(inline)).processedContent).toBe(inline);
20
91
  });
92
+ });
21
93
 
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
- });
94
+ test('preserves original fence-state behavior across multiple code blocks', async () => {
95
+ const input = ['```', '<https://in-block.com>', '```', '<https://outside.com>', '```', '<https://in-block-2.com>', '```'].join('\n');
96
+ const result = await preprocessMdxContent(input);
97
+ expect(result.success).toBe(true);
98
+ expect(result.processedContent).toContain('<https://in-block.com>');
99
+ expect(result.processedContent).toContain('<https://in-block-2.com>');
100
+ expect(result.processedContent).toContain('[https://outside.com](https://outside.com)');
101
+ });
26
102
 
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
103
 
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
- });
104
+ test('comments, multiline code spans and exported strings do not register fake imports', async () => {
105
+ for (const prefix of [
106
+ "{/*\nimport Demo from './demo.tsx';\n*/}",
107
+ "`example\nimport Demo from './demo.tsx';\nexample`",
108
+ "export const example = `\nimport Demo from './demo.tsx';\n`;",
109
+ ]) {
110
+ const result = await preprocessMdxContent(prefix + '\n\n<Demo />');
111
+ expect(result.success).toBe(true);
112
+ expect(result.processedContent).toContain('`<Demo />`');
113
+ }
114
+ });
115
+
116
+ test('import-looking prose without an MDX ESM declaration does not register names', async () => {
117
+ const result = await processMdxContent("import\nDemo from './demo.tsx';\n\n<Demo />");
118
+ expect(result.success).toBe(true);
119
+ expect(result.processedContent).toContain('`<Demo />`');
120
+ });
121
+
122
+
123
+ test('provisional names are AST-verified alongside real JSX with props', async () => {
124
+ const input = [
125
+ "import Real from './real.tsx';",
126
+ '',
127
+ '<Real count={0}>Try it</Real>',
128
+ '',
129
+ '{/*',
130
+ "import Fake from './fake.tsx';",
131
+ '*/}',
132
+ '',
133
+ 'export const example = `',
134
+ "import StringFake from './fake.tsx';",
135
+ '`;',
136
+ '',
137
+ '<Fake />',
138
+ '<StringFake />',
139
+ ].join('\n');
140
+ const result = await processMdxContent(input);
141
+ expect(result.success).toBe(true);
142
+ expect(result.processedContent).toContain('<Real count={0}>Try it</Real>');
143
+ expect(result.processedContent).toContain('`<Fake />`');
144
+ expect(result.processedContent).toContain('`<StringFake />`');
47
145
  });