blume 0.4.0 → 0.5.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/dist/cli/index.js +1137 -722
- package/dist/cli/index.js.map +28 -23
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/project.d.ts +12 -2
- package/dist/types/core/schema.d.ts +154 -15
- package/dist/types/core/types.d.ts +7 -0
- package/docs/advanced/api-reference.mdx +33 -23
- package/docs/advanced/bridge.mdx +74 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/advanced/migrate.mdx +119 -0
- package/docs/configuration/index.mdx +1 -1
- package/docs/content/components.mdx +55 -2
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/syntax.mdx +2 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +29 -1
- package/docs/reference/frontmatter.mdx +5 -0
- package/package.json +11 -1
- package/src/astro/generate.ts +18 -8
- package/src/astro/templates.ts +28 -4
- package/src/cli/commands/build.ts +107 -63
- package/src/cli/commands/check.ts +20 -0
- package/src/cli/dev-lock.ts +13 -5
- package/src/cli/prepare.ts +3 -0
- package/src/components/BlumePage.astro +6 -0
- package/src/components/Icon.astro +13 -10
- package/src/components/content/ApiField.astro +75 -0
- package/src/components/content/ParamField.astro +39 -0
- package/src/components/content/RequestField.astro +23 -0
- package/src/components/content/ResponseField.astro +23 -0
- package/src/components/content/Step.astro +1 -1
- package/src/components/layout/Breadcrumbs.astro +7 -2
- package/src/components/layout/NavTree.astro +24 -8
- package/src/components/layout/RootLayout.astro +56 -34
- package/src/components/layout/Search.astro +1 -1
- package/src/components/openapi/ApiOverview.astro +84 -0
- package/src/components/openapi/MethodBadge.astro +28 -0
- package/src/components/openapi/Operation.astro +140 -0
- package/src/components/openapi/ParametersTable.astro +97 -0
- package/src/components/openapi/RequestBody.astro +58 -0
- package/src/components/openapi/RequestPanel.astro +169 -0
- package/src/components/openapi/Responses.astro +91 -0
- package/src/components/openapi/SchemaProperty.astro +118 -0
- package/src/components/openapi/SchemaTable.astro +86 -0
- package/src/components/openapi/helpers.ts +238 -0
- package/src/components/openapi/panel.ts +59 -0
- package/src/components/openapi/snippets.ts +201 -0
- package/src/core/builtin-tags.ts +5 -0
- package/src/core/data.ts +2 -0
- package/src/core/project-graph.ts +5 -1
- package/src/core/project.ts +25 -3
- package/src/core/schema.ts +47 -6
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/resolve.ts +28 -6
- package/src/core/types.ts +7 -0
- package/src/migrate/mintlify/config.ts +153 -1
- package/src/migrate/mintlify/content.ts +8 -2
- package/src/migrate/mintlify/index.ts +58 -1
- package/src/openapi/model.ts +174 -0
- package/src/openapi/parse.ts +48 -0
- package/src/openapi/references.ts +164 -0
- package/src/openapi/render-mdx.ts +76 -0
- package/src/openapi/scalar.ts +15 -103
- package/src/openapi/source.ts +140 -0
- package/src/registry/eject.ts +15 -2
- package/src/theme/chrome-icons.ts +22 -0
- package/src/theme/icons.ts +151 -161
|
@@ -200,9 +200,19 @@ const searchLocale =
|
|
|
200
200
|
localeSwitch && localeSwitch.length > 1 ? locale : undefined;
|
|
201
201
|
// TOC entries: the configured heading range, or none when the TOC is disabled
|
|
202
202
|
// (an empty list makes TableOfContents render nothing).
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
// API operation pages own a two-column body (docs + request panel) and their own
|
|
204
|
+
// right rail, so they drop the table of contents and widen the content column.
|
|
205
|
+
const isApiOperation = pageType === "openapi-operation";
|
|
206
|
+
const tocHeadings =
|
|
207
|
+
toc.enabled && !isApiOperation
|
|
208
|
+
? headings.filter((h) => h.depth >= toc.minLevel && h.depth <= toc.maxLevel)
|
|
209
|
+
: [];
|
|
210
|
+
const gridClass = isApiOperation
|
|
211
|
+
? "lg:grid-cols-[17.5rem_minmax(0,1fr)]"
|
|
212
|
+
: "lg:grid-cols-[17.5rem_minmax(0,1fr)] xl:grid-cols-[17.5rem_minmax(0,1fr)_17.5rem]";
|
|
213
|
+
const articleClass = isApiOperation
|
|
214
|
+
? "prose max-w-none"
|
|
215
|
+
: "prose mx-auto max-w-[42rem]";
|
|
206
216
|
const pageTitle = page.title ? `${page.title} - ${site.title}` : site.title;
|
|
207
217
|
const description = page.description ?? site.description;
|
|
208
218
|
|
|
@@ -331,7 +341,7 @@ const bannerScript = banner?.dismissible
|
|
|
331
341
|
>
|
|
332
342
|
<slot name="ask" slot="ask" />
|
|
333
343
|
</HeaderSlot>
|
|
334
|
-
<div class="mx-auto grid grid-cols-1 items-start
|
|
344
|
+
<div class:list={["mx-auto grid grid-cols-1 items-start", gridClass]}>
|
|
335
345
|
<aside
|
|
336
346
|
aria-label="Primary"
|
|
337
347
|
class="fixed top-16 start-0 z-[35] h-[calc(100dvh-4rem)] w-64 max-w-[80vw] -translate-x-[105%] overflow-y-auto border-border border-e bg-background px-5 pt-4 pb-6 transition-transform rtl:translate-x-[105%] [:where([data-blume-nav-open])_&]:translate-x-0! lg:sticky lg:z-auto lg:w-auto lg:max-w-none lg:translate-x-0! lg:border-e-0 lg:bg-transparent lg:px-4"
|
|
@@ -354,47 +364,59 @@ const bannerScript = banner?.dismissible
|
|
|
354
364
|
</nav>
|
|
355
365
|
</aside>
|
|
356
366
|
<main class="min-w-0 px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
|
|
357
|
-
<BreadcrumbsSlot crumbs={crumbs} />
|
|
367
|
+
<BreadcrumbsSlot crumbs={crumbs} wide={isApiOperation} />
|
|
358
368
|
<TableOfContentsSlot
|
|
359
369
|
headings={tocHeadings}
|
|
360
370
|
title={strings.toc.title}
|
|
361
371
|
variant="mobile"
|
|
362
372
|
/>
|
|
363
373
|
<PageHeaderSlot headings={tocHeadings} page={page} route={page.route} />
|
|
364
|
-
<article class=
|
|
374
|
+
<article class:list={[articleClass]} dir={contentDir}>
|
|
365
375
|
<slot />
|
|
366
376
|
</article>
|
|
367
|
-
<PageFooterSlot headings={tocHeadings} page={page} route={page.route} />
|
|
368
377
|
{
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
378
|
+
!isApiOperation && (
|
|
379
|
+
<>
|
|
380
|
+
<PageFooterSlot
|
|
381
|
+
headings={tocHeadings}
|
|
382
|
+
page={page}
|
|
383
|
+
route={page.route}
|
|
384
|
+
/>
|
|
385
|
+
{formattedLastModified && (
|
|
386
|
+
<p class="mx-auto mt-10 max-w-[42rem] text-muted-foreground text-sm">
|
|
387
|
+
{strings.page.lastUpdated} {formattedLastModified}
|
|
388
|
+
</p>
|
|
389
|
+
)}
|
|
390
|
+
{feedback && <FeedbackSlot strings={strings.feedback} />}
|
|
391
|
+
<PaginationSlot next={next} prev={prev} strings={strings.page} />
|
|
392
|
+
</>
|
|
373
393
|
)
|
|
374
394
|
}
|
|
375
|
-
{feedback && <FeedbackSlot strings={strings.feedback} />}
|
|
376
|
-
<PaginationSlot next={next} prev={prev} strings={strings.page} />
|
|
377
395
|
</main>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
{
|
|
397
|
+
!isApiOperation && (
|
|
398
|
+
<aside
|
|
399
|
+
aria-label={strings.toc.title}
|
|
400
|
+
class="sticky top-16 hidden h-[calc(100dvh-4rem)] overflow-y-auto px-4 pt-6 pb-10 text-sm xl:block"
|
|
401
|
+
>
|
|
402
|
+
<TableOfContentsSlot
|
|
403
|
+
headings={tocHeadings}
|
|
404
|
+
title={strings.toc.title}
|
|
405
|
+
variant="desktop"
|
|
406
|
+
/>
|
|
407
|
+
<PageActions
|
|
408
|
+
askEnabled={askEnabled}
|
|
409
|
+
editUrl={editUrl}
|
|
410
|
+
exportEpub={exportEpub}
|
|
411
|
+
exportPdf={exportPdf}
|
|
412
|
+
mcpName={mcp?.name}
|
|
413
|
+
mcpUrl={mcpUrl}
|
|
414
|
+
route={page.route}
|
|
415
|
+
strings={strings.actions}
|
|
416
|
+
/>
|
|
417
|
+
</aside>
|
|
418
|
+
)
|
|
419
|
+
}
|
|
398
420
|
</div>
|
|
399
421
|
<FooterSlot navigation={navigation} site={site} ui={strings} />
|
|
400
422
|
{
|
|
@@ -414,7 +436,7 @@ const bannerScript = banner?.dismissible
|
|
|
414
436
|
type="button"
|
|
415
437
|
></button>
|
|
416
438
|
<script>
|
|
417
|
-
import { icons } from "../../theme/icons.ts";
|
|
439
|
+
import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
|
|
418
440
|
// Registers the <blume-mermaid> custom element (emitted by ```mermaid
|
|
419
441
|
// fences). Mermaid itself is lazy-loaded only on pages that use a diagram.
|
|
420
442
|
import "../content/mermaid-element.ts";
|
|
@@ -138,7 +138,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
138
138
|
/>
|
|
139
139
|
|
|
140
140
|
<script>
|
|
141
|
-
import { icons } from "../../theme/icons.ts";
|
|
141
|
+
import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
|
|
142
142
|
import {
|
|
143
143
|
escapeHtml,
|
|
144
144
|
highlight,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
import specs from "blume:openapi";
|
|
3
|
+
import MethodBadge from "./MethodBadge.astro";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
source: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { source } = Astro.props;
|
|
10
|
+
const spec = specs[source];
|
|
11
|
+
const operations = Object.values(spec?.operations ?? {});
|
|
12
|
+
const servers =
|
|
13
|
+
((spec?.document ?? {}) as { servers?: { url?: string }[] }).servers ?? [];
|
|
14
|
+
|
|
15
|
+
// Tags in declared order, then any operation tag not listed under `tags`.
|
|
16
|
+
const declaredSlugs = new Set(spec?.tags.map((tag) => tag.slug));
|
|
17
|
+
const extraSlugs = new Set<string>();
|
|
18
|
+
const extraTags: { slug: string; name: string; description: string }[] = [];
|
|
19
|
+
for (const operation of operations) {
|
|
20
|
+
if (!(declaredSlugs.has(operation.tagSlug) || extraSlugs.has(operation.tagSlug))) {
|
|
21
|
+
extraSlugs.add(operation.tagSlug);
|
|
22
|
+
extraTags.push({ description: "", name: operation.tag, slug: operation.tagSlug });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const sections = [...(spec?.tags ?? []), ...extraTags];
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
spec && (
|
|
30
|
+
<div>
|
|
31
|
+
{spec.version && (
|
|
32
|
+
<div class="not-prose mb-4 text-muted-foreground text-sm">
|
|
33
|
+
Version {spec.version}
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
{servers.length > 0 && (
|
|
37
|
+
<div class="not-prose mb-8 flex flex-wrap items-center gap-2">
|
|
38
|
+
<span class="text-muted-foreground text-xs">Base URL</span>
|
|
39
|
+
{servers.map((server) => (
|
|
40
|
+
<code class="rounded bg-muted px-2 py-0.5 text-foreground text-xs">
|
|
41
|
+
{server.url}
|
|
42
|
+
</code>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
{sections.map((tag) => {
|
|
47
|
+
const ops = operations.filter(
|
|
48
|
+
(operation) => operation.tagSlug === tag.slug
|
|
49
|
+
);
|
|
50
|
+
if (ops.length === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
<section class="mb-8">
|
|
55
|
+
<h2 class="mb-1 font-semibold text-foreground text-xl">
|
|
56
|
+
{tag.name}
|
|
57
|
+
</h2>
|
|
58
|
+
{tag.description && (
|
|
59
|
+
<div class="mb-3 text-muted-foreground text-sm" set:text={tag.description} />
|
|
60
|
+
)}
|
|
61
|
+
<ul class="not-prose flex list-none flex-col gap-2 p-0">
|
|
62
|
+
{ops.map((operation) => (
|
|
63
|
+
<li>
|
|
64
|
+
<a
|
|
65
|
+
class="flex items-center gap-3 rounded-blume border border-border p-3 text-inherit no-underline! transition-colors hover:border-accent hover:bg-muted hover:no-underline!"
|
|
66
|
+
href={operation.route}
|
|
67
|
+
>
|
|
68
|
+
<MethodBadge method={operation.method} />
|
|
69
|
+
<span class="font-medium text-foreground text-sm">
|
|
70
|
+
{operation.summary || operation.path}
|
|
71
|
+
</span>
|
|
72
|
+
<code class="ml-auto hidden text-muted-foreground text-xs sm:inline">
|
|
73
|
+
{operation.path}
|
|
74
|
+
</code>
|
|
75
|
+
</a>
|
|
76
|
+
</li>
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
</section>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
method: string;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { method, class: className } = Astro.props;
|
|
8
|
+
const upper = method.toUpperCase();
|
|
9
|
+
|
|
10
|
+
const COLORS: Record<string, string> = {
|
|
11
|
+
DELETE: "bg-red-500/15 text-red-700 dark:text-red-300",
|
|
12
|
+
GET: "bg-green-500/15 text-green-700 dark:text-green-300",
|
|
13
|
+
HEAD: "bg-muted text-muted-foreground",
|
|
14
|
+
OPTIONS: "bg-muted text-muted-foreground",
|
|
15
|
+
PATCH: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
|
|
16
|
+
POST: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
17
|
+
PUT: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
|
|
18
|
+
};
|
|
19
|
+
const color = COLORS[upper] ?? "bg-muted text-muted-foreground";
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<span
|
|
23
|
+
class:list={[
|
|
24
|
+
"not-prose inline-flex items-center rounded-md px-2 py-0.5 font-mono font-semibold text-xs uppercase tracking-wide",
|
|
25
|
+
color,
|
|
26
|
+
className,
|
|
27
|
+
]}>{upper}</span
|
|
28
|
+
>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
import specs from "blume:openapi";
|
|
3
|
+
import type { SchemaLike } from "./helpers.ts";
|
|
4
|
+
import { buildRequestSample, sampleLanguages } from "./snippets.ts";
|
|
5
|
+
import MethodBadge from "./MethodBadge.astro";
|
|
6
|
+
import ParametersTable from "./ParametersTable.astro";
|
|
7
|
+
import RequestBody from "./RequestBody.astro";
|
|
8
|
+
import RequestPanel from "./RequestPanel.astro";
|
|
9
|
+
import Responses from "./Responses.astro";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
source: string;
|
|
13
|
+
id: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ParamLike {
|
|
17
|
+
$ref?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
in?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
deprecated?: boolean;
|
|
23
|
+
schema?: SchemaLike;
|
|
24
|
+
example?: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface MediaTypeLike {
|
|
28
|
+
schema?: SchemaLike;
|
|
29
|
+
example?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FullOperation {
|
|
33
|
+
summary?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
deprecated?: boolean;
|
|
36
|
+
parameters?: ParamLike[];
|
|
37
|
+
requestBody?: {
|
|
38
|
+
description?: string;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
content?: Record<string, MediaTypeLike>;
|
|
41
|
+
};
|
|
42
|
+
responses?: Record<
|
|
43
|
+
string,
|
|
44
|
+
{ description?: string; content?: Record<string, MediaTypeLike> }
|
|
45
|
+
>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { source, id } = Astro.props;
|
|
49
|
+
const spec = specs[source];
|
|
50
|
+
const ref = spec?.operations[id];
|
|
51
|
+
|
|
52
|
+
const doc = (spec?.document ?? {}) as {
|
|
53
|
+
paths?: Record<string, Record<string, unknown> & { parameters?: ParamLike[] }>;
|
|
54
|
+
components?: {
|
|
55
|
+
schemas?: Record<string, SchemaLike>;
|
|
56
|
+
parameters?: Record<string, ParamLike>;
|
|
57
|
+
};
|
|
58
|
+
servers?: { url?: string }[];
|
|
59
|
+
};
|
|
60
|
+
const pathItem = ref ? doc.paths?.[ref.path] : undefined;
|
|
61
|
+
const operation = (
|
|
62
|
+
pathItem && ref ? pathItem[ref.method] : undefined
|
|
63
|
+
) as FullOperation | undefined;
|
|
64
|
+
|
|
65
|
+
const schemas = doc.components?.schemas ?? {};
|
|
66
|
+
const componentParams = doc.components?.parameters ?? {};
|
|
67
|
+
const PARAM_REF = /#\/components\/parameters\/(?<name>[^/]+)$/u;
|
|
68
|
+
const resolveParam = (param: ParamLike): ParamLike => {
|
|
69
|
+
if (typeof param.$ref === "string") {
|
|
70
|
+
const name = PARAM_REF.exec(param.$ref)?.groups?.name;
|
|
71
|
+
return (name && componentParams[name]) || param;
|
|
72
|
+
}
|
|
73
|
+
return param;
|
|
74
|
+
};
|
|
75
|
+
const params = [
|
|
76
|
+
...(pathItem?.parameters ?? []),
|
|
77
|
+
...(operation?.parameters ?? []),
|
|
78
|
+
].map(resolveParam);
|
|
79
|
+
|
|
80
|
+
const sample =
|
|
81
|
+
ref && operation
|
|
82
|
+
? buildRequestSample(
|
|
83
|
+
{ parameters: params, requestBody: operation.requestBody },
|
|
84
|
+
ref.method,
|
|
85
|
+
ref.path,
|
|
86
|
+
doc.servers ?? [],
|
|
87
|
+
schemas
|
|
88
|
+
)
|
|
89
|
+
: null;
|
|
90
|
+
const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
!(spec && ref && operation) ? (
|
|
95
|
+
<div class="text-muted-foreground">This API operation could not be found.</div>
|
|
96
|
+
) : (
|
|
97
|
+
<div class="not-prose">
|
|
98
|
+
<div class="mb-6 flex flex-wrap items-center gap-3">
|
|
99
|
+
<MethodBadge method={ref.method} />
|
|
100
|
+
<code class="break-all font-mono text-foreground text-sm">
|
|
101
|
+
{ref.path}
|
|
102
|
+
</code>
|
|
103
|
+
{operation.deprecated && (
|
|
104
|
+
<span class="font-medium text-[0.625rem] text-orange-600 uppercase tracking-wide dark:text-orange-400">
|
|
105
|
+
deprecated
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
<div class="grid grid-cols-1 items-start gap-x-10 gap-y-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
|
|
110
|
+
<div>
|
|
111
|
+
<ParametersTable parameters={params} schemas={schemas} />
|
|
112
|
+
{operation.requestBody && (
|
|
113
|
+
<RequestBody
|
|
114
|
+
expandAll={spec.expandSchemas}
|
|
115
|
+
requestBody={operation.requestBody}
|
|
116
|
+
schemas={schemas}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
{operation.responses && (
|
|
120
|
+
<Responses
|
|
121
|
+
expandAll={spec.expandSchemas}
|
|
122
|
+
responses={operation.responses}
|
|
123
|
+
schemas={schemas}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
{sample && (
|
|
128
|
+
<div class="xl:sticky xl:top-24 xl:self-start">
|
|
129
|
+
<RequestPanel
|
|
130
|
+
languages={languages}
|
|
131
|
+
responses={operation.responses ?? {}}
|
|
132
|
+
sample={sample}
|
|
133
|
+
schemas={schemas}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
constraints,
|
|
4
|
+
resolveSchema,
|
|
5
|
+
type SchemaLike,
|
|
6
|
+
typeLabel,
|
|
7
|
+
} from "./helpers.ts";
|
|
8
|
+
|
|
9
|
+
interface ParamLike {
|
|
10
|
+
name?: string;
|
|
11
|
+
in?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
deprecated?: boolean;
|
|
15
|
+
schema?: SchemaLike;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
parameters: ParamLike[];
|
|
20
|
+
schemas: Record<string, SchemaLike>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { parameters, schemas } = Astro.props;
|
|
24
|
+
|
|
25
|
+
const SECTIONS: { in: string; title: string }[] = [
|
|
26
|
+
{ in: "path", title: "Path parameters" },
|
|
27
|
+
{ in: "query", title: "Query parameters" },
|
|
28
|
+
{ in: "header", title: "Header parameters" },
|
|
29
|
+
{ in: "cookie", title: "Cookie parameters" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const groups = SECTIONS.map((section) => ({
|
|
33
|
+
items: parameters.filter((param) => param.in === section.in),
|
|
34
|
+
title: section.title,
|
|
35
|
+
})).filter((group) => group.items.length > 0);
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
groups.map((group) => (
|
|
40
|
+
<section class="mt-6">
|
|
41
|
+
<div
|
|
42
|
+
aria-level="3"
|
|
43
|
+
class="mb-2 font-semibold text-foreground text-sm"
|
|
44
|
+
role="heading"
|
|
45
|
+
>
|
|
46
|
+
{group.title}
|
|
47
|
+
</div>
|
|
48
|
+
<div class="not-prose rounded-blume border border-border px-4">
|
|
49
|
+
{group.items.map((param) => {
|
|
50
|
+
const resolved = resolveSchema(schemas, param.schema ?? {});
|
|
51
|
+
const type = param.schema ? typeLabel(param.schema, schemas) : "string";
|
|
52
|
+
const limits = constraints(resolved);
|
|
53
|
+
const enumValues = Array.isArray(resolved.enum) ? resolved.enum : null;
|
|
54
|
+
return (
|
|
55
|
+
<div class="border-border border-t py-3 first:border-t-0">
|
|
56
|
+
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
57
|
+
<code class="font-mono text-foreground text-sm">{param.name}</code>
|
|
58
|
+
<span class="text-muted-foreground text-xs">{type}</span>
|
|
59
|
+
{param.required && (
|
|
60
|
+
<span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
|
|
61
|
+
required
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
{param.deprecated && (
|
|
65
|
+
<span class="font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide line-through">
|
|
66
|
+
deprecated
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
{param.description && (
|
|
71
|
+
<div
|
|
72
|
+
class="mt-1 text-muted-foreground text-sm"
|
|
73
|
+
set:text={param.description}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
{limits.length > 0 && (
|
|
77
|
+
<div class="mt-1 text-muted-foreground text-xs">
|
|
78
|
+
{limits.join(" · ")}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{enumValues && (
|
|
82
|
+
<div class="mt-1 flex flex-wrap items-center gap-1 text-xs">
|
|
83
|
+
<span class="text-muted-foreground">Allowed:</span>
|
|
84
|
+
{enumValues.map((value) => (
|
|
85
|
+
<code class="rounded bg-muted px-1 py-0.5 text-foreground">
|
|
86
|
+
{String(value)}
|
|
87
|
+
</code>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
))
|
|
97
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { SchemaLike } from "./helpers.ts";
|
|
3
|
+
import SchemaTable from "./SchemaTable.astro";
|
|
4
|
+
|
|
5
|
+
interface MediaTypeLike {
|
|
6
|
+
schema?: SchemaLike;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface RequestBodyLike {
|
|
10
|
+
description?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
content?: Record<string, MediaTypeLike>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
requestBody: RequestBodyLike;
|
|
17
|
+
schemas: Record<string, SchemaLike>;
|
|
18
|
+
expandAll?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { requestBody, schemas, expandAll = false } = Astro.props;
|
|
22
|
+
|
|
23
|
+
const entries = Object.entries(requestBody.content ?? {});
|
|
24
|
+
const chosen = entries.find(([type]) => type.includes("json")) ?? entries[0];
|
|
25
|
+
const contentType = chosen?.[0] ?? "application/json";
|
|
26
|
+
const schema = chosen?.[1]?.schema ?? {};
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<section class="mt-8">
|
|
30
|
+
<div class="mb-2 flex flex-wrap items-center gap-2">
|
|
31
|
+
<div
|
|
32
|
+
aria-level="2"
|
|
33
|
+
class="font-semibold text-foreground text-lg"
|
|
34
|
+
role="heading"
|
|
35
|
+
>
|
|
36
|
+
Request body
|
|
37
|
+
</div>
|
|
38
|
+
{
|
|
39
|
+
requestBody.required && (
|
|
40
|
+
<span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
|
|
41
|
+
required
|
|
42
|
+
</span>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
<code class="text-muted-foreground text-xs">{contentType}</code>
|
|
46
|
+
</div>
|
|
47
|
+
{
|
|
48
|
+
requestBody.description && (
|
|
49
|
+
<div
|
|
50
|
+
class="text-muted-foreground text-sm"
|
|
51
|
+
set:text={requestBody.description}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
<div class="mt-3">
|
|
56
|
+
<SchemaTable expandAll={expandAll} schema={schema} schemas={schemas} />
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|