@scalar/openapi-to-markdown 1.2.0 → 1.4.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/CHANGELOG.md +28 -0
- package/README.md +51 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/create-markdown-from-openapi.js +1 -1
- package/dist/get-markdown-examples.d.ts +5 -1
- package/dist/get-markdown-examples.d.ts.map +1 -1
- package/dist/get-markdown-examples.js +25 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/load-document.d.ts.map +1 -1
- package/dist/load-document.js +120 -42
- package/dist/object-kinds.d.ts +14 -0
- package/dist/object-kinds.d.ts.map +1 -0
- package/dist/object-kinds.js +70 -0
- package/dist/render-document.d.ts +2 -1
- package/dist/render-document.d.ts.map +1 -1
- package/dist/render-document.js +3 -3
- package/dist/render-examples.d.ts +1 -1
- package/dist/render-examples.d.ts.map +1 -1
- package/dist/render-examples.js +29 -12
- package/dist/render-operation-details.d.ts +2 -2
- package/dist/render-operation-details.d.ts.map +1 -1
- package/dist/render-operation-details.js +5 -5
- package/dist/render-operation.d.ts.map +1 -1
- package/dist/render-operation.js +6 -6
- package/dist/render-schema.d.ts +3 -1
- package/dist/render-schema.d.ts.map +1 -1
- package/dist/render-schema.js +39 -7
- package/dist/restore-boolean-schemas.d.ts +8 -1
- package/dist/restore-boolean-schemas.d.ts.map +1 -1
- package/dist/restore-boolean-schemas.js +42 -38
- package/dist/select-document.d.ts +14 -2
- package/dist/select-document.d.ts.map +1 -1
- package/dist/select-document.js +6 -4
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @scalar/openapi-to-markdown
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#10394](https://github.com/scalar/scalar/pull/10394): Add opt-in linked schema rendering with caller-supplied model URLs for bounded operation, model, and webhook pages. Keep default expansion unchanged, retain reference siblings and inline schemas, and omit generated examples in linked mode.
|
|
8
|
+
|
|
9
|
+
## 1.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#10192](https://github.com/scalar/scalar/pull/10192): Mock-server XML response bytes now use the shared schema-aware serializer instead of `json2xml`, including attributes, namespaces, and root naming. Existing XML response snapshots may need updating. Supplied serialized XML remains unchanged.
|
|
14
|
+
|
|
15
|
+
Generate XML examples from schema metadata, preserving attributes, namespaces, array wrappers, repeated elements, and OpenAPI 3.2 text and CDATA nodes. Use the same XML serialization for request bodies, code snippets, response examples, mock responses, and Markdown documentation. Preserve serialized media examples and escape schema string examples as element text.
|
|
16
|
+
|
|
17
|
+
Explain XML generation failures in response example panels, including the serialized-example escape hatch for large payloads. Expose XML generation failures in mock response headers with `X-Scalar-XML-Error`, containing the first error diagnostic code. Report diagnostics to other consumers through a callback or the developer console, and format element-only descendants within mixed content without changing text values.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#10356](https://github.com/scalar/scalar/pull/10356): Load densely cross-linked API descriptions with far less memory. References are now linked after coercion, so TypeBox no longer copies the reference graph each time it checks a union or array. Stripe's API description loads in about 2 seconds with a peak of about 190 MB, down from about 17 seconds and 1.4 GB, so it now loads and renders with a 768 MB heap limit.
|
|
22
|
+
|
|
23
|
+
Coercion now checks each reference target in its own position, not through every reference that points to it. Before, a target that failed a strict schema check (for example, a schema with `oneOf` references) could turn the reference into an empty schema and drop sibling `x-` extensions. Those references and extensions are now kept, so affected response schemas render in full.
|
|
24
|
+
|
|
25
|
+
References to targets that coercion drops, such as a root-level `definitions` block, now link to a copy cast as the schema, parameter, response or other object the reference stands in for, and their anchors resolve against the resource that contains them. A `$ref: '#'` inside a schema with an `$id` now links to that schema.
|
|
26
|
+
|
|
27
|
+
Coerce reference targets stored in extension data and schema siblings on fallback references before rendering, avoiding crashes from malformed fields while preserving recursive links.
|
|
28
|
+
|
|
29
|
+
- [#10192](https://github.com/scalar/scalar/pull/10192): Apply edited XML bodies instead of their original serialized or data examples, render schema-free XML examples in Markdown, and share reference decoding and response provenance selection.
|
|
30
|
+
|
|
3
31
|
## 1.2.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -194,3 +194,54 @@ const markdown = await createMarkdownFromOpenApi(document, {
|
|
|
194
194
|
|
|
195
195
|
Use the default entry point for raw JSON, YAML, URLs, or file paths that still need
|
|
196
196
|
loading, migration, and reference resolution.
|
|
197
|
+
|
|
198
|
+
### Link shared schemas on individual pages
|
|
199
|
+
|
|
200
|
+
The default renderer expands shared schemas and includes the schemas needed by a
|
|
201
|
+
selected page. For large API descriptions, opt into `schemaReferences.mode:
|
|
202
|
+
'linked'` to expand the root schema of each parameter, request, response, or model
|
|
203
|
+
and replace nested `$ref` occurrences with links. Inline schemas continue to
|
|
204
|
+
render, including composition branches and reference siblings. Operation and
|
|
205
|
+
webhook pages do not collect or append transitive models; a model page includes
|
|
206
|
+
only the selected model.
|
|
207
|
+
|
|
208
|
+
The documentation generator supplies published URLs. The renderer does not assume
|
|
209
|
+
any routing convention:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { createOpenApiMarkdownRenderer } from '@scalar/openapi-to-markdown'
|
|
213
|
+
|
|
214
|
+
const renderer = await createOpenApiMarkdownRenderer(apiDescription)
|
|
215
|
+
// Build this map from the pages your generator actually publishes.
|
|
216
|
+
const modelUrls = new Map([
|
|
217
|
+
['account', '/reference/models/account'],
|
|
218
|
+
['customer', '/reference/models/customer'],
|
|
219
|
+
])
|
|
220
|
+
const markdown = await renderer.render({
|
|
221
|
+
operation: { path: '/v1/account', method: 'get' },
|
|
222
|
+
schemaReferences: {
|
|
223
|
+
mode: 'linked',
|
|
224
|
+
resolveUrl: ({ name }) => modelUrls.get(name),
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The callback receives the original reference string (`ref`) and its decoded
|
|
230
|
+
component name (`name`). For references outside `#/components/schemas/{name}`,
|
|
231
|
+
`name` is the reference string. Returning `undefined`, an empty URL, or an unsafe
|
|
232
|
+
URL retains the schema name as text. Omitting the callback has the same fallback.
|
|
233
|
+
References remain visible even when no destination page exists.
|
|
234
|
+
|
|
235
|
+
Use the same option with `{ model: 'account' }`, `{ webhook: { name: 'event',
|
|
236
|
+
method: 'post' } }`, the one-shot `createMarkdownFromOpenApi`, or the browser entry
|
|
237
|
+
point. The browser entry point still requires a workspace-resolved document.
|
|
238
|
+
Options and URL callbacks are isolated per render, including concurrent renders.
|
|
239
|
+
|
|
240
|
+
Linked mode omits schema-generated examples with an explicit note, avoiding
|
|
241
|
+
expansion through the example generator. Authored media-type and schema examples
|
|
242
|
+
remain available. Their size is not capped. Inline schema content and authored
|
|
243
|
+
text also remain proportional to the source; this mode bounds traversal across
|
|
244
|
+
shared references, not the byte size of arbitrary authored content. Root
|
|
245
|
+
composition branches that contain references link to those schemas rather than
|
|
246
|
+
flattening their constraints. Full-document exports still include every model
|
|
247
|
+
section; use a page selector for individual exports.
|
package/dist/browser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AAGnG,OAAO,EAAE,KAAK,oBAAoB,EAAkB,MAAM,mBAAmB,CAAA;AAE7E;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GACpC,UAAU,eAAe,EACzB,UAAU,oBAAoB,KAC7B,OAAO,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AAGnG,OAAO,EAAE,KAAK,oBAAoB,EAAkB,MAAM,mBAAmB,CAAA;AAE7E;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GACpC,UAAU,eAAe,EACzB,UAAU,oBAAoB,KAC7B,OAAO,CAAC,MAAM,CAA+E,CAAA"}
|
package/dist/browser.js
CHANGED
|
@@ -5,4 +5,4 @@ import { selectDocument } from './select-document.js';
|
|
|
5
5
|
* References must already be resolved by the store. This entry point does not load
|
|
6
6
|
* files, fetch URLs, or migrate raw API descriptions.
|
|
7
7
|
*/
|
|
8
|
-
export const createMarkdownFromOpenApi = async (document, options) => await createDocumentRenderer()(selectDocument(document, options));
|
|
8
|
+
export const createMarkdownFromOpenApi = async (document, options) => await createDocumentRenderer()(selectDocument(document, options), options);
|
|
@@ -8,7 +8,7 @@ import { selectDocument } from './select-document.js';
|
|
|
8
8
|
export const createOpenApiMarkdownRenderer = async (input) => {
|
|
9
9
|
const content = await loadDocument(input);
|
|
10
10
|
const renderDocument = createDocumentRenderer();
|
|
11
|
-
const render = async (options) => await renderDocument(selectDocument(content, options));
|
|
11
|
+
const render = async (options) => await renderDocument(selectDocument(content, options), options);
|
|
12
12
|
return { render };
|
|
13
13
|
};
|
|
14
14
|
/** Generate Markdown from an API description, optionally scoped to a single page. */
|
|
@@ -17,6 +17,10 @@ type MarkdownExample = {
|
|
|
17
17
|
serializedValue: string;
|
|
18
18
|
} | {
|
|
19
19
|
omitted: true;
|
|
20
|
+
} | {
|
|
21
|
+
dataValue: unknown;
|
|
22
|
+
} | {
|
|
23
|
+
error: string;
|
|
20
24
|
});
|
|
21
25
|
/**
|
|
22
26
|
* Estimate how many values an example generated from this schema contains, stopping just past the limit.
|
|
@@ -24,6 +28,6 @@ type MarkdownExample = {
|
|
|
24
28
|
*/
|
|
25
29
|
export declare const countGeneratedExampleValues: (root: unknown, limit?: number) => number;
|
|
26
30
|
/** Preserve supplied values; generate a fallback only when examples are not supplied. */
|
|
27
|
-
export declare const getMarkdownExamples: (source: ExampleSource, mediaType: string, mode?: "read" | "write", openapiVersion?: string) => MarkdownExample[];
|
|
31
|
+
export declare const getMarkdownExamples: (source: ExampleSource, mediaType: string, mode?: "read" | "write", openapiVersion?: string, schemaOpenapiVersion?: string, linked?: boolean) => MarkdownExample[];
|
|
28
32
|
export {};
|
|
29
33
|
//# sourceMappingURL=get-markdown-examples.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-markdown-examples.d.ts","sourceRoot":"","sources":["../src/get-markdown-examples.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"get-markdown-examples.d.ts","sourceRoot":"","sources":["../src/get-markdown-examples.ts"],"names":[],"mappings":"AAMA,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,gFAAgF;AAChF,KAAK,eAAe,GAAG;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,CACA;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAClB;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,eAAe,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GACjB;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GACtB;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CACpB,CAAA;AAYD;;;GAGG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,OAAO,EAAE,cAAoC,KAAG,MA2CjG,CAAA;AAED,yFAAyF;AACzF,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,aAAa,EACrB,WAAW,MAAM,EACjB,OAAO,MAAM,GAAG,OAAO,EACvB,uBAAwB,EAExB,6BAAqC,EACrC,gBAAc,KACb,eAAe,EA2CjB,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isXmlMediaType } from '@scalar/helpers/http/is-xml-media-type';
|
|
1
2
|
import { isObject } from '@scalar/helpers/object/is-object';
|
|
2
3
|
import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
3
|
-
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
4
|
+
import { getExampleFromSchema, getXmlBodyExample } from '@scalar/workspace-store/request-example';
|
|
4
5
|
/** Mirrors the depth at which `getExampleFromSchema` stops following nested schemas. */
|
|
5
6
|
const EXAMPLE_DEPTH = 10;
|
|
6
7
|
/**
|
|
@@ -68,7 +69,9 @@ export const countGeneratedExampleValues = (root, limit = MAX_GENERATED_EXAMPLE_
|
|
|
68
69
|
return count(root, 0);
|
|
69
70
|
};
|
|
70
71
|
/** Preserve supplied values; generate a fallback only when examples are not supplied. */
|
|
71
|
-
export const getMarkdownExamples = (source, mediaType, mode, openapiVersion = '3.2.0'
|
|
72
|
+
export const getMarkdownExamples = (source, mediaType, mode, openapiVersion = '3.2.0',
|
|
73
|
+
// Schema metadata can be upgraded while example fields still follow the original version.
|
|
74
|
+
schemaOpenapiVersion = openapiVersion, linked = false) => {
|
|
72
75
|
if (source.example !== undefined)
|
|
73
76
|
return [{ value: source.example }];
|
|
74
77
|
if (source.examples && Object.keys(source.examples).length) {
|
|
@@ -88,17 +91,34 @@ export const getMarkdownExamples = (source, mediaType, mode, openapiVersion = '3
|
|
|
88
91
|
if (typeof example.externalValue === 'string')
|
|
89
92
|
return [{ ...metadata, externalValue: example.externalValue }];
|
|
90
93
|
if (/^3\.2\./.test(openapiVersion) && example.dataValue !== undefined)
|
|
91
|
-
return [{ ...metadata,
|
|
94
|
+
return [{ ...metadata, dataValue: example.dataValue }];
|
|
92
95
|
return [];
|
|
93
96
|
});
|
|
94
97
|
}
|
|
95
|
-
const schema =
|
|
98
|
+
const schema = linked
|
|
99
|
+
? getResolvedRef(source.schema, mergeSiblingReferences)
|
|
100
|
+
: getResolvedRef(source.schema);
|
|
96
101
|
if (!isObject(schema))
|
|
97
102
|
return [];
|
|
103
|
+
if (linked) {
|
|
104
|
+
if (schema.example !== undefined)
|
|
105
|
+
return [{ value: schema.example }];
|
|
106
|
+
if (Array.isArray(schema.examples) && schema.examples.length)
|
|
107
|
+
return schema.examples.map((value) => ({ value }));
|
|
108
|
+
return [{ omitted: true }];
|
|
109
|
+
}
|
|
98
110
|
if (countGeneratedExampleValues(source.schema) > MAX_GENERATED_EXAMPLE_VALUES)
|
|
99
111
|
return [{ omitted: true }];
|
|
112
|
+
if (isXmlMediaType(mediaType)) {
|
|
113
|
+
const result = getXmlBodyExample(source.schema, undefined, {
|
|
114
|
+
mode,
|
|
115
|
+
openapiVersion: schemaOpenapiVersion,
|
|
116
|
+
});
|
|
117
|
+
return [
|
|
118
|
+
result.xml === undefined ? { error: 'Unable to generate an XML example.' } : { serializedValue: result.xml },
|
|
119
|
+
];
|
|
120
|
+
}
|
|
100
121
|
const value = getExampleFromSchema(getResolvedRef(source.schema, mergeSiblingReferences), {
|
|
101
|
-
xml: mediaType.includes('xml'),
|
|
102
122
|
mode,
|
|
103
123
|
});
|
|
104
124
|
return value === undefined ? [] : [{ value }];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { HttpMethod } from '@scalar/helpers/http/http-methods';
|
|
2
2
|
export type { OpenApiMarkdownRenderer } from './create-markdown-from-openapi.js';
|
|
3
3
|
export { createMarkdownFromOpenApi, createOpenApiMarkdownRenderer, } from './create-markdown-from-openapi.js';
|
|
4
|
-
export type { OpenApiRenderOptions, OperationSelector } from './select-document.js';
|
|
4
|
+
export type { OpenApiRenderOptions, OperationSelector, SchemaReferenceOptions } from './select-document.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAA;AAEnE,YAAY,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AAC7E,OAAO,EACL,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,gCAAgC,CAAA;AACvC,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAA;AAEnE,YAAY,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AAC7E,OAAO,EACL,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,gCAAgC,CAAA;AACvC,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-document.d.ts","sourceRoot":"","sources":["../src/load-document.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"load-document.d.ts","sourceRoot":"","sources":["../src/load-document.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,8DAA8D,CAAA;AA6LrE,8EAA8E;AAC9E,eAAO,MAAM,YAAY,GACvB,OAAO,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,KACxD,OAAO,CAAC,eAAe,CAgGzB,CAAA"}
|
package/dist/load-document.js
CHANGED
|
@@ -9,21 +9,99 @@ import { getRaw } from '@scalar/json-magic/magic-proxy';
|
|
|
9
9
|
import { upgrade } from '@scalar/openapi-upgrader';
|
|
10
10
|
import { deepClone } from '@scalar/workspace-store/helpers/deep-clone';
|
|
11
11
|
import { coerceValue } from '@scalar/workspace-store/schemas/typebox-coerce';
|
|
12
|
-
import { OpenAPIDocumentSchema, } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
13
|
-
import {
|
|
12
|
+
import { OpenAPIDocumentSchema, SchemaObjectSchema, } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
13
|
+
import { getChildKind, referenceTargetSchemas } from './object-kinds.js';
|
|
14
|
+
import { getChildPosition, restoreBooleanSchemas } from './restore-boolean-schemas.js';
|
|
14
15
|
/**
|
|
15
16
|
* Link references in a private, bundled document without proxies or expanded copies.
|
|
16
17
|
* JSON Magic owns `$id` and anchor indexing, which keeps local-reference behavior
|
|
17
18
|
* aligned with the external-reference bundler without retaining a second resolver.
|
|
18
19
|
* Non-enumerable links preserve serialization and share recursive schema targets.
|
|
19
|
-
*
|
|
20
|
+
* With `detectOnly`, nothing is linked and the pass only looks for external references.
|
|
20
21
|
* Returns whether external references remain and require bundling.
|
|
21
22
|
*/
|
|
22
|
-
const attachRefValues = (document,
|
|
23
|
-
//
|
|
23
|
+
const attachRefValues = (document, schemas = getSchemas(document), { detectOnly = false, fallback } = {}) => {
|
|
24
|
+
// Fallback targets are linked outside the walked tree and can be reached again from it,
|
|
25
|
+
// so one set keeps every object to a single visit.
|
|
24
26
|
const seen = new WeakSet();
|
|
27
|
+
// Large descriptions repeat the same reference strings thousands of times.
|
|
28
|
+
const targets = new Map();
|
|
29
|
+
// Share one cast copy between every reference of the same kind to the same dropped target.
|
|
30
|
+
const castFallbacks = new WeakMap();
|
|
25
31
|
let hasExternalReferences = false;
|
|
26
|
-
const
|
|
32
|
+
const lookUp = (path, ref) => {
|
|
33
|
+
// An empty path is a resource root, unless the fragment is the pointer to the empty key.
|
|
34
|
+
const segments = path === '' && !ref.includes('#/') ? [] : parseJsonPointerSegments(`/${path}`);
|
|
35
|
+
const linked = getValueByPath(document, segments);
|
|
36
|
+
let position = 'document';
|
|
37
|
+
let kind = 'document';
|
|
38
|
+
let parent = document;
|
|
39
|
+
for (const key of segments) {
|
|
40
|
+
kind = kind && getChildKind(kind, key);
|
|
41
|
+
position = Array.isArray(parent) ? position : position && getChildPosition(position, key);
|
|
42
|
+
// Arbitrary extension and example data has not passed an OpenAPI object schema.
|
|
43
|
+
if (position === 'document' && kind === undefined)
|
|
44
|
+
position = undefined;
|
|
45
|
+
parent = getValueByPath(parent, [key]).value;
|
|
46
|
+
}
|
|
47
|
+
const schema = getTargetSchema(position, kind);
|
|
48
|
+
if ((linked.value !== undefined && schema !== undefined) || fallback === undefined) {
|
|
49
|
+
return { ...linked, linked: true, schema };
|
|
50
|
+
}
|
|
51
|
+
// The context is the base of the resource that contains the fallback target.
|
|
52
|
+
return { ...getValueByPath(fallback.source, segments), linked: false };
|
|
53
|
+
};
|
|
54
|
+
const resolve = (ref, base) => {
|
|
55
|
+
const key = `${base}\u0000${ref}`;
|
|
56
|
+
if (targets.has(key)) {
|
|
57
|
+
return targets.get(key);
|
|
58
|
+
}
|
|
59
|
+
// JSON Magic has no entry for an empty fragment, which names the root of the current resource.
|
|
60
|
+
const path = ref === '#' ? (schemas.get(base) ?? '') : convertToLocalRef(ref, base, schemas);
|
|
61
|
+
const resolved = path === undefined ? undefined : lookUp(path, ref);
|
|
62
|
+
targets.set(key, resolved);
|
|
63
|
+
return resolved;
|
|
64
|
+
};
|
|
65
|
+
// Cast dropped targets like the objects they stand in for, so the renderer never sees uncoerced shapes.
|
|
66
|
+
const castFallback = (value, schema, position) => {
|
|
67
|
+
// OpenAPI 3.1 keeps boolean schemas, and a nested reference is linked once it is walked.
|
|
68
|
+
if (value === undefined || (fallback?.booleanSchemas && position === 'schema' && typeof value === 'boolean')) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
if (!isObject(value)) {
|
|
72
|
+
return coerceValue(schema, value);
|
|
73
|
+
}
|
|
74
|
+
const casts = castFallbacks.get(value) ?? new Map();
|
|
75
|
+
castFallbacks.set(value, casts);
|
|
76
|
+
if (!casts.has(schema)) {
|
|
77
|
+
// Cast schema siblings without losing the reference. Other Reference Objects only
|
|
78
|
+
// support summary and description; casting them as their targets would add defaults.
|
|
79
|
+
// Path Item Objects declare their own $ref field, so their siblings are cast normally.
|
|
80
|
+
const reference = typeof value.$ref === 'string' ? value.$ref : undefined;
|
|
81
|
+
const cast = reference === undefined || schema === referenceTargetSchemas.pathItem
|
|
82
|
+
? coerceValue(schema, value)
|
|
83
|
+
: position === 'schema'
|
|
84
|
+
? { ...coerceValue(SchemaObjectSchema, value), $ref: reference }
|
|
85
|
+
: {
|
|
86
|
+
$ref: reference,
|
|
87
|
+
...(typeof value.summary === 'string' ? { summary: value.summary } : {}),
|
|
88
|
+
...(typeof value.description === 'string' ? { description: value.description } : {}),
|
|
89
|
+
};
|
|
90
|
+
if (fallback?.booleanSchemas) {
|
|
91
|
+
restoreBooleanSchemas(value, cast, position);
|
|
92
|
+
}
|
|
93
|
+
casts.set(schema, cast);
|
|
94
|
+
}
|
|
95
|
+
return casts.get(schema);
|
|
96
|
+
};
|
|
97
|
+
// Schemas are recognized by their position, and every other object by the field that holds it.
|
|
98
|
+
const getTargetSchema = (position, kind) => {
|
|
99
|
+
if (position === 'schema') {
|
|
100
|
+
return SchemaObjectSchema;
|
|
101
|
+
}
|
|
102
|
+
return typeof kind === 'string' ? referenceTargetSchemas[kind] : undefined;
|
|
103
|
+
};
|
|
104
|
+
const visit = (node, context, position, kind) => {
|
|
27
105
|
if (node === null || typeof node !== 'object' || seen.has(node)) {
|
|
28
106
|
return;
|
|
29
107
|
}
|
|
@@ -32,45 +110,40 @@ const attachRefValues = (document, enumerable = false, schemas = getSchemas(docu
|
|
|
32
110
|
const base = getId(node) ?? context;
|
|
33
111
|
if (isObject(node) && typeof node.$ref === 'string') {
|
|
34
112
|
const ref = node.$ref;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (path === undefined && ref.split('#')[0]) {
|
|
40
|
-
hasExternalReferences = true;
|
|
41
|
-
}
|
|
42
|
-
if (enumerable) {
|
|
43
|
-
const followed = new WeakSet();
|
|
44
|
-
let resolved = target;
|
|
45
|
-
// TypeBox needs the final target, so collapse a pre-existing reference chain safely.
|
|
46
|
-
while (isObject(resolved) && '$ref-value' in resolved && !followed.has(resolved)) {
|
|
47
|
-
followed.add(resolved);
|
|
48
|
-
resolved = resolved['$ref-value'];
|
|
113
|
+
if (detectOnly) {
|
|
114
|
+
// JSON Magic only needs to bundle references outside this document's resource index.
|
|
115
|
+
if (ref.split('#')[0] && convertToLocalRef(ref, base, schemas) === undefined) {
|
|
116
|
+
hasExternalReferences = true;
|
|
49
117
|
}
|
|
50
|
-
|
|
118
|
+
}
|
|
119
|
+
else if (getTargetSchema(position, kind)) {
|
|
120
|
+
const resolved = resolve(ref, base);
|
|
121
|
+
const expectedSchema = getTargetSchema(position, kind);
|
|
122
|
+
const schema = resolved?.schema === expectedSchema ? undefined : expectedSchema;
|
|
123
|
+
// Point to the target instead of copying it into every reference.
|
|
124
|
+
const target = schema ? castFallback(resolved?.value, schema, position ?? 'document') : resolved?.value;
|
|
125
|
+
if (target !== undefined) {
|
|
51
126
|
Object.defineProperty(node, '$ref-value', {
|
|
52
|
-
value:
|
|
53
|
-
enumerable,
|
|
127
|
+
value: target,
|
|
128
|
+
enumerable: false,
|
|
54
129
|
configurable: true,
|
|
55
130
|
writable: true,
|
|
56
131
|
});
|
|
57
132
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
enumerable,
|
|
63
|
-
configurable: true,
|
|
64
|
-
writable: true,
|
|
65
|
-
});
|
|
133
|
+
// Fallback targets live outside the walked tree, so link their own references too.
|
|
134
|
+
if (resolved && (!resolved.linked || schema)) {
|
|
135
|
+
visit(target, resolved.context, position, kind);
|
|
136
|
+
}
|
|
66
137
|
}
|
|
67
138
|
}
|
|
68
139
|
// Preserve the nearest resource base while walking into child schemas.
|
|
69
|
-
for (const child of Object.
|
|
70
|
-
|
|
140
|
+
for (const [key, child] of Object.entries(node)) {
|
|
141
|
+
const childKind = kind && getChildKind(kind, key);
|
|
142
|
+
const childPosition = Array.isArray(node) ? position : position && getChildPosition(position, key);
|
|
143
|
+
visit(child, base, childPosition === 'document' && childKind === undefined ? undefined : childPosition, childKind);
|
|
71
144
|
}
|
|
72
145
|
};
|
|
73
|
-
visit(document);
|
|
146
|
+
visit(document, getId(document) ?? '', 'document', 'document');
|
|
74
147
|
return hasExternalReferences;
|
|
75
148
|
};
|
|
76
149
|
/** Coerce one plain document and keep references linked to shared targets. */
|
|
@@ -108,7 +181,7 @@ export const loadDocument = async (input) => {
|
|
|
108
181
|
const declaredOpenapiVersion = typeof raw.openapi === 'string' ? raw.openapi : '2.0';
|
|
109
182
|
const { document: upgraded } = upgrade(raw, '3.2', { onIncompatible: 'collect' });
|
|
110
183
|
const upgradedSchemas = getSchemas(upgraded);
|
|
111
|
-
const hasExternalReferences = attachRefValues(upgraded,
|
|
184
|
+
const hasExternalReferences = attachRefValues(upgraded, upgradedSchemas, { detectOnly: true });
|
|
112
185
|
let document = upgraded;
|
|
113
186
|
let schemas = upgradedSchemas;
|
|
114
187
|
// Resolve external targets only when the initial local-reference pass finds them.
|
|
@@ -130,17 +203,21 @@ export const loadDocument = async (input) => {
|
|
|
130
203
|
throw new Error(errors.join('\n'));
|
|
131
204
|
}
|
|
132
205
|
schemas = getSchemas(document);
|
|
133
|
-
attachRefValues(document, false, schemas);
|
|
134
206
|
}
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
207
|
+
// Cast the document before linking references. TypeBox clones every value that already
|
|
208
|
+
// matches a union or array schema, and its clone follows all own properties, including
|
|
209
|
+
// non-enumerable `$ref-value` links. On densely linked descriptions, each clone would copy
|
|
210
|
+
// most of the reference graph again, which costs gigabytes of transient memory.
|
|
211
|
+
// Reference branches accept a missing `$ref-value`, and every target is cast in its own
|
|
212
|
+
// position, so one target that fails a strict check can no longer replace the reference.
|
|
138
213
|
const coerced = coerceValue(OpenAPIDocumentSchema, document);
|
|
139
214
|
// Rendering must use the declared version for features added after OpenAPI 3.1.
|
|
140
215
|
coerced['x-original-oas-version'] = declaredOpenapiVersion;
|
|
141
216
|
// Boolean schemas were introduced in OpenAPI 3.1; older descriptions retain their existing coercion.
|
|
142
|
-
|
|
217
|
+
const booleanSchemas = /^3\.[12]\./.test(declaredOpenapiVersion);
|
|
218
|
+
if (booleanSchemas) {
|
|
143
219
|
restoreBooleanSchemas(document, coerced);
|
|
220
|
+
}
|
|
144
221
|
// Keep extension resources that local and bundled references can target.
|
|
145
222
|
for (const [key, value] of Object.entries(document)) {
|
|
146
223
|
if (key.startsWith('x-') && !(key in coerced)) {
|
|
@@ -152,7 +229,8 @@ export const loadDocument = async (input) => {
|
|
|
152
229
|
});
|
|
153
230
|
}
|
|
154
231
|
}
|
|
155
|
-
//
|
|
156
|
-
|
|
232
|
+
// Link references to coerced targets. Targets that coercion dropped fall back to source values,
|
|
233
|
+
// which are cast according to the object kind expected at the reference.
|
|
234
|
+
attachRefValues(coerced, schemas, { fallback: { source: document, booleanSchemas } });
|
|
157
235
|
return coerced;
|
|
158
236
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { coerceValue } from '@scalar/workspace-store/schemas/typebox-coerce';
|
|
2
|
+
/** The OpenAPI objects outside of schemas that the linking pass tells apart. */
|
|
3
|
+
export type ObjectKind = 'document' | 'components' | 'pathItem' | 'operation' | 'callback' | 'response' | 'requestBody' | 'mediaType' | 'encoding' | 'parameter' | 'header' | 'example' | 'link' | 'securityScheme';
|
|
4
|
+
/** A value is either one object of a kind, or a map or array whose values all have that kind. */
|
|
5
|
+
export type KindPosition = ObjectKind | {
|
|
6
|
+
values: ObjectKind;
|
|
7
|
+
};
|
|
8
|
+
type TargetSchema = Parameters<typeof coerceValue>[0];
|
|
9
|
+
/** Returns the kind of the value stored under `key`, or `undefined` outside the known structure. */
|
|
10
|
+
export declare const getChildKind: (position: KindPosition, key: string) => KindPosition | undefined;
|
|
11
|
+
/** Strict schemas for the objects that a Reference Object can stand in for. */
|
|
12
|
+
export declare const referenceTargetSchemas: Partial<Record<ObjectKind, TargetSchema>>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=object-kinds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-kinds.d.ts","sourceRoot":"","sources":["../src/object-kinds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gDAAgD,CAAA;AAejF,gFAAgF;AAChF,MAAM,MAAM,UAAU,GAClB,UAAU,GACV,YAAY,GACZ,UAAU,GACV,WAAW,GACX,UAAU,GACV,UAAU,GACV,aAAa,GACb,WAAW,GACX,UAAU,GACV,WAAW,GACX,QAAQ,GACR,SAAS,GACT,MAAM,GACN,gBAAgB,CAAA;AAEpB,iGAAiG;AACjG,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAAA;AAE9D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAmDrD,oGAAoG;AACpG,eAAO,MAAM,YAAY,GAAI,UAAU,YAAY,EAAE,KAAK,MAAM,KAAG,YAAY,GAAG,SAMjF,CAAA;AAED,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAY5E,CAAA"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CallbackObjectSchema, ExampleObjectSchema, HeaderObjectSchema, LinkObjectSchema, MediaTypeObjectSchema, OperationObjectSchema, ParameterObjectSchema, PathItemObjectSchema, RequestBodyObjectSchema, ResponseObjectSchema, SecuritySchemeObjectSchema, } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
2
|
+
const methods = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace', 'connect', 'query'];
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors the OpenAPI 3.2 structure of the strict workspace schemas, so a reference knows which
|
|
5
|
+
* object it stands in for. A `*` key applies to every field, such as the expressions of a callback.
|
|
6
|
+
*/
|
|
7
|
+
const childKinds = {
|
|
8
|
+
document: {
|
|
9
|
+
paths: { values: 'pathItem' },
|
|
10
|
+
webhooks: { values: 'pathItem' },
|
|
11
|
+
components: 'components',
|
|
12
|
+
},
|
|
13
|
+
components: {
|
|
14
|
+
responses: { values: 'response' },
|
|
15
|
+
parameters: { values: 'parameter' },
|
|
16
|
+
examples: { values: 'example' },
|
|
17
|
+
requestBodies: { values: 'requestBody' },
|
|
18
|
+
headers: { values: 'header' },
|
|
19
|
+
securitySchemes: { values: 'securityScheme' },
|
|
20
|
+
links: { values: 'link' },
|
|
21
|
+
callbacks: { values: 'callback' },
|
|
22
|
+
pathItems: { values: 'pathItem' },
|
|
23
|
+
mediaTypes: { values: 'mediaType' },
|
|
24
|
+
},
|
|
25
|
+
pathItem: {
|
|
26
|
+
...Object.fromEntries(methods.map((method) => [method, 'operation'])),
|
|
27
|
+
additionalOperations: { values: 'operation' },
|
|
28
|
+
parameters: { values: 'parameter' },
|
|
29
|
+
},
|
|
30
|
+
operation: {
|
|
31
|
+
parameters: { values: 'parameter' },
|
|
32
|
+
requestBody: 'requestBody',
|
|
33
|
+
responses: { values: 'response' },
|
|
34
|
+
callbacks: { values: 'callback' },
|
|
35
|
+
},
|
|
36
|
+
callback: { '*': 'pathItem' },
|
|
37
|
+
response: { headers: { values: 'header' }, content: { values: 'mediaType' }, links: { values: 'link' } },
|
|
38
|
+
requestBody: { content: { values: 'mediaType' } },
|
|
39
|
+
mediaType: {
|
|
40
|
+
examples: { values: 'example' },
|
|
41
|
+
encoding: { values: 'encoding' },
|
|
42
|
+
prefixEncoding: { values: 'encoding' },
|
|
43
|
+
itemEncoding: 'encoding',
|
|
44
|
+
},
|
|
45
|
+
encoding: { headers: { values: 'header' } },
|
|
46
|
+
parameter: { examples: { values: 'example' }, content: { values: 'mediaType' } },
|
|
47
|
+
header: { examples: { values: 'example' }, content: { values: 'mediaType' } },
|
|
48
|
+
};
|
|
49
|
+
/** Returns the kind of the value stored under `key`, or `undefined` outside the known structure. */
|
|
50
|
+
export const getChildKind = (position, key) => {
|
|
51
|
+
if (typeof position !== 'string') {
|
|
52
|
+
return position.values;
|
|
53
|
+
}
|
|
54
|
+
const children = childKinds[position];
|
|
55
|
+
return children?.[key] ?? children?.['*'];
|
|
56
|
+
};
|
|
57
|
+
/** Strict schemas for the objects that a Reference Object can stand in for. */
|
|
58
|
+
export const referenceTargetSchemas = {
|
|
59
|
+
pathItem: PathItemObjectSchema,
|
|
60
|
+
operation: OperationObjectSchema,
|
|
61
|
+
callback: CallbackObjectSchema,
|
|
62
|
+
response: ResponseObjectSchema,
|
|
63
|
+
requestBody: RequestBodyObjectSchema,
|
|
64
|
+
mediaType: MediaTypeObjectSchema,
|
|
65
|
+
parameter: ParameterObjectSchema,
|
|
66
|
+
header: HeaderObjectSchema,
|
|
67
|
+
example: ExampleObjectSchema,
|
|
68
|
+
link: LinkObjectSchema,
|
|
69
|
+
securityScheme: SecuritySchemeObjectSchema,
|
|
70
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
2
|
+
import type { SchemaReferenceOptions } from './select-document.js';
|
|
2
3
|
/** Build Markdown directly, retaining caches only for this immutable document snapshot. */
|
|
3
|
-
export declare const createDocumentRenderer: () => ((document: OpenApiDocument) => Promise<string>);
|
|
4
|
+
export declare const createDocumentRenderer: () => ((document: OpenApiDocument, options?: SchemaReferenceOptions) => Promise<string>);
|
|
4
5
|
//# sourceMappingURL=render-document.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-document.d.ts","sourceRoot":"","sources":["../src/render-document.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,8DAA8D,CAAA;
|
|
1
|
+
{"version":3,"file":"render-document.d.ts","sourceRoot":"","sources":["../src/render-document.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,8DAA8D,CAAA;AAYpH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAI/D,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,QAAO,CAAC,CACzC,QAAQ,EAAE,eAAe,EACzB,OAAO,CAAC,EAAE,sBAAsB,KAC7B,OAAO,CAAC,MAAM,CAAC,CA+InB,CAAA"}
|
package/dist/render-document.js
CHANGED
|
@@ -14,10 +14,10 @@ const serializer = unified().use(remarkGfm).use(remarkStringify, { bullet: '-' }
|
|
|
14
14
|
export const createDocumentRenderer = () => {
|
|
15
15
|
const descriptions = createDescriptionParser();
|
|
16
16
|
const schemaRenderer = createSchemaRenderer();
|
|
17
|
-
return async (document) => {
|
|
17
|
+
return async (document, options) => {
|
|
18
18
|
const description = descriptions();
|
|
19
19
|
// Each page expands a shared schema once, then refers back to it.
|
|
20
|
-
const schemas = schemaRenderer.forDocument(document.components?.schemas);
|
|
20
|
+
const schemas = schemaRenderer.forDocument(document.components?.schemas, options);
|
|
21
21
|
const { info } = document;
|
|
22
22
|
const metadata = [
|
|
23
23
|
field('OpenAPI Version', inlineCode(document.openapi)),
|
|
@@ -108,7 +108,7 @@ export const createDocumentRenderer = () => {
|
|
|
108
108
|
: item(paragraph(strong(text('Type:')))),
|
|
109
109
|
]), ...(await description(view.description)), ...schemas.render(schema, 0, [], { hideDescription: true, name }));
|
|
110
110
|
if (view.type === 'object')
|
|
111
|
-
nodes.push(...(await renderExamples({ schema }, description, 'application/json', undefined, document['x-original-oas-version'] ?? document.openapi)));
|
|
111
|
+
nodes.push(...(await renderExamples({ schema }, description, 'application/json', undefined, document['x-original-oas-version'] ?? document.openapi, document.openapi, schemas.linked)));
|
|
112
112
|
flush();
|
|
113
113
|
}
|
|
114
114
|
flush();
|
|
@@ -2,5 +2,5 @@ import type { RootContent } from 'mdast';
|
|
|
2
2
|
import { type ExampleSource } from './get-markdown-examples.js';
|
|
3
3
|
import type { DescriptionParser } from './parse-description.js';
|
|
4
4
|
/** 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) => Promise<RootContent[]>;
|
|
5
|
+
export declare const renderExamples: (source: ExampleSource, description: DescriptionParser, mediaType?: string, mode?: "read" | "write", openapiVersion?: string, schemaOpenapiVersion?: string, linked?: boolean) => Promise<RootContent[]>;
|
|
6
6
|
//# sourceMappingURL=render-examples.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-examples.d.ts","sourceRoot":"","sources":["../src/render-examples.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/render-examples.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isXmlMediaType } from '@scalar/helpers/http/is-xml-media-type';
|
|
2
|
+
import { getXmlBodyExample } from '@scalar/workspace-store/request-example';
|
|
2
3
|
import { getMarkdownExamples } from './get-markdown-examples.js';
|
|
3
4
|
import { link, paragraph, strong, text } from './markdown-nodes.js';
|
|
4
5
|
/** Render supplied examples before considering a schema-generated fallback. */
|
|
5
|
-
export const renderExamples = async (source, description, mediaType = 'application/json', mode, openapiVersion = '3.2.0'
|
|
6
|
+
export const renderExamples = async (source, description, mediaType = 'application/json', mode, openapiVersion = '3.2.0',
|
|
7
|
+
// Schema metadata can be upgraded while example fields still follow the original version.
|
|
8
|
+
schemaOpenapiVersion = openapiVersion, linked = false) => {
|
|
6
9
|
const nodes = [];
|
|
7
|
-
for (const example of getMarkdownExamples(source, mediaType, mode, openapiVersion)) {
|
|
10
|
+
for (const example of getMarkdownExamples(source, mediaType, mode, openapiVersion, schemaOpenapiVersion, linked)) {
|
|
8
11
|
nodes.push(paragraph(strong(text(example.name ? `Example: ${example.name}` : 'Example:'))));
|
|
9
12
|
if ('omitted' in example) {
|
|
10
|
-
nodes.push(paragraph(text('[Generated example omitted because it is too large]')));
|
|
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]'}`)));
|
|
11
14
|
continue;
|
|
12
15
|
}
|
|
13
16
|
if (example.summary)
|
|
14
17
|
nodes.push(paragraph(text(example.summary)));
|
|
15
18
|
nodes.push(...(await description(example.description)));
|
|
19
|
+
if ('error' in example) {
|
|
20
|
+
nodes.push(paragraph(text(example.error)));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
16
23
|
if ('externalValue' in example) {
|
|
17
24
|
nodes.push(paragraph(strong(text('External value:')), text(' '), link(example.externalValue, example.externalValue)));
|
|
18
25
|
continue;
|
|
@@ -20,21 +27,31 @@ export const renderExamples = async (source, description, mediaType = 'applicati
|
|
|
20
27
|
if ('serializedValue' in example) {
|
|
21
28
|
nodes.push({
|
|
22
29
|
type: 'code',
|
|
23
|
-
lang: mediaType
|
|
30
|
+
lang: isXmlMediaType(mediaType) ? 'xml' : mediaType.includes('json') ? 'json' : 'text',
|
|
24
31
|
value: example.serializedValue,
|
|
25
32
|
});
|
|
26
33
|
continue;
|
|
27
34
|
}
|
|
28
|
-
const xml = mediaType
|
|
35
|
+
const xml = isXmlMediaType(mediaType);
|
|
36
|
+
const value = 'dataValue' in example ? example.dataValue : example.value;
|
|
37
|
+
if (xml && !source.schema && 'value' in example && (value === null || typeof value !== 'object')) {
|
|
38
|
+
nodes.push({ type: 'code', lang: 'xml', value: String(value) });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const result = xml
|
|
42
|
+
? getXmlBodyExample(source.schema, example, {
|
|
43
|
+
mode,
|
|
44
|
+
openapiVersion: schemaOpenapiVersion,
|
|
45
|
+
})
|
|
46
|
+
: undefined;
|
|
47
|
+
if (xml && result?.xml === undefined) {
|
|
48
|
+
nodes.push(paragraph(text('Unable to generate an XML example.')));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
29
51
|
nodes.push({
|
|
30
52
|
type: 'code',
|
|
31
53
|
lang: xml ? 'xml' : 'json',
|
|
32
|
-
|
|
33
|
-
value: xml
|
|
34
|
-
? example.value !== null && typeof example.value === 'object'
|
|
35
|
-
? json2xml(example.value)
|
|
36
|
-
: String(example.value)
|
|
37
|
-
: (JSON.stringify(example.value, null, 2) ?? ''),
|
|
54
|
+
value: result?.xml ?? JSON.stringify(value, null, 2) ?? '',
|
|
38
55
|
});
|
|
39
56
|
}
|
|
40
57
|
return nodes;
|
|
@@ -3,9 +3,9 @@ import type { RootContent } from 'mdast';
|
|
|
3
3
|
import type { DescriptionParser } from './parse-description.js';
|
|
4
4
|
import type { SchemaRenderer } from './render-schema.js';
|
|
5
5
|
/** 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) => Promise<RootContent[]>;
|
|
6
|
+
export declare const renderHeaders: (headers: ResponseObject["headers"], description: DescriptionParser, schemas: SchemaRenderer, openapiVersion: string, schemaOpenapiVersion?: string) => Promise<RootContent[]>;
|
|
7
7
|
/** 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) => Promise<RootContent[]>;
|
|
8
|
+
export declare const renderEncoding: (encoding: Record<string, EncodingObject> | undefined, mediaType: string, description: DescriptionParser, schemas: SchemaRenderer, openapiVersion: string, schemaOpenapiVersion?: string) => Promise<RootContent[]>;
|
|
9
9
|
/** Render response link expressions as literal values, without evaluating them. */
|
|
10
10
|
export declare const renderResponseLinks: (links: ResponseObject["links"], description: DescriptionParser) => Promise<RootContent[]>;
|
|
11
11
|
//# 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,
|
|
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"}
|
|
@@ -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) => {
|
|
5
|
+
export const renderHeaders = async (headers, description, schemas, openapiVersion, schemaOpenapiVersion = openapiVersion) => {
|
|
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,20 @@ 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)));
|
|
21
|
+
blocks.push(...(await renderExamples({ example: header.example, examples: header.examples }, description, 'application/json', undefined, openapiVersion, schemaOpenapiVersion, schemas.linked)));
|
|
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)));
|
|
27
|
+
blocks.push(...(await renderExamples(content, description, mediaType, undefined, openapiVersion, schemaOpenapiVersion, schemas.linked)));
|
|
28
28
|
}
|
|
29
29
|
entries.push(item(...blocks));
|
|
30
30
|
}
|
|
31
31
|
return entries.length ? [paragraph(strong(text('Headers:'))), list(entries)] : [];
|
|
32
32
|
};
|
|
33
33
|
/** Preserve explicit encoding settings, including false flags and part headers. */
|
|
34
|
-
export const renderEncoding = async (encoding, mediaType, description, schemas, openapiVersion) => {
|
|
34
|
+
export const renderEncoding = async (encoding, mediaType, description, schemas, openapiVersion, schemaOpenapiVersion = openapiVersion) => {
|
|
35
35
|
const multipart = mediaType.startsWith('multipart/');
|
|
36
36
|
if (!multipart && mediaType !== 'application/x-www-form-urlencoded')
|
|
37
37
|
return [];
|
|
@@ -51,7 +51,7 @@ export const renderEncoding = async (encoding, mediaType, description, schemas,
|
|
|
51
51
|
if (fields.length)
|
|
52
52
|
blocks.push(list(fields));
|
|
53
53
|
if (multipart)
|
|
54
|
-
blocks.push(...(await renderHeaders(entry.headers, description, schemas, openapiVersion)));
|
|
54
|
+
blocks.push(...(await renderHeaders(entry.headers, description, schemas, openapiVersion, schemaOpenapiVersion)));
|
|
55
55
|
entries.push(item(...blocks));
|
|
56
56
|
}
|
|
57
57
|
return entries.length ? [paragraph(strong(text('Encoding:'))), list(entries)] : [];
|
|
@@ -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,
|
|
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"}
|
package/dist/render-operation.js
CHANGED
|
@@ -59,12 +59,12 @@ export const renderOperation = async (document, path, method, pathItem, operatio
|
|
|
59
59
|
if ('schema' in parameter && parameter.schema !== undefined)
|
|
60
60
|
nodes.push(...schemas.render(parameter.schema));
|
|
61
61
|
if ('example' in parameter || 'examples' in parameter)
|
|
62
|
-
nodes.push(...(await renderExamples({ example: parameter.example, examples: parameter.examples }, description, 'application/json', 'write', openapiVersion)));
|
|
62
|
+
nodes.push(...(await renderExamples({ example: parameter.example, examples: parameter.examples }, description, 'application/json', 'write', openapiVersion, document.openapi)));
|
|
63
63
|
for (const [mediaType, content] of Object.entries('content' in parameter ? (parameter.content ?? {}) : {})) {
|
|
64
64
|
nodes.push(heading(6, text(`Content-Type: ${mediaType}`)));
|
|
65
65
|
if (content.schema !== undefined)
|
|
66
66
|
nodes.push(...schemas.render(content.schema));
|
|
67
|
-
nodes.push(...(await renderExamples(content, description, mediaType, 'write', openapiVersion)));
|
|
67
|
+
nodes.push(...(await renderExamples(content, description, mediaType, 'write', openapiVersion, document.openapi, schemas.linked)));
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
const body = getResolvedRef(operation.requestBody, mergeSiblingReferences);
|
|
@@ -76,8 +76,8 @@ export const renderOperation = async (document, path, method, pathItem, operatio
|
|
|
76
76
|
nodes.push(heading(5, text(`Content-Type: ${mediaType}`)));
|
|
77
77
|
if (content.schema !== undefined)
|
|
78
78
|
nodes.push(...schemas.render(content.schema));
|
|
79
|
-
nodes.push(...(await renderExamples(content, description, mediaType, 'write', openapiVersion)));
|
|
80
|
-
nodes.push(...(await renderEncoding(content.encoding, mediaType, description, schemas, openapiVersion)));
|
|
79
|
+
nodes.push(...(await renderExamples(content, description, mediaType, 'write', openapiVersion, document.openapi, schemas.linked)));
|
|
80
|
+
nodes.push(...(await renderEncoding(content.encoding, mediaType, description, schemas, openapiVersion, document.openapi)));
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
const responses = Object.entries(operation.responses ?? {}).flatMap(([status, reference]) => {
|
|
@@ -88,12 +88,12 @@ export const renderOperation = async (document, path, method, pathItem, operatio
|
|
|
88
88
|
nodes.push(heading(4, text('Responses')));
|
|
89
89
|
for (const { status, response } of responses) {
|
|
90
90
|
nodes.push(heading(5, text(`Status: ${status}${response.description ? ` ${response.description}` : ''}`)));
|
|
91
|
-
nodes.push(...(await renderHeaders(response.headers, description, schemas, openapiVersion)), ...(await renderResponseLinks(response.links, description)));
|
|
91
|
+
nodes.push(...(await renderHeaders(response.headers, description, schemas, openapiVersion, document.openapi)), ...(await renderResponseLinks(response.links, description)));
|
|
92
92
|
for (const [mediaType, content] of Object.entries(response.content ?? {})) {
|
|
93
93
|
nodes.push(heading(6, text(`Content-Type: ${mediaType}`)));
|
|
94
94
|
if (content.schema !== undefined)
|
|
95
95
|
nodes.push(...schemas.render(content.schema));
|
|
96
|
-
nodes.push(...(await renderExamples(content, description, mediaType, 'read', openapiVersion)));
|
|
96
|
+
nodes.push(...(await renderExamples(content, description, mediaType, 'read', openapiVersion, document.openapi, schemas.linked)));
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
return nodes;
|
package/dist/render-schema.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MaybeRefSchemaObject, SchemaObject } from '@scalar/workspace-store/schemas/v3.2/strict/schema';
|
|
2
2
|
import type { RootContent } from 'mdast';
|
|
3
|
+
import type { SchemaReferenceOptions } from './select-document.js';
|
|
3
4
|
/** Boolean schemas must survive rendering without being coerced into empty objects. */
|
|
4
5
|
type MarkdownSchema = MaybeRefSchemaObject | boolean;
|
|
5
6
|
type SchemaView = {
|
|
@@ -56,12 +57,13 @@ type RenderOptions = {
|
|
|
56
57
|
* run concurrently, and each needs its own record.
|
|
57
58
|
*/
|
|
58
59
|
export type SchemaRenderer = {
|
|
60
|
+
linked: boolean;
|
|
59
61
|
view: (schema: MarkdownSchema) => SchemaView;
|
|
60
62
|
render: (schema: MarkdownSchema, depth?: number, ancestors?: readonly unknown[], options?: RenderOptions) => RootContent[];
|
|
61
63
|
/** Restore the node budget, for example for each operation or model section. */
|
|
62
64
|
beginSection: () => void;
|
|
63
65
|
/** Start a document that shares normalized views, given the models it renders in its own sections. */
|
|
64
|
-
forDocument: (models?: Record<string, MarkdownSchema
|
|
66
|
+
forDocument: (models?: Record<string, MarkdownSchema>, options?: SchemaReferenceOptions) => SchemaRenderer;
|
|
65
67
|
};
|
|
66
68
|
type SchemaRendererOptions = {
|
|
67
69
|
/** Upper bound on expanded schema nodes per section, so no description can make a page unbounded. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-schema.d.ts","sourceRoot":"","sources":["../src/render-schema.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-schema.d.ts","sourceRoot":"","sources":["../src/render-schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,oDAAoD,CAAA;AAC5G,OAAO,KAAK,EAAuC,WAAW,EAAE,MAAM,OAAO,CAAA;AAG7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA;AAE/D,uFAAuF;AACvF,KAAK,cAAc,GAAG,oBAAoB,GAAG,OAAO,CAAA;AAEpD,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,YAAY,GAAG,OAAO,CAAA;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,KAAK,CAAC,EAAE,cAAc,EAAE,CAAA;IACxB,KAAK,CAAC,EAAE,cAAc,EAAE,CAAA;IACxB,KAAK,CAAC,EAAE,cAAc,EAAE,CAAA;IACxB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,UAAU,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAA;IACtC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC7B,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oBAAoB,CAAC,EAAE,cAAc,CAAA;IACrC,aAAa,CAAC,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAA;IAC1E,+GAA+G;IAC/G,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,KAAK,aAAa,GAAG;IACnB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,kHAAkH;IAClH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,OAAO,CAAA;IACf,IAAI,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,UAAU,CAAA;IAC5C,MAAM,EAAE,CACN,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,CAAC,EAAE,aAAa,KACpB,WAAW,EAAE,CAAA;IAClB,gFAAgF;IAChF,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,sGAAsG;IACtG,WAAW,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,EAAE,sBAAsB,KAAK,cAAc,CAAA;CAC3G,CAAA;AAED,KAAK,qBAAqB,GAAG;IAC3B,qGAAqG;IACrG,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAwED,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,GAAI,eAA0B,qBAA0B,KAAG,cA8O3F,CAAA"}
|
package/dist/render-schema.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { unescapeJsonPointer } from '@scalar/helpers/json/unescape-json-pointer';
|
|
2
1
|
import { isObject } from '@scalar/helpers/object/is-object';
|
|
3
2
|
import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
4
|
-
import { inlineCode, item, list, paragraph, strong, text } from './markdown-nodes.js';
|
|
3
|
+
import { inlineCode, item, link, list, paragraph, safeUrl, strong, text } from './markdown-nodes.js';
|
|
5
4
|
/** Guards against stack exhaustion. Past it, references to models point to their own section instead. */
|
|
6
5
|
const MAX_DEPTH = 64;
|
|
7
6
|
/**
|
|
@@ -29,11 +28,10 @@ const referenceKeys = new Set(['$ref', '$ref-value', '$global', '$status']);
|
|
|
29
28
|
const emphasis = (...children) => ({ type: 'emphasis', children });
|
|
30
29
|
/** Prefer the component name, which is how model sections and other references identify the schema. */
|
|
31
30
|
const getReferenceName = (ref) => {
|
|
32
|
-
const match = /^#\/components\/schemas\/([^/]+)$/.exec(ref);
|
|
33
|
-
if (!match)
|
|
34
|
-
return ref;
|
|
35
31
|
try {
|
|
36
|
-
|
|
32
|
+
// URI fragments are decoded before JSON Pointer segments and escapes, exactly once.
|
|
33
|
+
const match = /^#\/components\/schemas\/([^/]+)$/.exec(decodeURIComponent(ref));
|
|
34
|
+
return match ? match[1].replaceAll('~1', '/').replaceAll('~0', '~') : ref;
|
|
37
35
|
}
|
|
38
36
|
catch {
|
|
39
37
|
return ref;
|
|
@@ -133,7 +131,8 @@ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
|
133
131
|
nodes.push(text(`${nodes.length ? ' — ' : ''}${value.description}`));
|
|
134
132
|
return nodes;
|
|
135
133
|
};
|
|
136
|
-
const forDocument = (models = {}) => {
|
|
134
|
+
const forDocument = (models = {}, settings = {}) => {
|
|
135
|
+
const linked = settings.schemaReferences?.mode === 'linked';
|
|
137
136
|
/** Shared schemas this document already expanded, with the name later references use. */
|
|
138
137
|
const shown = new Map();
|
|
139
138
|
/** Models that the document renders in their own sections after the operations. */
|
|
@@ -152,6 +151,31 @@ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
|
152
151
|
return nodes;
|
|
153
152
|
};
|
|
154
153
|
const render = (input, depth = 0, ancestors = [], options = {}) => {
|
|
154
|
+
if (linked && isObject(input) && '$ref' in input && typeof input.$ref === 'string') {
|
|
155
|
+
const target = getResolvedRef(input);
|
|
156
|
+
// Reference siblings are independent constraints, not replacements for target keywords.
|
|
157
|
+
const siblings = Object.fromEntries(Object.entries(input).filter(([key]) => !referenceKeys.has(key)));
|
|
158
|
+
const hasSiblings = Object.keys(siblings).length > 0;
|
|
159
|
+
if (depth > 0 || target === undefined) {
|
|
160
|
+
const name = getReferenceName(input.$ref);
|
|
161
|
+
const url = settings.schemaReferences?.resolveUrl?.({ ref: input.$ref, name });
|
|
162
|
+
const nodes = [
|
|
163
|
+
paragraph(text('Schema: '), url && safeUrl(url) ? link(url, name) : inlineCode(name)),
|
|
164
|
+
];
|
|
165
|
+
if (hasSiblings)
|
|
166
|
+
nodes.push(...render(siblings, depth, ancestors));
|
|
167
|
+
return nodes;
|
|
168
|
+
}
|
|
169
|
+
// An alias with siblings is another reference boundary. Keep its reference visible
|
|
170
|
+
// instead of overwriting it with the outer reference during normalization.
|
|
171
|
+
if (hasSiblings || (isObject(target) && '$ref' in target)) {
|
|
172
|
+
return [
|
|
173
|
+
...(hasSiblings ? [paragraph(strong(text('All of:')))] : []),
|
|
174
|
+
...render(target, depth + 1, ancestors),
|
|
175
|
+
...(hasSiblings ? render(siblings, depth, ancestors) : []),
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
155
179
|
// Follow the original target: merging reference siblings creates fresh objects.
|
|
156
180
|
const identity = getResolvedRef(input) ?? input;
|
|
157
181
|
if (typeof identity === 'object' && ancestors.includes(identity)) {
|
|
@@ -206,6 +230,13 @@ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
|
206
230
|
if (annotations.length)
|
|
207
231
|
nodes.push(paragraph(...annotations));
|
|
208
232
|
}
|
|
233
|
+
if (linked) {
|
|
234
|
+
// A reference sibling may require a field declared only in the target schema.
|
|
235
|
+
const declared = new Set(value.properties.map(([name]) => name));
|
|
236
|
+
const required = [...value.required].filter((name) => !declared.has(name));
|
|
237
|
+
if (required.length)
|
|
238
|
+
nodes.push(paragraph(strong(text('Required fields:')), text(' '), inlineCode(required.join(', '))));
|
|
239
|
+
}
|
|
209
240
|
if (value.properties.length) {
|
|
210
241
|
const properties = value.properties.map(([name, schema]) => {
|
|
211
242
|
const child = view(schema);
|
|
@@ -244,6 +275,7 @@ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
|
244
275
|
return nodes;
|
|
245
276
|
};
|
|
246
277
|
return {
|
|
278
|
+
linked,
|
|
247
279
|
view,
|
|
248
280
|
render,
|
|
249
281
|
beginSection: () => {
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
/** Where a value sits in an OpenAPI description: a schema, a map of schemas, or anything else. */
|
|
2
|
+
export type SchemaPosition = 'document' | 'schema' | 'map';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the position of the value stored under `key`, or `undefined` when that value
|
|
5
|
+
* is not part of the schema tree, such as example payloads and schema metadata.
|
|
6
|
+
*/
|
|
7
|
+
export declare const getChildPosition: (position: SchemaPosition, key: string) => SchemaPosition | undefined;
|
|
1
8
|
/**
|
|
2
9
|
* The workspace schema currently casts boolean JSON Schemas into empty objects.
|
|
3
10
|
* Restore them only in schema positions, never in example payloads or metadata.
|
|
4
11
|
* TODO: Remove this bridge when the shared schema accepts boolean JSON Schemas.
|
|
5
12
|
*/
|
|
6
|
-
export declare const restoreBooleanSchemas: (source: unknown, target: unknown) => void;
|
|
13
|
+
export declare const restoreBooleanSchemas: (source: unknown, target: unknown, position?: SchemaPosition) => void;
|
|
7
14
|
//# sourceMappingURL=restore-boolean-schemas.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"restore-boolean-schemas.d.ts","sourceRoot":"","sources":["../src/restore-boolean-schemas.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"restore-boolean-schemas.d.ts","sourceRoot":"","sources":["../src/restore-boolean-schemas.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAA;AAmB1D;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAU,cAAc,EAAE,KAAK,MAAM,KAAG,cAAc,GAAG,SAWzF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,OAAO,EACf,QAAQ,OAAO,EACf,WAAU,cAA2B,KACpC,IA2BF,CAAA"}
|
|
@@ -1,46 +1,50 @@
|
|
|
1
1
|
import { isObject } from '@scalar/helpers/object/is-object';
|
|
2
|
+
const schemaMaps = new Set(['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']);
|
|
3
|
+
const schemaArrays = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems']);
|
|
4
|
+
const schemaFields = new Set([
|
|
5
|
+
'items',
|
|
6
|
+
'not',
|
|
7
|
+
'additionalProperties',
|
|
8
|
+
'additionalItems',
|
|
9
|
+
'contains',
|
|
10
|
+
'contentSchema',
|
|
11
|
+
'propertyNames',
|
|
12
|
+
'if',
|
|
13
|
+
'then',
|
|
14
|
+
'else',
|
|
15
|
+
'unevaluatedProperties',
|
|
16
|
+
'unevaluatedItems',
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Returns the position of the value stored under `key`, or `undefined` when that value
|
|
20
|
+
* is not part of the schema tree, such as example payloads and schema metadata.
|
|
21
|
+
*/
|
|
22
|
+
export const getChildPosition = (position, key) => {
|
|
23
|
+
if (position === 'map')
|
|
24
|
+
return 'schema';
|
|
25
|
+
if (position === 'schema') {
|
|
26
|
+
if (schemaMaps.has(key))
|
|
27
|
+
return 'map';
|
|
28
|
+
if (schemaArrays.has(key) || schemaFields.has(key))
|
|
29
|
+
return 'schema';
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (key === 'schema' || key === 'itemSchema')
|
|
33
|
+
return 'schema';
|
|
34
|
+
if (key === 'schemas')
|
|
35
|
+
return 'map';
|
|
36
|
+
if (['example', 'examples', 'default', 'const', 'enum'].includes(key))
|
|
37
|
+
return undefined;
|
|
38
|
+
return 'document';
|
|
39
|
+
};
|
|
2
40
|
/**
|
|
3
41
|
* The workspace schema currently casts boolean JSON Schemas into empty objects.
|
|
4
42
|
* Restore them only in schema positions, never in example payloads or metadata.
|
|
5
43
|
* TODO: Remove this bridge when the shared schema accepts boolean JSON Schemas.
|
|
6
44
|
*/
|
|
7
|
-
export const restoreBooleanSchemas = (source, target) => {
|
|
45
|
+
export const restoreBooleanSchemas = (source, target, position = 'document') => {
|
|
8
46
|
const seen = new WeakSet();
|
|
9
|
-
const
|
|
10
|
-
const schemaArrays = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems']);
|
|
11
|
-
const schemaFields = new Set([
|
|
12
|
-
'items',
|
|
13
|
-
'not',
|
|
14
|
-
'additionalProperties',
|
|
15
|
-
'additionalItems',
|
|
16
|
-
'contains',
|
|
17
|
-
'propertyNames',
|
|
18
|
-
'if',
|
|
19
|
-
'then',
|
|
20
|
-
'else',
|
|
21
|
-
'unevaluatedProperties',
|
|
22
|
-
'unevaluatedItems',
|
|
23
|
-
'$ref-value',
|
|
24
|
-
]);
|
|
25
|
-
const getChildContext = (context, key) => {
|
|
26
|
-
if (context === 'map')
|
|
27
|
-
return 'schema';
|
|
28
|
-
if (context === 'schema') {
|
|
29
|
-
if (schemaMaps.has(key))
|
|
30
|
-
return 'map';
|
|
31
|
-
if (schemaArrays.has(key) || schemaFields.has(key))
|
|
32
|
-
return 'schema';
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
if (key === 'schema' || key === 'itemSchema')
|
|
36
|
-
return 'schema';
|
|
37
|
-
if (key === 'schemas')
|
|
38
|
-
return 'map';
|
|
39
|
-
if (['example', 'examples', 'default', 'const', 'enum'].includes(key))
|
|
40
|
-
return undefined;
|
|
41
|
-
return 'document';
|
|
42
|
-
};
|
|
43
|
-
const visit = (original, coerced, context = 'document') => {
|
|
47
|
+
const visit = (original, coerced, context) => {
|
|
44
48
|
if (context === 'schema' && typeof original === 'boolean')
|
|
45
49
|
return original;
|
|
46
50
|
if (!original || typeof original !== 'object' || !coerced || typeof coerced !== 'object' || seen.has(coerced))
|
|
@@ -61,7 +65,7 @@ export const restoreBooleanSchemas = (source, target) => {
|
|
|
61
65
|
const property = Object.getOwnPropertyDescriptor(coerced, key);
|
|
62
66
|
if (!property || !('value' in property))
|
|
63
67
|
continue;
|
|
64
|
-
const childContext =
|
|
68
|
+
const childContext = getChildPosition(context, key);
|
|
65
69
|
if (childContext === undefined)
|
|
66
70
|
continue;
|
|
67
71
|
Object.defineProperty(coerced, key, { ...property, value: visit(value, property.value, childContext) });
|
|
@@ -69,5 +73,5 @@ export const restoreBooleanSchemas = (source, target) => {
|
|
|
69
73
|
}
|
|
70
74
|
return coerced;
|
|
71
75
|
};
|
|
72
|
-
visit(source, target);
|
|
76
|
+
visit(source, target, position);
|
|
73
77
|
};
|
|
@@ -10,9 +10,21 @@ export type OperationSelector = {
|
|
|
10
10
|
pointer: string;
|
|
11
11
|
};
|
|
12
12
|
/** Select one reference page, or omit selectors for the whole document. */
|
|
13
|
-
export type OpenApiRenderOptions = {
|
|
13
|
+
export type OpenApiRenderOptions = SchemaReferenceOptions & ({
|
|
14
14
|
[Key in keyof PageSelectors]: Partial<Record<Exclude<keyof PageSelectors, Key>, never>> & Pick<PageSelectors, Key>;
|
|
15
|
-
}[keyof PageSelectors] | Partial<Record<keyof PageSelectors, never
|
|
15
|
+
}[keyof PageSelectors] | Partial<Record<keyof PageSelectors, never>>);
|
|
16
|
+
/** Control shared-schema expansion without assuming a documentation URL layout. */
|
|
17
|
+
export type SchemaReferenceOptions = {
|
|
18
|
+
/** Expand root schemas, link nested references, and omit generated examples and the transitive appendix. */
|
|
19
|
+
schemaReferences?: {
|
|
20
|
+
mode: 'linked';
|
|
21
|
+
/** Return a published URL, or undefined to retain the schema name as plain text. */
|
|
22
|
+
resolveUrl?: (reference: {
|
|
23
|
+
ref: string;
|
|
24
|
+
name: string;
|
|
25
|
+
}) => string | undefined;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
16
28
|
type PageSelectors = {
|
|
17
29
|
operation: OperationSelector;
|
|
18
30
|
tag: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-document.d.ts","sourceRoot":"","sources":["../src/select-document.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAA;AACjF,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,8DAA8D,CAAA;AAEnH,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,eAAe,CAAA;CACxB,GACD;IACE,WAAW,EAAE,MAAM,CAAA;CACpB,GACD;IACE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AACL,2EAA2E;AAC3E,MAAM,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"select-document.d.ts","sourceRoot":"","sources":["../src/select-document.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAA;AACjF,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,8DAA8D,CAAA;AAEnH,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,eAAe,CAAA;CACxB,GACD;IACE,WAAW,EAAE,MAAM,CAAA;CACpB,GACD;IACE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AACL,2EAA2E;AAC3E,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GACvD,CACI;KACG,GAAG,IAAI,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,aAAa,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,GACrF,IAAI,CAAC,aAAa,EAAE,GAAG,CAAC;CAC3B,CAAC,MAAM,aAAa,CAAC,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,aAAa,EAAE,KAAK,CAAC,CAAC,CAC9C,CAAA;AAEH,mFAAmF;AACnF,MAAM,MAAM,sBAAsB,GAAG;IACnC,4GAA4G;IAC5G,gBAAgB,CAAC,EAAE;QACjB,IAAI,EAAE,QAAQ,CAAA;QACd,oFAAoF;QACpF,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,KAAK,MAAM,GAAG,SAAS,CAAA;KAC9E,CAAA;CACF,CAAA;AAED,KAAK,aAAa,GAAG;IACnB,SAAS,EAAE,iBAAiB,CAAA;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAA;IAClD,YAAY,EAAE,IAAI,CAAA;CACnB,CAAA;AAyKD,sEAAsE;AACtE,eAAO,MAAM,cAAc,GAAI,UAAU,eAAe,EAAE,UAAS,oBAAyB,KAAG,eA2N9F,CAAA"}
|
package/dist/select-document.js
CHANGED
|
@@ -126,7 +126,7 @@ export const selectDocument = (document, options = {}) => {
|
|
|
126
126
|
if (!isObject(options)) {
|
|
127
127
|
throw new Error('Render options must be an object');
|
|
128
128
|
}
|
|
129
|
-
const keys = Object.keys(options).filter((key) => options[key] !== undefined);
|
|
129
|
+
const keys = Object.keys(options).filter((key) => key !== 'schemaReferences' && options[key] !== undefined);
|
|
130
130
|
if (!keys.length) {
|
|
131
131
|
return document;
|
|
132
132
|
}
|
|
@@ -320,9 +320,11 @@ export const selectDocument = (document, options = {}) => {
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
};
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
323
|
+
if (options.schemaReferences?.mode !== 'linked') {
|
|
324
|
+
visit({ paths: selected.paths, webhooks: selected.webhooks });
|
|
325
|
+
for (const root of modelRoots) {
|
|
326
|
+
visit(root);
|
|
327
|
+
}
|
|
326
328
|
}
|
|
327
329
|
selected.components = {
|
|
328
330
|
...document.components,
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"llm",
|
|
17
17
|
"swagger"
|
|
18
18
|
],
|
|
19
|
-
"version": "1.
|
|
19
|
+
"version": "1.4.0",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@scalar/code-highlight": "0.4.7",
|
|
44
|
-
"@scalar/helpers": "0.
|
|
45
|
-
"@scalar/json-magic": "0.15.
|
|
46
|
-
"@scalar/openapi-upgrader": "0.
|
|
47
|
-
"@scalar/workspace-store": "0.
|
|
44
|
+
"@scalar/helpers": "0.16.0",
|
|
45
|
+
"@scalar/json-magic": "0.15.3",
|
|
46
|
+
"@scalar/openapi-upgrader": "0.4.1",
|
|
47
|
+
"@scalar/workspace-store": "0.67.1",
|
|
48
48
|
"rehype-parse": "^9.0.1",
|
|
49
49
|
"rehype-remark": "^10.0.1",
|
|
50
50
|
"rehype-sanitize": "^6.0.0",
|