@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +66 -5
  3. package/dist/create-markdown-from-openapi.d.ts.map +1 -1
  4. package/dist/create-markdown-from-openapi.js +3 -2
  5. package/dist/document-anchors.d.ts +8 -0
  6. package/dist/document-anchors.d.ts.map +1 -0
  7. package/dist/document-anchors.js +25 -0
  8. package/dist/document-examples.d.ts +14 -0
  9. package/dist/document-examples.d.ts.map +1 -0
  10. package/dist/document-examples.js +54 -0
  11. package/dist/get-markdown-examples.d.ts +2 -0
  12. package/dist/get-markdown-examples.d.ts.map +1 -1
  13. package/dist/get-markdown-examples.js +11 -12
  14. package/dist/markdown-nodes.d.ts +22 -1
  15. package/dist/markdown-nodes.d.ts.map +1 -1
  16. package/dist/markdown-nodes.js +4 -0
  17. package/dist/parse-description.d.ts +2 -0
  18. package/dist/parse-description.d.ts.map +1 -1
  19. package/dist/parse-description.js +28 -1
  20. package/dist/render-document.d.ts +2 -2
  21. package/dist/render-document.d.ts.map +1 -1
  22. package/dist/render-document.js +217 -84
  23. package/dist/render-examples.d.ts +9 -1
  24. package/dist/render-examples.d.ts.map +1 -1
  25. package/dist/render-examples.js +26 -8
  26. package/dist/render-operation-details.d.ts +3 -2
  27. package/dist/render-operation-details.d.ts.map +1 -1
  28. package/dist/render-operation-details.js +8 -5
  29. package/dist/render-operation.d.ts +16 -1
  30. package/dist/render-operation.d.ts.map +1 -1
  31. package/dist/render-operation.js +168 -42
  32. package/dist/render-schema.d.ts +30 -3
  33. package/dist/render-schema.d.ts.map +1 -1
  34. package/dist/render-schema.js +474 -111
  35. package/dist/render-security.d.ts +7 -3
  36. package/dist/render-security.d.ts.map +1 -1
  37. package/dist/render-security.js +61 -12
  38. package/dist/select-document.d.ts +34 -3
  39. package/dist/select-document.d.ts.map +1 -1
  40. package/dist/select-document.js +58 -30
  41. package/package.json +18 -9
@@ -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().use(remarkGfm).use(remarkStringify, { bullet: '-' }).freeze();
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
- // Each page expands a shared schema once, then refers back to it.
20
- const schemas = schemaRenderer.forDocument(document.components?.schemas, options);
21
- const { info } = document;
22
- const metadata = [
23
- field('OpenAPI Version', inlineCode(document.openapi)),
24
- field('API Version', inlineCode(info.version)),
25
- ];
26
- if (info.termsOfService)
27
- metadata.push(field('Terms of service', link(info.termsOfService, info.termsOfService)));
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 sections = [];
65
- const flush = () => {
66
- if (nodes.length) {
67
- const tree = { type: 'root', children: nodes.splice(0) };
68
- sections.push(serializer.stringify(tree).trimEnd());
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
- let hasOperations = false;
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
- entries.push({ method, operation: getResolvedRef(operation, mergeSiblingReferences) });
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
- for (const { method, operation } of entries) {
86
- if (!hasOperations) {
87
- nodes.push(heading(2, text(group.title)));
88
- hasOperations = true;
89
- }
90
- schemas.beginSection();
91
- nodes.push(...(await renderOperation(document, path, method, pathItem, operation, group.webhook, {
92
- description,
93
- schemas,
94
- })));
95
- flush();
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
- const models = Object.entries(document.components?.schemas ?? {});
100
- if (models.length)
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
- for (const [name, schema] of models) {
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
- nodes.push(heading(3, text(view.title ?? name)), list([
106
- view.type
107
- ? field('Type', inlineCode(Array.isArray(view.type) ? view.type.join(' | ') : view.type))
108
- : item(paragraph(strong(text('Type:')))),
109
- ]), ...(await description(view.description)), ...schemas.render(schema, 0, [], { hideDescription: true, name }));
110
- if (view.type === 'object')
111
- nodes.push(...(await renderExamples({ schema }, description, 'application/json', undefined, document['x-original-oas-version'] ?? document.openapi, document.openapi, schemas.linked)));
112
- flush();
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
- flush();
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?: boolean) => Promise<RootContent[]>;
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,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAExC,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,gBAAc,KACb,OAAO,CAAC,WAAW,EAAE,CAyDvB,CAAA"}
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"}
@@ -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
- for (const example of getMarkdownExamples(source, mediaType, mode, openapiVersion, schemaOpenapiVersion, linked)) {
11
- nodes.push(paragraph(strong(text(example.name ? `Example: ${example.name}` : 'Example:'))));
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(`${linked ? '[Generated example omitted in linked schema mode; see the schema documentation]' : '[Generated example omitted because it is too large]'}`)));
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
- nodes.push({
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
- nodes.push({ type: 'code', lang: 'xml', value: String(value) });
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
- nodes.push({
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;AAGlD,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,KACpC,OAAO,CAAC,WAAW,EAAE,CA4CvB,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,KACpC,OAAO,CAAC,WAAW,EAAE,CA6BvB,CAAA;AAID,mFAAmF;AACnF,eAAO,MAAM,mBAAmB,GAC9B,OAAO,cAAc,CAAC,OAAO,CAAC,EAC9B,aAAa,iBAAiB,KAC7B,OAAO,CAAC,WAAW,EAAE,CA2BvB,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, schemas.linked)));
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":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAEf,cAAc,EAGf,MAAM,8DAA8D,CAAA;AACrE,OAAO,KAAK,EAAY,WAAW,EAAE,MAAM,OAAO,CAAA;AAGlD,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;CACxB,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,eAAe,GAC1B,UAAU,eAAe,EACzB,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,UAAU,cAAc,EACxB,WAAW,eAAe,EAC1B,SAAS,OAAO,EAChB,0BAA0B,aAAa,KACtC,OAAO,CAAC,WAAW,EAAE,CA0IvB,CAAA"}
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"}