@umami/shiso 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/shiso.mjs +20 -1
- package/dist/chunks/App.js +360 -26
- package/dist/chunks/docs.js +2 -2
- package/dist/entry-client.js +1 -1
- package/dist/entry-server.js +30 -2
- package/docs.schema.json +90 -0
- package/mdx.config.ts +13 -97
- package/package.json +5 -4
- package/scripts/build-runtime.mjs +1 -0
- package/scripts/check-content.mjs +358 -0
- package/scripts/expand-navigation-globs.mjs +208 -0
- package/scripts/expand-openapi-navigation.mjs +94 -0
- package/scripts/generate-openapi.mjs +117 -0
- package/scripts/generate-search-index.mjs +31 -1
- package/scripts/lib/openapi.mjs +653 -0
- package/scripts/load-docs-config.mjs +52 -6
- package/scripts/load-shiso-config.mjs +45 -3
- package/scripts/prerender.mjs +83 -3
- package/scripts/vite-docs-config.mjs +27 -6
- package/src/App.tsx +0 -1
- package/src/components/CodeBlock.tsx +72 -8
- package/src/components/DocContent.tsx +18 -0
- package/src/components/Docs.tsx +6 -1
- package/src/components/OpenApiOperation.tsx +197 -0
- package/src/components/SideNav.tsx +8 -2
- package/src/components/docs/CodeGroup.tsx +6 -2
- package/src/entry-server.tsx +50 -0
- package/src/lib/code-blocks.ts +18 -0
- package/src/lib/code-meta.ts +87 -0
- package/src/lib/docs-config.ts +4 -1
- package/src/lib/openapi.generated.ts +4 -0
- package/src/lib/openapi.ts +63 -0
- package/src/lib/rehype-shiki.ts +196 -0
- package/src/lib/site-model.ts +2 -0
- package/src/lib/types.ts +116 -2
- package/src/styles/global.css +63 -73
- package/types/config.d.ts +11 -4
- package/vite.config.ts +49 -3
- package/CHANGELOG.md +0 -171
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { CodeBlock } from '@/components/CodeBlock';
|
|
2
|
+
import { Badge } from '@/components/docs/Badge';
|
|
3
|
+
import { CodeGroup } from '@/components/docs/CodeGroup';
|
|
4
|
+
import { Expandable } from '@/components/docs/Expandable';
|
|
5
|
+
import { ParamField } from '@/components/docs/ParamField';
|
|
6
|
+
import { ResponseField } from '@/components/docs/ResponseField';
|
|
7
|
+
import { operationSections, statusColor } from '@/lib/openapi';
|
|
8
|
+
import type { NormalizedOperation, SchemaNode } from '@/lib/types';
|
|
9
|
+
|
|
10
|
+
type ParamLocation = 'path' | 'query' | 'header' | 'cookie';
|
|
11
|
+
|
|
12
|
+
const PARAM_LOCATIONS: ParamLocation[] = ['path', 'query', 'header', 'cookie'];
|
|
13
|
+
|
|
14
|
+
function FieldChildren({ node }: { node: SchemaNode }) {
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
{node.description}
|
|
18
|
+
{node.enum && node.enum.length > 0 && (
|
|
19
|
+
<div className="mt-1">
|
|
20
|
+
Options:{' '}
|
|
21
|
+
{node.enum.map((value, index) => (
|
|
22
|
+
<span key={value}>
|
|
23
|
+
{index > 0 && ', '}
|
|
24
|
+
<code>{value}</code>
|
|
25
|
+
</span>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
)}
|
|
29
|
+
{node.children && node.children.length > 0 && (
|
|
30
|
+
<Expandable title="properties">
|
|
31
|
+
{node.children.map(child => (
|
|
32
|
+
<SchemaField key={child.name || child.type} node={child} />
|
|
33
|
+
))}
|
|
34
|
+
</Expandable>
|
|
35
|
+
)}
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function SchemaField({ node }: { node: SchemaNode }) {
|
|
41
|
+
return (
|
|
42
|
+
<ResponseField
|
|
43
|
+
name={node.name || node.type}
|
|
44
|
+
type={node.name ? node.type : undefined}
|
|
45
|
+
required={node.required}
|
|
46
|
+
deprecated={node.deprecated}
|
|
47
|
+
default={node.default}
|
|
48
|
+
>
|
|
49
|
+
<FieldChildren node={node} />
|
|
50
|
+
</ResponseField>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Renders a schema tree: a root object's properties, or the node itself. */
|
|
55
|
+
function SchemaFields({ node }: { node: SchemaNode }) {
|
|
56
|
+
if (!node.name && node.children?.length) {
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
{node.children.map(child => (
|
|
60
|
+
<SchemaField key={child.name || child.type} node={child} />
|
|
61
|
+
))}
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return <SchemaField node={node} />;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function HighlightedCode({
|
|
70
|
+
language,
|
|
71
|
+
title,
|
|
72
|
+
html,
|
|
73
|
+
source,
|
|
74
|
+
lineCount,
|
|
75
|
+
}: {
|
|
76
|
+
language: string;
|
|
77
|
+
title?: string;
|
|
78
|
+
html?: string;
|
|
79
|
+
source: string;
|
|
80
|
+
lineCount?: number;
|
|
81
|
+
}) {
|
|
82
|
+
return (
|
|
83
|
+
<CodeBlock
|
|
84
|
+
data-language={language}
|
|
85
|
+
data-title={title}
|
|
86
|
+
data-line-count={lineCount ? String(lineCount) : undefined}
|
|
87
|
+
>
|
|
88
|
+
{html ? (
|
|
89
|
+
// Highlighted at build time by the openapi generator; same markup as
|
|
90
|
+
// the rehype-shiki pipeline produces for fenced code blocks.
|
|
91
|
+
<code
|
|
92
|
+
className={`language-${language}`}
|
|
93
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: build-time generated markup from the project's own OpenAPI spec.
|
|
94
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
95
|
+
/>
|
|
96
|
+
) : (
|
|
97
|
+
<code className={`language-${language}`}>{source}</code>
|
|
98
|
+
)}
|
|
99
|
+
</CodeBlock>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface OpenApiOperationProps {
|
|
104
|
+
operation: NormalizedOperation;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The generated reference for one API operation, rendered under the page body. */
|
|
108
|
+
export function OpenApiOperation({ operation }: OpenApiOperationProps) {
|
|
109
|
+
const sections = new Map(operationSections(operation).map(entry => [entry.name, entry.id]));
|
|
110
|
+
const showParameters = sections.has('Parameters');
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="docs-markdown">
|
|
114
|
+
{showParameters && (
|
|
115
|
+
<section>
|
|
116
|
+
<h2 id={sections.get('Parameters')}>Parameters</h2>
|
|
117
|
+
{operation.security.length > 0 && (
|
|
118
|
+
<ParamField header="Authorization" type="string" required>
|
|
119
|
+
Authentication credentials, e.g. <code>Bearer <token></code> (
|
|
120
|
+
{operation.security.join(', ')}).
|
|
121
|
+
</ParamField>
|
|
122
|
+
)}
|
|
123
|
+
{PARAM_LOCATIONS.map(location =>
|
|
124
|
+
operation.parameters[location].map(parameter => (
|
|
125
|
+
<ParamField
|
|
126
|
+
key={`${location}-${parameter.name}`}
|
|
127
|
+
{...{ [location]: parameter.name }}
|
|
128
|
+
type={parameter.type}
|
|
129
|
+
required={parameter.required}
|
|
130
|
+
>
|
|
131
|
+
<FieldChildren node={{ ...parameter, name: undefined, description: undefined }} />
|
|
132
|
+
{parameter.description}
|
|
133
|
+
</ParamField>
|
|
134
|
+
)),
|
|
135
|
+
)}
|
|
136
|
+
</section>
|
|
137
|
+
)}
|
|
138
|
+
{operation.requestBody && (
|
|
139
|
+
<section>
|
|
140
|
+
<h2 id={sections.get('Request body')}>Request body</h2>
|
|
141
|
+
<SchemaFields node={operation.requestBody.schema} />
|
|
142
|
+
{operation.requestBody.example && (
|
|
143
|
+
<HighlightedCode
|
|
144
|
+
language="json"
|
|
145
|
+
title="Example request"
|
|
146
|
+
html={operation.requestBody.exampleHtml}
|
|
147
|
+
source={operation.requestBody.example}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
</section>
|
|
151
|
+
)}
|
|
152
|
+
{operation.responses.length > 0 && (
|
|
153
|
+
<section>
|
|
154
|
+
<h2 id={sections.get('Responses')}>Responses</h2>
|
|
155
|
+
{operation.responses.map(response => (
|
|
156
|
+
<div key={response.status} className="mt-6 first:mt-0">
|
|
157
|
+
<div className="flex items-center gap-2">
|
|
158
|
+
<Badge color={statusColor(response.status)} size="sm">
|
|
159
|
+
{response.status}
|
|
160
|
+
</Badge>
|
|
161
|
+
{response.description && (
|
|
162
|
+
<span className="text-muted-foreground text-sm">{response.description}</span>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
{response.schema && <SchemaFields node={response.schema} />}
|
|
166
|
+
{response.example && (
|
|
167
|
+
<HighlightedCode
|
|
168
|
+
language="json"
|
|
169
|
+
title={`${response.status} example`}
|
|
170
|
+
html={response.exampleHtml}
|
|
171
|
+
source={response.example}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
176
|
+
</section>
|
|
177
|
+
)}
|
|
178
|
+
{operation.samples.length > 0 && (
|
|
179
|
+
<section>
|
|
180
|
+
<h2 id={sections.get('Code samples')}>Code samples</h2>
|
|
181
|
+
<CodeGroup>
|
|
182
|
+
{operation.samples.map(sample => (
|
|
183
|
+
<HighlightedCode
|
|
184
|
+
key={sample.language}
|
|
185
|
+
language={sample.language}
|
|
186
|
+
title={sample.label}
|
|
187
|
+
html={sample.html}
|
|
188
|
+
source={sample.source}
|
|
189
|
+
lineCount={sample.lineCount}
|
|
190
|
+
/>
|
|
191
|
+
))}
|
|
192
|
+
</CodeGroup>
|
|
193
|
+
</section>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode, useEffect, useState } from 'react';
|
|
2
2
|
import { Link, useLocation, useNavigate } from 'react-router';
|
|
3
|
+
import { Badge as MethodBadge } from '@/components/docs/Badge';
|
|
3
4
|
import { resolveIcon } from '@/components/docs/utils';
|
|
4
5
|
import { ChevronRight, ExternalLink } from '@/components/icons';
|
|
5
6
|
import { Badge } from '@/components/ui/badge';
|
|
@@ -7,6 +8,7 @@ import { Button } from '@/components/ui/button';
|
|
|
7
8
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
8
9
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
9
10
|
import { flattenNav, isNodeHidden } from '@/lib/docs-config';
|
|
11
|
+
import { methodColor } from '@/lib/openapi';
|
|
10
12
|
import type { DocsTab, NavGroupNode, NavNode } from '@/lib/types';
|
|
11
13
|
import { cn } from '@/lib/utils';
|
|
12
14
|
|
|
@@ -256,7 +258,7 @@ function NavNodes({
|
|
|
256
258
|
}
|
|
257
259
|
|
|
258
260
|
if (node.kind === 'page') {
|
|
259
|
-
const { url, label, icon, tag } = node.page;
|
|
261
|
+
const { url, label, icon, tag, method } = node.page;
|
|
260
262
|
const isSelected = url === pathname;
|
|
261
263
|
|
|
262
264
|
rendered.push(
|
|
@@ -274,7 +276,11 @@ function NavNodes({
|
|
|
274
276
|
>
|
|
275
277
|
{resolveIcon(icon)}
|
|
276
278
|
{label}
|
|
277
|
-
{
|
|
279
|
+
{method ? (
|
|
280
|
+
<MethodBadge color={methodColor(method)} size="xs" className="ml-auto font-mono">
|
|
281
|
+
{method === 'DELETE' ? 'DEL' : method}
|
|
282
|
+
</MethodBadge>
|
|
283
|
+
) : tag ? (
|
|
278
284
|
<Badge
|
|
279
285
|
variant="secondary"
|
|
280
286
|
className="ml-auto h-auto rounded-sm px-[0.35rem] py-[0.05rem] text-[0.7rem] text-muted-foreground uppercase"
|
|
@@ -39,14 +39,18 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (blocks.length === 1) {
|
|
42
|
-
return
|
|
42
|
+
return (
|
|
43
|
+
<div className={`${styles.tabs} [&_[data-slot=code-block-header]]:hidden`}>
|
|
44
|
+
{blocks[0].content}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
return (
|
|
46
50
|
<TabsPrimitive
|
|
47
51
|
value={selected}
|
|
48
52
|
onValueChange={setSelectedKey}
|
|
49
|
-
className="my-4 gap-0 overflow-hidden rounded-lg bg-card [&_[data-slot=code-block]]:my-0 [&_[data-slot=code-block]]:rounded-none [&_[data-slot=code-block]]:border-0 [&_[data-slot=code-block]]:bg-transparent"
|
|
53
|
+
className="my-4 gap-0 overflow-hidden rounded-lg bg-card [&_[data-slot=code-block]]:my-0 [&_[data-slot=code-block]]:rounded-none [&_[data-slot=code-block]]:border-0 [&_[data-slot=code-block]]:bg-transparent [&_[data-slot=code-block-header]]:hidden"
|
|
50
54
|
>
|
|
51
55
|
<TabsList
|
|
52
56
|
variant="line"
|
package/src/entry-server.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getLocaleByPathname,
|
|
12
12
|
getRedirects,
|
|
13
13
|
getSeo,
|
|
14
|
+
siteConfig,
|
|
14
15
|
siteName,
|
|
15
16
|
standalonePages,
|
|
16
17
|
} from '@/lib/site-config';
|
|
@@ -27,6 +28,13 @@ export interface SitemapEntry {
|
|
|
27
28
|
lastmod?: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export interface LlmsPage {
|
|
32
|
+
route: string;
|
|
33
|
+
filePath: string;
|
|
34
|
+
title: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
/** Base-relative routes for every scope. The prerenderer prepends the deploy base itself. */
|
|
31
39
|
export function getRoutes(): string[] {
|
|
32
40
|
return [...docsSite.pages.map(page => page.url), ...standalonePages.map(page => page.path)];
|
|
@@ -51,6 +59,46 @@ export function getMarkdownPages(): { route: string; filePath: string }[] {
|
|
|
51
59
|
];
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
/** Navigable Markdown pages used to generate llms.txt and llms-full.txt. */
|
|
63
|
+
export function getLlmsPages(): LlmsPage[] {
|
|
64
|
+
const { indexing } = getSeo();
|
|
65
|
+
const pages: LlmsPage[] = [];
|
|
66
|
+
|
|
67
|
+
for (const page of docsSite.pages) {
|
|
68
|
+
const doc = getDocModule(page.filePath);
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
doc?.frontmatter?.noindex === true ||
|
|
72
|
+
((page.hidden || getScopeForPage(docsSite, page).hidden) && indexing !== 'all')
|
|
73
|
+
) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pages.push({
|
|
78
|
+
route: page.url,
|
|
79
|
+
filePath: page.filePath,
|
|
80
|
+
title: doc?.frontmatter?.title || page.label,
|
|
81
|
+
description: doc?.frontmatter?.description,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const page of standalonePages) {
|
|
86
|
+
if (page.filePath.endsWith('.tsx')) continue;
|
|
87
|
+
|
|
88
|
+
const doc = getDocModule(page.filePath);
|
|
89
|
+
if (doc?.frontmatter?.noindex === true) continue;
|
|
90
|
+
|
|
91
|
+
pages.push({
|
|
92
|
+
route: page.path,
|
|
93
|
+
filePath: page.filePath,
|
|
94
|
+
title: doc?.frontmatter?.title || page.title || page.path,
|
|
95
|
+
description: doc?.frontmatter?.description,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return pages;
|
|
100
|
+
}
|
|
101
|
+
|
|
54
102
|
/**
|
|
55
103
|
* Absolute URLs for the sitemap, honoring `seo.indexing` and per-page
|
|
56
104
|
* noindex. Empty when the shiso.config `siteUrl` is not configured, since a sitemap
|
|
@@ -106,4 +154,6 @@ export function render(url: string): RenderResult {
|
|
|
106
154
|
return { html, head: renderHeadToString(buildHead(url)), htmlAttrs: getLocaleByPathname(url) };
|
|
107
155
|
}
|
|
108
156
|
|
|
157
|
+
export const siteDescription = siteConfig.description;
|
|
158
|
+
|
|
109
159
|
export { docsHomeUrl, siteName };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Relative import: this module is also loaded by vite.config.ts, which esbuild
|
|
2
|
+
// bundles without applying the '@/' resolve alias.
|
|
3
|
+
import type { ResolvedCodeBlockConfig, StylingConfig } from './types.ts';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_CODE_THEME = { light: 'github-light', dark: 'github-dark' } as const;
|
|
6
|
+
|
|
7
|
+
/** Applies defaults to `styling.codeBlocks` from docs.json. */
|
|
8
|
+
export function resolveCodeBlockConfig(styling?: StylingConfig): ResolvedCodeBlockConfig {
|
|
9
|
+
const codeBlocks = styling?.codeBlocks;
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
lineNumbers: codeBlocks?.lineNumbers === true,
|
|
13
|
+
theme: {
|
|
14
|
+
light: codeBlocks?.theme?.light || DEFAULT_CODE_THEME.light,
|
|
15
|
+
dark: codeBlocks?.theme?.dark || DEFAULT_CODE_THEME.dark,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses the fenced code meta string (the text after the language tag) into
|
|
3
|
+
* the directives Shiso understands:
|
|
4
|
+
*
|
|
5
|
+
* ```ts title="app.ts" {1,3-5} showLineNumbers=10
|
|
6
|
+
*
|
|
7
|
+
* Anything unrecognized becomes the title, so the bare label form used by
|
|
8
|
+
* `CodeGroup` (```bash npm) keeps working.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface CodeMeta {
|
|
12
|
+
title?: string;
|
|
13
|
+
highlightLines: number[];
|
|
14
|
+
showLineNumbers?: boolean;
|
|
15
|
+
startLine?: number;
|
|
16
|
+
/** Unrecognized tokens, in order. */
|
|
17
|
+
rest: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Splits on whitespace while keeping quoted segments (title="my file.ts") intact. */
|
|
21
|
+
function tokenize(meta: string): string[] {
|
|
22
|
+
return meta.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseLineList(list: string): number[] {
|
|
26
|
+
const lines = new Set<number>();
|
|
27
|
+
|
|
28
|
+
for (const part of list.split(',')) {
|
|
29
|
+
const range = part.trim();
|
|
30
|
+
if (!range) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const [from, to = from] = range.split('-').map(Number);
|
|
35
|
+
if (!Number.isInteger(from) || from < 1 || !Number.isInteger(to)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (let line = from; line <= to; line += 1) {
|
|
40
|
+
lines.add(line);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [...lines].sort((a, b) => a - b);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function unquote(value: string): string {
|
|
48
|
+
return value.replace(/^(["'])(.*)\1$/, '$2');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseCodeMeta(meta: string | undefined): CodeMeta {
|
|
52
|
+
const result: CodeMeta = { highlightLines: [], rest: [] };
|
|
53
|
+
const lines = new Set<number>();
|
|
54
|
+
|
|
55
|
+
for (const token of tokenize((meta || '').trim())) {
|
|
56
|
+
const highlight = token.match(/^\{([\d,\s-]+)\}$/);
|
|
57
|
+
const title = token.match(/^title=(.+)$/);
|
|
58
|
+
const numbered = token.match(/^showLineNumbers(?:=(\d+))?$/);
|
|
59
|
+
|
|
60
|
+
if (highlight) {
|
|
61
|
+
for (const line of parseLineList(highlight[1])) {
|
|
62
|
+
lines.add(line);
|
|
63
|
+
}
|
|
64
|
+
} else if (title) {
|
|
65
|
+
result.title = unquote(title[1]);
|
|
66
|
+
} else if (numbered) {
|
|
67
|
+
result.showLineNumbers = true;
|
|
68
|
+
if (numbered[1]) {
|
|
69
|
+
result.startLine = Number(numbered[1]);
|
|
70
|
+
}
|
|
71
|
+
} else if (token === 'hideLineNumbers') {
|
|
72
|
+
result.showLineNumbers = false;
|
|
73
|
+
} else {
|
|
74
|
+
result.rest.push(unquote(token));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
result.highlightLines = [...lines].sort((a, b) => a - b);
|
|
79
|
+
|
|
80
|
+
// Back-compat: a bare label (```bash npm) is the title, which CodeGroup uses
|
|
81
|
+
// as the tab name.
|
|
82
|
+
if (!result.title && result.rest.length) {
|
|
83
|
+
result.title = result.rest.join(' ');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
package/src/lib/docs-config.ts
CHANGED
|
@@ -391,6 +391,7 @@ interface PendingPage {
|
|
|
391
391
|
hidden?: boolean;
|
|
392
392
|
icon?: string;
|
|
393
393
|
tag?: string;
|
|
394
|
+
method?: string;
|
|
394
395
|
}
|
|
395
396
|
|
|
396
397
|
type PendingNode =
|
|
@@ -424,7 +425,7 @@ function addPage(
|
|
|
424
425
|
pageRef: string,
|
|
425
426
|
context: WalkContext,
|
|
426
427
|
state: WalkState,
|
|
427
|
-
extra: { label?: string; icon?: string; tag?: string; hidden?: boolean } = {},
|
|
428
|
+
extra: { label?: string; icon?: string; tag?: string; method?: string; hidden?: boolean } = {},
|
|
428
429
|
): number {
|
|
429
430
|
const { fileSlug, slug } = normalizePageReference(pageRef);
|
|
430
431
|
const order = state.order.value++;
|
|
@@ -440,6 +441,7 @@ function addPage(
|
|
|
440
441
|
hidden: extra.hidden || context.hidden || undefined,
|
|
441
442
|
icon: extra.icon,
|
|
442
443
|
tag: extra.tag,
|
|
444
|
+
method: extra.method,
|
|
443
445
|
});
|
|
444
446
|
|
|
445
447
|
return order;
|
|
@@ -497,6 +499,7 @@ function collectPages(items: PageItem[], context: WalkContext, state: WalkState)
|
|
|
497
499
|
label,
|
|
498
500
|
icon: typeof item.icon === 'string' ? item.icon : undefined,
|
|
499
501
|
tag: typeof item.tag === 'string' ? item.tag : undefined,
|
|
502
|
+
method: typeof item.method === 'string' ? item.method : undefined,
|
|
500
503
|
hidden: item.hidden === true,
|
|
501
504
|
}),
|
|
502
505
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime helpers for OpenAPI reference pages. Operation data is produced at
|
|
3
|
+
* build time by scripts/generate-openapi.mjs and reaches the client through
|
|
4
|
+
* the aliased openapi.generated module.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { BadgeColor } from '@/components/docs/Badge';
|
|
8
|
+
import { OPENAPI_OPERATIONS } from '@/lib/openapi.generated';
|
|
9
|
+
import { createSlugger } from '@/lib/slug';
|
|
10
|
+
import type { NormalizedOperation, TocEntry } from '@/lib/types';
|
|
11
|
+
|
|
12
|
+
export const METHOD_COLORS: Record<string, BadgeColor> = {
|
|
13
|
+
GET: 'green',
|
|
14
|
+
POST: 'blue',
|
|
15
|
+
PUT: 'orange',
|
|
16
|
+
PATCH: 'purple',
|
|
17
|
+
DELETE: 'red',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function methodColor(method?: string): BadgeColor {
|
|
21
|
+
return (method && METHOD_COLORS[method.toUpperCase()]) || 'gray';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function statusColor(status: string): BadgeColor {
|
|
25
|
+
if (status.startsWith('2')) return 'green';
|
|
26
|
+
if (status.startsWith('3')) return 'blue';
|
|
27
|
+
if (status.startsWith('4') || status.startsWith('5')) return 'red';
|
|
28
|
+
return 'gray';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Looks up the operation bound by an `openapi:` frontmatter value. */
|
|
32
|
+
export function getOperation(key?: unknown): NormalizedOperation | undefined {
|
|
33
|
+
if (typeof key !== 'string' || !key.trim()) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const [method, ...rest] = key.trim().split(/\s+/);
|
|
38
|
+
return OPENAPI_OPERATIONS[`${method.toUpperCase()} ${rest.join(' ')}`];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function hasParameters(operation: NormalizedOperation): boolean {
|
|
42
|
+
const { query, path, header, cookie } = operation.parameters;
|
|
43
|
+
return (
|
|
44
|
+
query.length + path.length + header.length + cookie.length > 0 || operation.security.length > 0
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Section headings for an operation, in render order. The single source of
|
|
50
|
+
* truth for section ids: the component, the table of contents, the content
|
|
51
|
+
* checker, and the search indexer all derive their anchors from these labels.
|
|
52
|
+
*/
|
|
53
|
+
export function operationSections(operation: NormalizedOperation): TocEntry[] {
|
|
54
|
+
const slugger = createSlugger();
|
|
55
|
+
const names = [
|
|
56
|
+
hasParameters(operation) ? 'Parameters' : undefined,
|
|
57
|
+
operation.requestBody ? 'Request body' : undefined,
|
|
58
|
+
operation.responses.length ? 'Responses' : undefined,
|
|
59
|
+
operation.samples.length ? 'Code samples' : undefined,
|
|
60
|
+
].filter((name): name is string => Boolean(name));
|
|
61
|
+
|
|
62
|
+
return names.map(name => ({ name, id: slugger.slug(name), size: 2 }));
|
|
63
|
+
}
|