@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.
- package/README.md +59 -164
- package/bin/chat-server.ts +270 -0
- package/bin/dist/analytics-server.d.ts +11 -0
- package/bin/dist/analytics-server.d.ts.map +1 -0
- package/bin/dist/analytics-server.js +121 -0
- package/bin/dist/analytics-server.js.map +1 -0
- package/bin/dist/automation.d.ts +35 -0
- package/bin/dist/automation.d.ts.map +1 -0
- package/bin/dist/automation.js +259 -0
- package/bin/dist/automation.js.map +1 -0
- package/bin/dist/chat-server.d.ts +8 -0
- package/bin/dist/chat-server.d.ts.map +1 -0
- package/bin/dist/chat-server.js +322 -0
- package/bin/dist/chat-server.js.map +1 -0
- package/bin/dist/check.d.ts +16 -0
- package/bin/dist/check.d.ts.map +1 -0
- package/bin/dist/check.js +247 -0
- package/bin/dist/check.js.map +1 -0
- package/bin/dist/dependency-overlay.d.ts +7 -0
- package/bin/dist/dependency-overlay.d.ts.map +1 -0
- package/bin/dist/dependency-overlay.js +137 -0
- package/bin/dist/dependency-overlay.js.map +1 -0
- package/bin/dist/dory.js +254 -402
- package/bin/dist/dory.js.map +1 -1
- package/bin/dist/i18n-init.d.ts +6 -0
- package/bin/dist/i18n-init.d.ts.map +1 -0
- package/bin/dist/i18n-init.js +56 -0
- package/bin/dist/i18n-init.js.map +1 -0
- package/bin/dist/mcp-http.d.ts +8 -0
- package/bin/dist/mcp-http.d.ts.map +1 -0
- package/bin/dist/mcp-http.js +72 -0
- package/bin/dist/mcp-http.js.map +1 -0
- package/bin/dist/mcp.d.ts +2 -0
- package/bin/dist/mcp.d.ts.map +1 -0
- package/bin/dist/mcp.js +81 -0
- package/bin/dist/mcp.js.map +1 -0
- package/bin/dist/preview-entry.d.ts +2 -0
- package/bin/dist/preview-entry.d.ts.map +1 -0
- package/bin/dist/preview-entry.js +6 -0
- package/bin/dist/preview-entry.js.map +1 -0
- package/bin/dist/preview-server.d.ts +5 -0
- package/bin/dist/preview-server.d.ts.map +1 -0
- package/bin/dist/preview-server.js +146 -0
- package/bin/dist/preview-server.js.map +1 -0
- package/bin/dist/preview-static.d.ts +7 -0
- package/bin/dist/preview-static.d.ts.map +1 -0
- package/bin/dist/preview-static.js +70 -0
- package/bin/dist/preview-static.js.map +1 -0
- package/bin/dist/project-config.d.ts +22 -0
- package/bin/dist/project-config.d.ts.map +1 -0
- package/bin/dist/project-config.js +44 -0
- package/bin/dist/project-config.js.map +1 -0
- package/dist/index.html +57 -0
- package/dist/runtime/dory-runtime.css +2 -0
- package/dist/runtime/dory-runtime.html +14 -0
- package/dist/runtime/dory-runtime.js +5490 -0
- package/docs/api-playground-demo.mdx +119 -0
- package/docs/api-reference/asyncapi.json +324 -0
- package/docs/api-reference/openapi.json +366 -0
- package/docs/api-reference/operations/create.mdx +5 -0
- package/docs/api-reference/operations/delete.mdx +5 -0
- package/docs/api-reference/operations/get.mdx +5 -0
- package/docs/api-reference/operations/update.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
- package/docs/assets/dory.jpeg +0 -0
- package/docs/components/accordion.mdx +145 -0
- package/docs/components/callouts.mdx +134 -0
- package/docs/components/cards.mdx +153 -0
- package/docs/components/code-blocks.mdx +281 -0
- package/docs/components/expandable.mdx +189 -0
- package/docs/components/icons.mdx +71 -0
- package/docs/components/images.mdx +82 -0
- package/docs/components/latex.mdx +148 -0
- package/docs/components/layout.mdx +178 -0
- package/docs/components/lists.mdx +134 -0
- package/docs/components/mermaid.mdx +149 -0
- package/docs/components/overview.mdx +81 -0
- package/docs/components/source.mdx +130 -0
- package/docs/components/steps.mdx +122 -0
- package/docs/components/tables.mdx +114 -0
- package/docs/components/tags.mdx +179 -0
- package/docs/components/typography.mdx +185 -0
- package/docs/configuration/analytics.mdx +45 -0
- package/docs/configuration/chat.mdx +59 -0
- package/docs/configuration/dory-json.mdx +278 -0
- package/docs/configuration/embed.mdx +73 -0
- package/docs/configuration/fonts.mdx +86 -0
- package/docs/configuration/internationalization.mdx +61 -0
- package/docs/configuration/navigation.mdx +217 -0
- package/docs/configuration/theming.mdx +166 -0
- package/docs/contributing.mdx +47 -0
- package/docs/dory.json +136 -0
- package/docs/embed-example.html +176 -0
- package/docs/favicon.svg +18 -0
- package/docs/features.mdx +440 -0
- package/docs/getting-started/automation.mdx +76 -0
- package/docs/getting-started/configuration.mdx +95 -0
- package/docs/getting-started/custom-components.mdx +60 -0
- package/docs/getting-started/installation.mdx +88 -0
- package/docs/getting-started/quick-start.mdx +53 -0
- package/docs/introduction.mdx +50 -0
- package/docs/logo/logo.svg +18 -0
- package/docs/playgrounds/api-playground.mdx +122 -0
- package/docs/playgrounds/websocket-playground.mdx +98 -0
- package/docs/social-card.png +0 -0
- package/docs/social-card.svg +17 -0
- package/docs/websocket-playground-demo.mdx +135 -0
- package/package.json +23 -17
- package/scripts/verify-component-examples.mjs +126 -0
- package/scripts/verify-runtime-parity.mjs +147 -0
- package/src/app.css +22 -1
- package/src/assets/fonts/HankenGrotesk.ttf +0 -0
- package/src/assets/fonts/OFL.txt +94 -0
- package/src/components/chat-assistant.tsx +229 -0
- package/src/components/dropdown.tsx +3 -3
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/hooks.tsx +20 -1
- package/src/components/navigation.tsx +82 -51
- package/src/components/notification.tsx +2 -3
- package/src/components/routes.tsx +17 -19
- package/src/components/search-index.test.ts +15 -0
- package/src/components/search-index.ts +49 -48
- package/src/components/search.tsx +208 -104
- package/src/components/store.ts +27 -9
- package/src/config/mdx-common.ts +11 -0
- package/src/config/mdx.ts +10 -11
- package/src/embed/loader.ts +0 -3
- package/src/embed/types.ts +0 -2
- package/src/embed/widget-iframe.ts +19 -21
- package/src/embedded-runtime/config.ts +138 -0
- package/src/embedded-runtime/index.tsx +680 -0
- package/src/embedded-runtime/runtime.html +14 -0
- package/src/entry-server.tsx +46 -124
- package/src/main.tsx +17 -5
- package/src/mdx/accordion.tsx +3 -3
- package/src/mdx/api-playground.tsx +18 -16
- package/src/mdx/async-api.tsx +2 -2
- package/src/mdx/card.tsx +3 -2
- package/src/mdx/code.tsx +11 -9
- package/src/mdx/fence.tsx +6 -5
- package/src/mdx/icon-grid.tsx +4 -1
- package/src/mdx/icon-names.test.ts +15 -0
- package/src/mdx/icon-names.ts +60 -0
- package/src/mdx/icon-source-variants.ts +102 -0
- package/src/mdx/icon.tsx +12 -73
- package/src/mdx/latex.test.ts +38 -0
- package/src/mdx/latex.tsx +42 -64
- package/src/mdx/mdx.tsx +1 -1
- package/src/mdx/mermaid.tsx +12 -6
- package/src/mdx/open-api.tsx +130 -74
- package/src/mdx/preprocess.test.ts +16 -0
- package/src/mdx/preprocess.ts +61 -0
- package/src/mdx/processor.test.ts +135 -37
- package/src/mdx/processor.ts +54 -92
- package/src/mdx/source.tsx +5 -4
- package/src/mdx/specimen.tsx +57 -43
- package/src/mdx/tabs.tsx +32 -21
- package/src/mdx/text.tsx +11 -11
- package/src/mdx/try-it-panel.tsx +284 -0
- package/src/mdx/type-scale.tsx +7 -7
- package/src/mdx/websocket-playground.tsx +21 -14
- package/src/plugins/analytics-injector.ts +8 -0
- package/src/plugins/chat-dev-server.ts +32 -0
- package/src/plugins/docs-assets.test.ts +25 -0
- package/src/plugins/docs-assets.ts +6 -2
- package/src/plugins/frontmatter-dev-server.ts +2 -2
- package/src/plugins/frontmatter-generator.ts +1 -1
- package/src/plugins/html-files-middleware.test.ts +21 -1
- package/src/plugins/html-files-middleware.ts +26 -6
- package/src/plugins/html-metadata-injector.ts +24 -4
- package/src/plugins/llm-txt-dev-server.ts +2 -2
- package/src/plugins/llm-txt-generator.ts +21 -4
- package/src/plugins/prerender.ts +47 -20
- package/src/plugins/robots-generator.ts +1 -1
- package/src/plugins/sanitize.ts +6 -5
- package/src/plugins/shared.ts +6 -18
- package/src/plugins/sitemap-generator.ts +1 -1
- package/src/plugins/social-preview.test.ts +71 -0
- package/src/plugins/social-preview.ts +87 -0
- package/src/plugins/unified-mdx-plugin.ts +1 -1
- package/src/theme.scss +630 -207
- package/src/types/config.ts +53 -1
- package/src/ui/breadcrumb.tsx +17 -0
- package/src/ui/docs-header-view.tsx +32 -0
- package/src/ui/docs-navigation-view.tsx +69 -0
- package/src/ui/docs-page-frame.tsx +50 -0
- package/src/ui/dory-mark.tsx +9 -0
- package/src/ui/header.tsx +33 -90
- package/src/ui/layout.tsx +99 -153
- package/src/ui/mobile-navigation.tsx +10 -8
- package/src/ui/page-actions.tsx +67 -0
- package/src/ui/page-feedback.tsx +50 -0
- package/src/ui/page-footer.tsx +23 -0
- package/src/ui/prev-next-link.tsx +15 -69
- package/src/ui/prev-next-view.tsx +27 -0
- package/src/ui/table-of-content-view.tsx +167 -0
- package/src/ui/table-of-content.tsx +25 -183
- package/src/utils/analytics.test.ts +36 -0
- package/src/utils/analytics.ts +35 -0
- package/src/utils/colors.ts +7 -0
- package/src/utils/content-scope.test.ts +26 -0
- package/src/utils/content-scope.ts +39 -0
- package/src/utils/search-content.ts +12 -0
- package/src/utils/theme.ts +1 -7
- package/vite.config.ssr.ts +4 -4
- package/vite.config.ts +22 -16
package/src/mdx/open-api.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CatalogIcon, Button } from "@clidey/ux"
|
|
2
2
|
import classNames from "classnames"
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
import { h } from "preact"
|
|
5
5
|
import { useState } from "preact/hooks"
|
|
6
|
-
import {
|
|
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
|
|
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
|
-
<
|
|
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="
|
|
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 ? <
|
|
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 ?
|
|
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
|
-
<
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
<
|
|
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
|
-
<
|
|
459
|
+
<CatalogIcon name="play" className="h-3 w-3 fill-current" />
|
|
409
460
|
Try it
|
|
410
461
|
</Button>
|
|
411
462
|
</div>
|
|
412
463
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
13
|
-
expect(
|
|
14
|
-
expect(
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
});
|