@scalar/openapi-to-markdown 1.4.0 → 1.5.1
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/CHANGELOG.md +32 -0
- package/README.md +66 -5
- package/dist/create-markdown-from-openapi.d.ts.map +1 -1
- package/dist/create-markdown-from-openapi.js +3 -2
- package/dist/document-anchors.d.ts +8 -0
- package/dist/document-anchors.d.ts.map +1 -0
- package/dist/document-anchors.js +25 -0
- package/dist/document-examples.d.ts +14 -0
- package/dist/document-examples.d.ts.map +1 -0
- package/dist/document-examples.js +54 -0
- package/dist/get-markdown-examples.d.ts +2 -0
- package/dist/get-markdown-examples.d.ts.map +1 -1
- package/dist/get-markdown-examples.js +11 -12
- package/dist/markdown-nodes.d.ts +22 -1
- package/dist/markdown-nodes.d.ts.map +1 -1
- package/dist/markdown-nodes.js +4 -0
- package/dist/parse-description.d.ts +2 -0
- package/dist/parse-description.d.ts.map +1 -1
- package/dist/parse-description.js +28 -1
- package/dist/render-document.d.ts +2 -2
- package/dist/render-document.d.ts.map +1 -1
- package/dist/render-document.js +217 -84
- package/dist/render-examples.d.ts +9 -1
- package/dist/render-examples.d.ts.map +1 -1
- package/dist/render-examples.js +26 -8
- package/dist/render-operation-details.d.ts +3 -2
- package/dist/render-operation-details.d.ts.map +1 -1
- package/dist/render-operation-details.js +8 -5
- package/dist/render-operation.d.ts +16 -1
- package/dist/render-operation.d.ts.map +1 -1
- package/dist/render-operation.js +168 -42
- package/dist/render-schema.d.ts +30 -3
- package/dist/render-schema.d.ts.map +1 -1
- package/dist/render-schema.js +474 -111
- package/dist/render-security.d.ts +7 -3
- package/dist/render-security.d.ts.map +1 -1
- package/dist/render-security.js +61 -12
- package/dist/select-document.d.ts +34 -3
- package/dist/select-document.d.ts.map +1 -1
- package/dist/select-document.js +58 -30
- package/package.json +18 -9
package/dist/render-document.js
CHANGED
|
@@ -1,117 +1,250 @@
|
|
|
1
|
+
import { isObject } from '@scalar/helpers/object/is-object';
|
|
1
2
|
import { forEachPathItemOperation, getResolvedPathItem, } from '@scalar/workspace-store/helpers/for-each-path-item-operation';
|
|
2
3
|
import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
3
4
|
import remarkGfm from 'remark-gfm';
|
|
4
5
|
import remarkStringify from 'remark-stringify';
|
|
5
6
|
import { unified } from 'unified';
|
|
7
|
+
import { anchor, createDocumentAnchors } from './document-anchors.js';
|
|
8
|
+
import { createDocumentExamples } from './document-examples.js';
|
|
6
9
|
import { field, heading, inlineCode, item, link, list, paragraph, strong, text } from './markdown-nodes.js';
|
|
7
|
-
import { createDescriptionParser } from './parse-description.js';
|
|
10
|
+
import { createDescriptionParser, expandDescriptions } from './parse-description.js';
|
|
8
11
|
import { renderExamples } from './render-examples.js';
|
|
9
|
-
import { renderOperation } from './render-operation.js';
|
|
12
|
+
import { formatOperationMethod, renderOperation } from './render-operation.js';
|
|
10
13
|
import { createSchemaRenderer } from './render-schema.js';
|
|
11
14
|
import { renderSecurity } from './render-security.js';
|
|
12
|
-
const serializer = unified()
|
|
15
|
+
const serializer = unified()
|
|
16
|
+
.use(remarkGfm)
|
|
17
|
+
.use(remarkStringify, {
|
|
18
|
+
bullet: '-',
|
|
19
|
+
join: [
|
|
20
|
+
(left, right, parent) => {
|
|
21
|
+
if (!('spread' in parent))
|
|
22
|
+
return undefined;
|
|
23
|
+
// Inside a tight list item, content after a nested list would otherwise continue its last item.
|
|
24
|
+
if (left.type === 'list' && right.type !== 'list')
|
|
25
|
+
return 1;
|
|
26
|
+
// Code blocks in descriptions read better apart from the prose around them.
|
|
27
|
+
if (left.type === 'code' || right.type === 'code')
|
|
28
|
+
return 1;
|
|
29
|
+
return undefined;
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
.freeze();
|
|
34
|
+
/** Authored values must survive even when the model does not generate an object example. */
|
|
35
|
+
const hasAuthoredExamples = (view) => isObject(view.schema) &&
|
|
36
|
+
(view.schema.example !== undefined || (Array.isArray(view.schema.examples) && view.schema.examples.length > 0));
|
|
37
|
+
/**
|
|
38
|
+
* Refer to a model that an operation or an earlier model on the same page already expanded.
|
|
39
|
+
* Keep what that expansion did not print: a title, and a description that a reference sibling replaced.
|
|
40
|
+
*/
|
|
41
|
+
const renderShownModel = async (name, view, previous, description) => {
|
|
42
|
+
const label = view.title && view.title !== name
|
|
43
|
+
? [strong(text(view.title)), text(' ('), inlineCode(name), text(')')]
|
|
44
|
+
: [inlineCode(name)];
|
|
45
|
+
label.push(text(' — shown above'));
|
|
46
|
+
if (previous.name !== name)
|
|
47
|
+
label.push(text(' as '), inlineCode(previous.name));
|
|
48
|
+
label.push(text('.'));
|
|
49
|
+
const blocks = [paragraph(...label)];
|
|
50
|
+
if (view.description && view.description !== previous.description)
|
|
51
|
+
blocks.push(...(await description(view.description)));
|
|
52
|
+
return item(...blocks);
|
|
53
|
+
};
|
|
13
54
|
/** Build Markdown directly, retaining caches only for this immutable document snapshot. */
|
|
14
55
|
export const createDocumentRenderer = () => {
|
|
15
56
|
const descriptions = createDescriptionParser();
|
|
16
57
|
const schemaRenderer = createSchemaRenderer();
|
|
17
|
-
return async (document, options) => {
|
|
58
|
+
return async (document, options = {}) => {
|
|
18
59
|
const description = descriptions();
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (info.contact) {
|
|
29
|
-
const contact = [text(info.contact.name ?? '')];
|
|
30
|
-
metadata.push(item(paragraph(strong(text('Contact:')), text(' '), ...contact, ...(info.contact.url ? [text(' '), link(info.contact.url, info.contact.url)] : []), ...(info.contact.email ? [text(' '), link(`mailto:${info.contact.email}`, info.contact.email)] : []))));
|
|
31
|
-
}
|
|
32
|
-
if (info.license)
|
|
33
|
-
metadata.push(field('License', info.license.url ? link(info.license.url, info.license.name ?? '') : text(info.license.name)));
|
|
34
|
-
const nodes = [
|
|
35
|
-
heading(1, text(info.title)),
|
|
36
|
-
list(metadata),
|
|
37
|
-
...(await description(info.description)),
|
|
38
|
-
];
|
|
39
|
-
if (document.servers?.length) {
|
|
40
|
-
nodes.push(heading(2, text('Servers')));
|
|
41
|
-
const servers = document.servers.map((server) => {
|
|
42
|
-
const nested = [];
|
|
43
|
-
if (server.description)
|
|
44
|
-
nested.push(field('Description', text(server.description)));
|
|
45
|
-
const variables = Object.entries(server.variables ?? {});
|
|
46
|
-
if (variables.length)
|
|
47
|
-
nested.push(item(paragraph(strong(text('Variables:'))), list(variables.map(([name, variable]) => item(paragraph(inlineCode(name), text(' (default: '), inlineCode(variable.default), text(`)${variable.description ? `: ${variable.description}` : ''}`)))))));
|
|
48
|
-
const entry = field('URL', inlineCode(server.url));
|
|
49
|
-
if (nested.length)
|
|
50
|
-
entry.children.push(list(nested));
|
|
51
|
-
return entry;
|
|
52
|
-
});
|
|
53
|
-
nodes.push(list(servers));
|
|
54
|
-
}
|
|
55
|
-
nodes.push(...(await renderSecurity(document.security, document.components?.securitySchemes, description)));
|
|
56
|
-
if (document.tags?.length) {
|
|
57
|
-
nodes.push(heading(2, text('Tags')));
|
|
58
|
-
for (const tag of document.tags) {
|
|
59
|
-
nodes.push(heading(3, text(tag.name)), ...(await description(tag.description)));
|
|
60
|
-
if (tag.externalDocs)
|
|
61
|
-
nodes.push(paragraph(link(tag.externalDocs.url, tag.externalDocs.description ?? tag.externalDocs.url)));
|
|
60
|
+
const whole = [options.operation, options.webhook, options.model, options.tag, options.introduction].every((selector) => selector === undefined);
|
|
61
|
+
const anchors = createDocumentAnchors();
|
|
62
|
+
const examples = whole ? createDocumentExamples(anchors) : undefined;
|
|
63
|
+
const documentContext = whole
|
|
64
|
+
? {
|
|
65
|
+
anchors,
|
|
66
|
+
servers: document.servers !== undefined ? anchors.get('context', 'global-servers') : undefined,
|
|
67
|
+
authentication: document.security !== undefined ? anchors.get('context', 'global-authentication') : undefined,
|
|
68
|
+
pathServers: new WeakMap(),
|
|
62
69
|
}
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
flush();
|
|
72
|
-
for (const group of [
|
|
70
|
+
: undefined;
|
|
71
|
+
const destinations = whole
|
|
72
|
+
? new Map(Object.keys(document.components?.schemas ?? {}).map((name) => [
|
|
73
|
+
name,
|
|
74
|
+
`#${encodeURIComponent(anchors.get('schema', name))}`,
|
|
75
|
+
]))
|
|
76
|
+
: undefined;
|
|
77
|
+
const groups = [
|
|
73
78
|
{ title: 'Operations', paths: document.paths, webhook: false },
|
|
74
79
|
{ title: 'Webhooks', paths: document.webhooks, webhook: true },
|
|
75
|
-
]) {
|
|
76
|
-
|
|
80
|
+
].map((group) => {
|
|
81
|
+
const entries = [];
|
|
77
82
|
for (const [path, reference] of Object.entries(group.paths ?? {})) {
|
|
78
83
|
const pathItem = getResolvedPathItem(reference);
|
|
79
84
|
if (!pathItem)
|
|
80
85
|
continue;
|
|
81
|
-
const entries = [];
|
|
82
86
|
forEachPathItemOperation(reference, (method, operation) => {
|
|
83
|
-
|
|
87
|
+
const label = `${formatOperationMethod(method)} ${path}`;
|
|
88
|
+
entries.push({
|
|
89
|
+
path,
|
|
90
|
+
method,
|
|
91
|
+
pathItem,
|
|
92
|
+
operation: getResolvedRef(operation, mergeSiblingReferences),
|
|
93
|
+
label,
|
|
94
|
+
id: whole
|
|
95
|
+
? anchors.get(group.webhook ? 'webhook' : 'operation', JSON.stringify([path, method]), label)
|
|
96
|
+
: undefined,
|
|
97
|
+
});
|
|
84
98
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
}
|
|
100
|
+
return { ...group, entries };
|
|
101
|
+
});
|
|
102
|
+
// Each page expands a shared schema once, then refers back to it.
|
|
103
|
+
const schemas = schemaRenderer.forDocument(document.components?.schemas, options, destinations);
|
|
104
|
+
const openapiVersion = document['x-original-oas-version'] ?? document.openapi;
|
|
105
|
+
// A page for one operation, webhook or model starts with that item, not with the API.
|
|
106
|
+
const single = options.operation !== undefined || options.webhook !== undefined || options.model !== undefined;
|
|
107
|
+
const nodes = [];
|
|
108
|
+
const sections = [];
|
|
109
|
+
const flush = async () => {
|
|
110
|
+
if (!nodes.length)
|
|
111
|
+
return;
|
|
112
|
+
const children = nodes.splice(0);
|
|
113
|
+
await expandDescriptions(children, description);
|
|
114
|
+
sections.push(serializer.stringify({ type: 'root', children }).trimEnd());
|
|
115
|
+
};
|
|
116
|
+
if (!single) {
|
|
117
|
+
const { info } = document;
|
|
118
|
+
const metadata = [field('OpenAPI Version', inlineCode(document.openapi))];
|
|
119
|
+
if (info.version)
|
|
120
|
+
metadata.push(field('API Version', inlineCode(info.version)));
|
|
121
|
+
if (info.termsOfService)
|
|
122
|
+
metadata.push(field('Terms of service', link(info.termsOfService, info.termsOfService)));
|
|
123
|
+
if (info.contact) {
|
|
124
|
+
const contact = [text(info.contact.name ?? '')];
|
|
125
|
+
metadata.push(item(paragraph(strong(text('Contact:')), text(' '), ...contact, ...(info.contact.url ? [text(' '), link(info.contact.url, info.contact.url)] : []), ...(info.contact.email ? [text(' '), link(`mailto:${info.contact.email}`, info.contact.email)] : []))));
|
|
126
|
+
}
|
|
127
|
+
if (info.license)
|
|
128
|
+
metadata.push(field('License', info.license.url ? link(info.license.url, info.license.name ?? '') : text(info.license.name)));
|
|
129
|
+
nodes.push(heading(1, text(info.title)), list(metadata), ...(await description(info.description)));
|
|
130
|
+
if (whole) {
|
|
131
|
+
const contents = [];
|
|
132
|
+
for (const group of groups) {
|
|
133
|
+
if (!group.entries.length)
|
|
134
|
+
continue;
|
|
135
|
+
contents.push(paragraph(strong(text(group.title))), list(group.entries.map(({ id, label }) => item(paragraph(link(`#${encodeURIComponent(id)}`, label))))));
|
|
136
|
+
}
|
|
137
|
+
if (destinations?.size) {
|
|
138
|
+
contents.push(paragraph(strong(text('Schemas'))), list([...destinations].map(([name, url]) => item(paragraph(link(url, name))))));
|
|
96
139
|
}
|
|
140
|
+
if (contents.length)
|
|
141
|
+
nodes.push(heading(2, text('Contents')), ...contents);
|
|
142
|
+
}
|
|
143
|
+
if (document.servers?.length || (whole && document.servers !== undefined)) {
|
|
144
|
+
if (documentContext?.servers)
|
|
145
|
+
nodes.push(anchor(documentContext.servers));
|
|
146
|
+
nodes.push(heading(2, text('Servers')));
|
|
147
|
+
const effectiveServers = document.servers?.length
|
|
148
|
+
? document.servers
|
|
149
|
+
: [{ url: '/' }];
|
|
150
|
+
const servers = effectiveServers.map((server) => {
|
|
151
|
+
const nested = [];
|
|
152
|
+
if (server.description)
|
|
153
|
+
nested.push(field('Description', text(server.description)));
|
|
154
|
+
const variables = Object.entries(server.variables ?? {});
|
|
155
|
+
if (variables.length)
|
|
156
|
+
nested.push(item(paragraph(strong(text('Variables:'))), list(variables.map(([name, variable]) => item(paragraph(inlineCode(name), text(' (default: '), inlineCode(variable.default), text(')'), ...(whole && variable.enum?.length
|
|
157
|
+
? [text(', possible values: '), inlineCode(variable.enum.join(', '))]
|
|
158
|
+
: []), text(variable.description ? `: ${variable.description}` : '')))))));
|
|
159
|
+
const entry = field('URL', inlineCode(server.url));
|
|
160
|
+
if (nested.length)
|
|
161
|
+
entry.children.push(list(nested));
|
|
162
|
+
return entry;
|
|
163
|
+
});
|
|
164
|
+
nodes.push(list(servers));
|
|
165
|
+
}
|
|
166
|
+
if (documentContext?.authentication)
|
|
167
|
+
nodes.push(anchor(documentContext.authentication));
|
|
168
|
+
nodes.push(...(await renderSecurity(document.security, document.components?.securitySchemes, description, 2)));
|
|
169
|
+
if (document.tags?.length) {
|
|
170
|
+
nodes.push(heading(2, text('Tags')));
|
|
171
|
+
for (const tag of document.tags) {
|
|
172
|
+
nodes.push(heading(3, text(tag.name)), ...(await description(tag.description)));
|
|
173
|
+
if (tag.externalDocs)
|
|
174
|
+
nodes.push(paragraph(link(tag.externalDocs.url, tag.externalDocs.description ?? tag.externalDocs.url)));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await flush();
|
|
178
|
+
}
|
|
179
|
+
for (const group of groups) {
|
|
180
|
+
let hasOperations = false;
|
|
181
|
+
for (const { path, pathItem, method, operation, id } of group.entries) {
|
|
182
|
+
if (!hasOperations && !single)
|
|
183
|
+
nodes.push(heading(2, text(group.title)));
|
|
184
|
+
hasOperations = true;
|
|
185
|
+
schemas.beginSection();
|
|
186
|
+
if (id)
|
|
187
|
+
nodes.push(anchor(id));
|
|
188
|
+
nodes.push(...(await renderOperation(document, path, method, pathItem, operation, group.webhook, {
|
|
189
|
+
description,
|
|
190
|
+
schemas,
|
|
191
|
+
examples,
|
|
192
|
+
documentContext,
|
|
193
|
+
level: single ? 1 : 3,
|
|
194
|
+
})));
|
|
195
|
+
await flush();
|
|
97
196
|
}
|
|
98
197
|
}
|
|
99
|
-
|
|
100
|
-
|
|
198
|
+
// A model page renders its own model first, as the page title.
|
|
199
|
+
const models = Object.entries(document.components?.schemas ?? {}).sort(([a], [b]) => Number(b === options.model) - Number(a === options.model));
|
|
200
|
+
const renderModel = async (name, schema, level) => {
|
|
201
|
+
const view = schemas.view(schema);
|
|
202
|
+
const summary = schemas.summarize(schema);
|
|
203
|
+
if (whole)
|
|
204
|
+
nodes.push(anchor(anchors.get('schema', name)));
|
|
205
|
+
nodes.push(heading(level, text(view.title ?? name)));
|
|
206
|
+
if (summary.length)
|
|
207
|
+
nodes.push(paragraph(strong(text('Type:')), text('\u00a0'), ...summary));
|
|
208
|
+
nodes.push(...(await description(view.description)), ...schemas.render(schema, 0, [], { hideDetails: true, name }));
|
|
209
|
+
if (view.type === 'object' || hasAuthoredExamples(view)) {
|
|
210
|
+
// A model page's own schema is bounded, so it gets a generated example even in linked mode.
|
|
211
|
+
const own = name === options.model;
|
|
212
|
+
nodes.push(...(await renderExamples({ schema: own ? schemas.exampleSchema(schema) : schema }, description, 'application/json', undefined, openapiVersion, document.openapi, {
|
|
213
|
+
linked: schemas.linked && !own,
|
|
214
|
+
quiet: schemas.linked,
|
|
215
|
+
examples,
|
|
216
|
+
})));
|
|
217
|
+
}
|
|
218
|
+
await flush();
|
|
219
|
+
};
|
|
220
|
+
const selected = models.find(([name]) => name === options.model);
|
|
221
|
+
if (selected) {
|
|
222
|
+
schemas.beginSection();
|
|
223
|
+
await renderModel(selected[0], selected[1], 1);
|
|
224
|
+
}
|
|
225
|
+
const dependencies = models.filter(([name]) => name !== options.model);
|
|
226
|
+
if (dependencies.length)
|
|
101
227
|
nodes.push(heading(2, text('Schemas')));
|
|
102
|
-
|
|
228
|
+
// Models the page already expanded cost one line each, grouped into a single list.
|
|
229
|
+
const shownAbove = [];
|
|
230
|
+
const flushShownAbove = () => {
|
|
231
|
+
if (shownAbove.length)
|
|
232
|
+
nodes.push(list(shownAbove.splice(0)));
|
|
233
|
+
};
|
|
234
|
+
for (const [name, schema] of dependencies) {
|
|
103
235
|
schemas.beginSection();
|
|
104
236
|
const view = schemas.view(schema);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
237
|
+
const previous = schemas.shownAs(schema);
|
|
238
|
+
// A generated example only restates the schema, but an authored one is not printed above.
|
|
239
|
+
if (!whole && previous && !hasAuthoredExamples(view)) {
|
|
240
|
+
shownAbove.push(await renderShownModel(name, view, previous, description));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
flushShownAbove();
|
|
244
|
+
await renderModel(name, schema, 3);
|
|
113
245
|
}
|
|
114
|
-
|
|
246
|
+
flushShownAbove();
|
|
247
|
+
await flush();
|
|
115
248
|
return `${sections.join('\n\n')}\n`;
|
|
116
249
|
};
|
|
117
250
|
};
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { RootContent } from 'mdast';
|
|
2
|
+
import type { DocumentExamples } from './document-examples.js';
|
|
2
3
|
import { type ExampleSource } from './get-markdown-examples.js';
|
|
3
4
|
import type { DescriptionParser } from './parse-description.js';
|
|
4
5
|
/** Render supplied examples before considering a schema-generated fallback. */
|
|
5
|
-
export declare const renderExamples: (source: ExampleSource, description: DescriptionParser, mediaType?: string, mode?: "read" | "write", openapiVersion?: string, schemaOpenapiVersion?: string, linked
|
|
6
|
+
export declare const renderExamples: (source: ExampleSource, description: DescriptionParser, mediaType?: string, mode?: "read" | "write", openapiVersion?: string, schemaOpenapiVersion?: string, { linked, quiet, examples, }?: {
|
|
7
|
+
/** Use only authored examples, since generating one would expand every linked schema. */
|
|
8
|
+
linked?: boolean;
|
|
9
|
+
/** Leave out an example that is too large to generate, instead of noting it. */
|
|
10
|
+
quiet?: boolean;
|
|
11
|
+
/** A whole document shares generation and links to identical generated examples. */
|
|
12
|
+
examples?: DocumentExamples;
|
|
13
|
+
}) => Promise<RootContent[]>;
|
|
6
14
|
//# sourceMappingURL=render-examples.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-examples.d.ts","sourceRoot":"","sources":["../src/render-examples.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"render-examples.d.ts","sourceRoot":"","sources":["../src/render-examples.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,OAAO,CAAA;AAG9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,yBAAyB,CAAA;AAEjF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,cAAc,GACzB,QAAQ,aAAa,EACrB,aAAa,iBAAiB,EAC9B,kBAA8B,EAC9B,OAAO,MAAM,GAAG,OAAO,EACvB,uBAAwB,EAExB,6BAAqC,EACrC,+BAIG;IACD,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,oFAAoF;IACpF,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CACvB,KACL,OAAO,CAAC,WAAW,EAAE,CA8EvB,CAAA"}
|
package/dist/render-examples.js
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
import { isXmlMediaType } from '@scalar/helpers/http/is-xml-media-type';
|
|
2
2
|
import { getXmlBodyExample } from '@scalar/workspace-store/request-example';
|
|
3
|
+
import { anchor } from './document-anchors.js';
|
|
3
4
|
import { getMarkdownExamples } from './get-markdown-examples.js';
|
|
4
|
-
import { link, paragraph, strong, text } from './markdown-nodes.js';
|
|
5
|
+
import { emphasis, link, paragraph, strong, text } from './markdown-nodes.js';
|
|
5
6
|
/** Render supplied examples before considering a schema-generated fallback. */
|
|
6
7
|
export const renderExamples = async (source, description, mediaType = 'application/json', mode, openapiVersion = '3.2.0',
|
|
7
8
|
// Schema metadata can be upgraded while example fields still follow the original version.
|
|
8
|
-
schemaOpenapiVersion = openapiVersion, linked = false) => {
|
|
9
|
+
schemaOpenapiVersion = openapiVersion, { linked = false, quiet = linked, examples, } = {}) => {
|
|
9
10
|
const nodes = [];
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const values = (examples?.get ?? getMarkdownExamples)(source, mediaType, mode, openapiVersion, schemaOpenapiVersion, linked);
|
|
12
|
+
for (const example of values) {
|
|
13
|
+
if ('omitted' in example && quiet)
|
|
14
|
+
continue;
|
|
15
|
+
const start = nodes.length;
|
|
16
|
+
const generated = examples && example.generated;
|
|
17
|
+
nodes.push(paragraph(strong(text(generated ? 'Generated example:' : example.name ? `Example: ${example.name}` : 'Example:'))));
|
|
18
|
+
const addCode = (code) => {
|
|
19
|
+
if (generated) {
|
|
20
|
+
const scope = JSON.stringify([mediaType, mode, openapiVersion, schemaOpenapiVersion, code.lang]);
|
|
21
|
+
const destination = examples.show(source, scope, code);
|
|
22
|
+
if (destination.previous) {
|
|
23
|
+
nodes.splice(start, nodes.length - start, paragraph(link(`#${destination.id}`, 'Generated example')));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
nodes.splice(start, 0, anchor(destination.id));
|
|
27
|
+
}
|
|
28
|
+
nodes.push(code);
|
|
29
|
+
};
|
|
12
30
|
if ('omitted' in example) {
|
|
13
|
-
nodes.push(paragraph(text(
|
|
31
|
+
nodes.push(paragraph(emphasis(text('Generated example omitted because it is too large.'))));
|
|
14
32
|
continue;
|
|
15
33
|
}
|
|
16
34
|
if (example.summary)
|
|
@@ -25,7 +43,7 @@ schemaOpenapiVersion = openapiVersion, linked = false) => {
|
|
|
25
43
|
continue;
|
|
26
44
|
}
|
|
27
45
|
if ('serializedValue' in example) {
|
|
28
|
-
|
|
46
|
+
addCode({
|
|
29
47
|
type: 'code',
|
|
30
48
|
lang: isXmlMediaType(mediaType) ? 'xml' : mediaType.includes('json') ? 'json' : 'text',
|
|
31
49
|
value: example.serializedValue,
|
|
@@ -35,7 +53,7 @@ schemaOpenapiVersion = openapiVersion, linked = false) => {
|
|
|
35
53
|
const xml = isXmlMediaType(mediaType);
|
|
36
54
|
const value = 'dataValue' in example ? example.dataValue : example.value;
|
|
37
55
|
if (xml && !source.schema && 'value' in example && (value === null || typeof value !== 'object')) {
|
|
38
|
-
|
|
56
|
+
addCode({ type: 'code', lang: 'xml', value: String(value) });
|
|
39
57
|
continue;
|
|
40
58
|
}
|
|
41
59
|
const result = xml
|
|
@@ -48,7 +66,7 @@ schemaOpenapiVersion = openapiVersion, linked = false) => {
|
|
|
48
66
|
nodes.push(paragraph(text('Unable to generate an XML example.')));
|
|
49
67
|
continue;
|
|
50
68
|
}
|
|
51
|
-
|
|
69
|
+
addCode({
|
|
52
70
|
type: 'code',
|
|
53
71
|
lang: xml ? 'xml' : 'json',
|
|
54
72
|
value: result?.xml ?? JSON.stringify(value, null, 2) ?? '',
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { EncodingObject, ResponseObject } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
2
2
|
import type { RootContent } from 'mdast';
|
|
3
|
+
import type { DocumentExamples } from './document-examples.js';
|
|
3
4
|
import type { DescriptionParser } from './parse-description.js';
|
|
4
5
|
import type { SchemaRenderer } from './render-schema.js';
|
|
5
6
|
/** Render response or multipart headers, whose names come from their containing map. */
|
|
6
|
-
export declare const renderHeaders: (headers: ResponseObject["headers"], description: DescriptionParser, schemas: SchemaRenderer, openapiVersion: string, schemaOpenapiVersion?: string) => Promise<RootContent[]>;
|
|
7
|
+
export declare const renderHeaders: (headers: ResponseObject["headers"], description: DescriptionParser, schemas: SchemaRenderer, openapiVersion: string, schemaOpenapiVersion?: string, examples?: DocumentExamples) => Promise<RootContent[]>;
|
|
7
8
|
/** Preserve explicit encoding settings, including false flags and part headers. */
|
|
8
|
-
export declare const renderEncoding: (encoding: Record<string, EncodingObject> | undefined, mediaType: string, description: DescriptionParser, schemas: SchemaRenderer, openapiVersion: string, schemaOpenapiVersion?: string) => Promise<RootContent[]>;
|
|
9
|
+
export declare const renderEncoding: (encoding: Record<string, EncodingObject> | undefined, mediaType: string, description: DescriptionParser, schemas: SchemaRenderer, openapiVersion: string, schemaOpenapiVersion?: string, examples?: DocumentExamples) => Promise<RootContent[]>;
|
|
9
10
|
/** Render response link expressions as literal values, without evaluating them. */
|
|
10
11
|
export declare const renderResponseLinks: (links: ResponseObject["links"], description: DescriptionParser) => Promise<RootContent[]>;
|
|
11
12
|
//# sourceMappingURL=render-operation-details.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-operation-details.d.ts","sourceRoot":"","sources":["../src/render-operation-details.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,8DAA8D,CAAA;AAClH,OAAO,KAAK,EAAY,WAAW,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"render-operation-details.d.ts","sourceRoot":"","sources":["../src/render-operation-details.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,8DAA8D,CAAA;AAClH,OAAO,KAAK,EAAY,WAAW,EAAE,MAAM,OAAO,CAAA;AAElD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAE3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,wFAAwF;AACxF,eAAO,MAAM,aAAa,GACxB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,aAAa,iBAAiB,EAC9B,SAAS,cAAc,EACvB,gBAAgB,MAAM,EACtB,6BAAqC,EACrC,WAAW,gBAAgB,KAC1B,OAAO,CAAC,WAAW,EAAE,CAuCvB,CAAA;AAED,mFAAmF;AACnF,eAAO,MAAM,cAAc,GACzB,UAAU,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,SAAS,EACpD,WAAW,MAAM,EACjB,aAAa,iBAAiB,EAC9B,SAAS,cAAc,EACvB,gBAAgB,MAAM,EACtB,6BAAqC,EACrC,WAAW,gBAAgB,KAC1B,OAAO,CAAC,WAAW,EAAE,CA8BvB,CAAA;AAID,mFAAmF;AACnF,eAAO,MAAM,mBAAmB,GAC9B,OAAO,cAAc,CAAC,OAAO,CAAC,EAC9B,aAAa,iBAAiB,KAC7B,OAAO,CAAC,WAAW,EAAE,CA2BvB,CAAA"}
|
|
@@ -2,7 +2,7 @@ import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/
|
|
|
2
2
|
import { inlineCode, item, list, paragraph, strong, text } from './markdown-nodes.js';
|
|
3
3
|
import { renderExamples } from './render-examples.js';
|
|
4
4
|
/** Render response or multipart headers, whose names come from their containing map. */
|
|
5
|
-
export const renderHeaders = async (headers, description, schemas, openapiVersion, schemaOpenapiVersion = openapiVersion) => {
|
|
5
|
+
export const renderHeaders = async (headers, description, schemas, openapiVersion, schemaOpenapiVersion = openapiVersion, examples) => {
|
|
6
6
|
const entries = [];
|
|
7
7
|
for (const [name, reference] of Object.entries(headers ?? {})) {
|
|
8
8
|
// OpenAPI reserves Content-Type for the media type map / encoding field.
|
|
@@ -18,20 +18,23 @@ export const renderHeaders = async (headers, description, schemas, openapiVersio
|
|
|
18
18
|
if ('schema' in header && header.schema !== undefined)
|
|
19
19
|
blocks.push(...schemas.render(header.schema));
|
|
20
20
|
if ('example' in header || 'examples' in header) {
|
|
21
|
-
blocks.push(...(await renderExamples({ example: header.example, examples: header.examples }, description, 'application/json', undefined, openapiVersion, schemaOpenapiVersion, schemas.linked)));
|
|
21
|
+
blocks.push(...(await renderExamples({ example: header.example, examples: header.examples }, description, 'application/json', undefined, openapiVersion, schemaOpenapiVersion, { linked: schemas.linked, examples })));
|
|
22
22
|
}
|
|
23
23
|
for (const [mediaType, content] of Object.entries('content' in header ? (header.content ?? {}) : {})) {
|
|
24
24
|
blocks.push(paragraph(strong(text('Content-Type:')), text(` ${mediaType}`)));
|
|
25
25
|
if (content.schema !== undefined)
|
|
26
26
|
blocks.push(...schemas.render(content.schema));
|
|
27
|
-
blocks.push(...(await renderExamples(content, description, mediaType, undefined, openapiVersion, schemaOpenapiVersion,
|
|
27
|
+
blocks.push(...(await renderExamples(content, description, mediaType, undefined, openapiVersion, schemaOpenapiVersion, {
|
|
28
|
+
linked: schemas.linked,
|
|
29
|
+
examples,
|
|
30
|
+
})));
|
|
28
31
|
}
|
|
29
32
|
entries.push(item(...blocks));
|
|
30
33
|
}
|
|
31
34
|
return entries.length ? [paragraph(strong(text('Headers:'))), list(entries)] : [];
|
|
32
35
|
};
|
|
33
36
|
/** Preserve explicit encoding settings, including false flags and part headers. */
|
|
34
|
-
export const renderEncoding = async (encoding, mediaType, description, schemas, openapiVersion, schemaOpenapiVersion = openapiVersion) => {
|
|
37
|
+
export const renderEncoding = async (encoding, mediaType, description, schemas, openapiVersion, schemaOpenapiVersion = openapiVersion, examples) => {
|
|
35
38
|
const multipart = mediaType.startsWith('multipart/');
|
|
36
39
|
if (!multipart && mediaType !== 'application/x-www-form-urlencoded')
|
|
37
40
|
return [];
|
|
@@ -51,7 +54,7 @@ export const renderEncoding = async (encoding, mediaType, description, schemas,
|
|
|
51
54
|
if (fields.length)
|
|
52
55
|
blocks.push(list(fields));
|
|
53
56
|
if (multipart)
|
|
54
|
-
blocks.push(...(await renderHeaders(entry.headers, description, schemas, openapiVersion, schemaOpenapiVersion)));
|
|
57
|
+
blocks.push(...(await renderHeaders(entry.headers, description, schemas, openapiVersion, schemaOpenapiVersion, examples)));
|
|
55
58
|
entries.push(item(...blocks));
|
|
56
59
|
}
|
|
57
60
|
return entries.length ? [paragraph(strong(text('Encoding:'))), list(entries)] : [];
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import type { OpenApiDocument, OperationObject, PathItemObject } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
2
2
|
import type { RootContent } from 'mdast';
|
|
3
|
+
import { type createDocumentAnchors } from './document-anchors.js';
|
|
4
|
+
import type { DocumentExamples } from './document-examples.js';
|
|
3
5
|
import type { DescriptionParser } from './parse-description.js';
|
|
4
6
|
import type { SchemaRenderer } from './render-schema.js';
|
|
5
7
|
/** Dependencies shared by all sections of one prepared renderer. */
|
|
6
8
|
type RenderContext = {
|
|
7
9
|
description: DescriptionParser;
|
|
8
10
|
schemas: SchemaRenderer;
|
|
11
|
+
examples?: DocumentExamples;
|
|
12
|
+
documentContext?: DocumentContext;
|
|
13
|
+
/** The heading level of the operation title: 1 on its own page, 3 inside a document. */
|
|
14
|
+
level?: number;
|
|
9
15
|
};
|
|
16
|
+
/** Destinations for defaults that a whole document explains once. */
|
|
17
|
+
export type DocumentContext = {
|
|
18
|
+
anchors: ReturnType<typeof createDocumentAnchors>;
|
|
19
|
+
servers?: string;
|
|
20
|
+
authentication?: string;
|
|
21
|
+
pathServers: WeakMap<object, string>;
|
|
22
|
+
};
|
|
23
|
+
/** Format standard HTTP methods while preserving custom method names. */
|
|
24
|
+
export declare const formatOperationMethod: (method: string) => string;
|
|
10
25
|
/** Render effective operation context without mutating the prepared document. */
|
|
11
|
-
export declare const renderOperation: (document: OpenApiDocument, path: string, method: string, pathItem: PathItemObject, operation: OperationObject, webhook: boolean, { description, schemas }: RenderContext) => Promise<RootContent[]>;
|
|
26
|
+
export declare const renderOperation: (document: OpenApiDocument, path: string, method: string, pathItem: PathItemObject, operation: OperationObject, webhook: boolean, { description, schemas, examples, documentContext, level }: RenderContext) => Promise<RootContent[]>;
|
|
12
27
|
export {};
|
|
13
28
|
//# sourceMappingURL=render-operation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-operation.d.ts","sourceRoot":"","sources":["../src/render-operation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-operation.d.ts","sourceRoot":"","sources":["../src/render-operation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAEf,cAAc,EAGf,MAAM,8DAA8D,CAAA;AACrE,OAAO,KAAK,EAAsC,WAAW,EAAE,MAAM,OAAO,CAAA;AAE5E,OAAO,EAAU,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAE3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAGrD,oEAAoE;AACpE,KAAK,aAAa,GAAG;IACnB,WAAW,EAAE,iBAAiB,CAAA;IAC9B,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,qEAAqE;AACrE,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAA;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACrC,CAAA;AAoCD,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,KAAG,MACkC,CAAA;AAEzF,iFAAiF;AACjF,eAAO,MAAM,eAAe,GAC1B,UAAU,eAAe,EACzB,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,UAAU,cAAc,EACxB,WAAW,eAAe,EAC1B,SAAS,OAAO,EAChB,4DAAgE,aAAa,KAC5E,OAAO,CAAC,WAAW,EAAE,CAiLvB,CAAA"}
|