@scalar/openapi-to-markdown 1.0.2 → 1.2.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +20 -4
  3. package/dist/browser.d.ts +9 -0
  4. package/dist/browser.d.ts.map +1 -0
  5. package/dist/browser.js +8 -0
  6. package/dist/create-markdown-from-openapi.d.ts +0 -3
  7. package/dist/create-markdown-from-openapi.d.ts.map +1 -1
  8. package/dist/create-markdown-from-openapi.js +1 -18
  9. package/dist/get-markdown-examples.d.ts +29 -0
  10. package/dist/get-markdown-examples.d.ts.map +1 -0
  11. package/dist/get-markdown-examples.js +105 -0
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/load-document.d.ts.map +1 -1
  16. package/dist/load-document.js +8 -1
  17. package/dist/render-document.d.ts.map +1 -1
  18. package/dist/render-document.js +8 -3
  19. package/dist/render-examples.d.ts +6 -0
  20. package/dist/render-examples.d.ts.map +1 -0
  21. package/dist/render-examples.js +41 -0
  22. package/dist/render-operation-details.d.ts +11 -0
  23. package/dist/render-operation-details.d.ts.map +1 -0
  24. package/dist/render-operation-details.js +85 -0
  25. package/dist/render-operation.d.ts.map +1 -1
  26. package/dist/render-operation.js +26 -15
  27. package/dist/render-schema.d.ts +55 -13
  28. package/dist/render-schema.d.ts.map +1 -1
  29. package/dist/render-schema.js +227 -69
  30. package/dist/restore-boolean-schemas.d.ts +7 -0
  31. package/dist/restore-boolean-schemas.d.ts.map +1 -0
  32. package/dist/restore-boolean-schemas.js +73 -0
  33. package/dist/select-document.d.ts +3 -3
  34. package/dist/select-document.d.ts.map +1 -1
  35. package/dist/select-document.js +57 -36
  36. package/package.json +13 -10
@@ -1,15 +1,19 @@
1
+ import { isHttpMethod } from '@scalar/helpers/http/is-http-method';
1
2
  import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
3
  import { field, heading, inlineCode, item, list, paragraph, strong, text } from './markdown-nodes.js';
4
+ import { renderExamples } from './render-examples.js';
5
+ import { renderEncoding, renderHeaders, renderResponseLinks } from './render-operation-details.js';
3
6
  import { renderSecurity } from './render-security.js';
4
7
  /** Render effective operation context without mutating the prepared document. */
5
8
  export const renderOperation = async (document, path, method, pathItem, operation, webhook, { description, schemas }) => {
9
+ const displayMethod = method === method.toLowerCase() && isHttpMethod(method) ? method.toUpperCase() : method;
10
+ const openapiVersion = document['x-original-oas-version'] ?? document.openapi;
6
11
  const stability = operation['x-scalar-stability'];
7
- const title = (operation.summary || `${method.toUpperCase()} ${path}`) +
12
+ const title = (operation.summary || `${displayMethod} ${path}`) +
8
13
  (stability ? ` (${stability})` : operation.deprecated ? ' ⚠️ Deprecated' : '');
9
- const metadata = [
10
- field('Method', inlineCode(method.toUpperCase())),
11
- field(webhook ? 'Webhook' : 'Path', inlineCode(path)),
12
- ];
14
+ const metadata = [field('Method', inlineCode(displayMethod)), field(webhook ? 'Webhook' : 'Path', inlineCode(path))];
15
+ if (operation.operationId)
16
+ metadata.push(field('Operation ID', inlineCode(operation.operationId)));
13
17
  if (operation.tags)
14
18
  metadata.push(field('Tags', text(operation.tags.join(', '))));
15
19
  if (stability)
@@ -52,23 +56,28 @@ export const renderOperation = async (document, path, method, pathItem, operatio
52
56
  if ('allowReserved' in parameter && parameter.allowReserved)
53
57
  fields.push(field('Allow Reserved', text('true')));
54
58
  nodes.push(list(fields), ...(await description(parameter.description)));
55
- if ('schema' in parameter && parameter.schema)
59
+ if ('schema' in parameter && parameter.schema !== undefined)
56
60
  nodes.push(...schemas.render(parameter.schema));
61
+ if ('example' in parameter || 'examples' in parameter)
62
+ nodes.push(...(await renderExamples({ example: parameter.example, examples: parameter.examples }, description, 'application/json', 'write', openapiVersion)));
57
63
  for (const [mediaType, content] of Object.entries('content' in parameter ? (parameter.content ?? {}) : {})) {
58
64
  nodes.push(heading(6, text(`Content-Type: ${mediaType}`)));
59
- if (content.schema)
65
+ if (content.schema !== undefined)
60
66
  nodes.push(...schemas.render(content.schema));
67
+ nodes.push(...(await renderExamples(content, description, mediaType, 'write', openapiVersion)));
61
68
  }
62
69
  }
63
70
  const body = getResolvedRef(operation.requestBody, mergeSiblingReferences);
64
- if (body?.content) {
71
+ if (body) {
65
72
  nodes.push(heading(4, text('Request Body')), ...(await description(body.description)));
66
- if (body.required)
67
- nodes.push(paragraph(strong(text('Required:')), text(' true')));
68
- for (const [mediaType, content] of Object.entries(body.content)) {
73
+ if (typeof body.required === 'boolean')
74
+ nodes.push(paragraph(strong(text('Required:')), text(' '), inlineCode(body.required)));
75
+ for (const [mediaType, content] of Object.entries(body.content ?? {})) {
69
76
  nodes.push(heading(5, text(`Content-Type: ${mediaType}`)));
70
- if (content.schema)
71
- nodes.push(...schemas.render(content.schema), paragraph(strong(text('Example:'))), schemas.example(content.schema, mediaType.includes('xml')));
77
+ if (content.schema !== undefined)
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)));
72
81
  }
73
82
  }
74
83
  const responses = Object.entries(operation.responses ?? {}).flatMap(([status, reference]) => {
@@ -79,10 +88,12 @@ export const renderOperation = async (document, path, method, pathItem, operatio
79
88
  nodes.push(heading(4, text('Responses')));
80
89
  for (const { status, response } of responses) {
81
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)));
82
92
  for (const [mediaType, content] of Object.entries(response.content ?? {})) {
83
93
  nodes.push(heading(6, text(`Content-Type: ${mediaType}`)));
84
- if (content.schema)
85
- nodes.push(...schemas.render(content.schema), paragraph(strong(text('Example:'))), schemas.example(content.schema, mediaType.includes('xml')));
94
+ if (content.schema !== undefined)
95
+ nodes.push(...schemas.render(content.schema));
96
+ nodes.push(...(await renderExamples(content, description, mediaType, 'read', openapiVersion)));
86
97
  }
87
98
  }
88
99
  return nodes;
@@ -1,31 +1,73 @@
1
1
  import type { MaybeRefSchemaObject, SchemaObject } from '@scalar/workspace-store/schemas/v3.2/strict/schema';
2
- import type { Code, RootContent } from 'mdast';
2
+ import type { RootContent } from 'mdast';
3
+ /** Boolean schemas must survive rendering without being coerced into empty objects. */
4
+ type MarkdownSchema = MaybeRefSchemaObject | boolean;
3
5
  type SchemaView = {
4
- schema: SchemaObject;
6
+ schema: SchemaObject | boolean;
5
7
  title?: string;
6
8
  description?: string;
7
9
  type?: string | string[];
8
10
  format?: string;
9
11
  enum?: unknown[];
12
+ const?: unknown;
10
13
  default?: unknown;
11
- allOf?: MaybeRefSchemaObject[];
12
- anyOf?: MaybeRefSchemaObject[];
13
- oneOf?: MaybeRefSchemaObject[];
14
- not?: MaybeRefSchemaObject;
15
- properties: [string, MaybeRefSchemaObject][];
14
+ readOnly?: boolean;
15
+ writeOnly?: boolean;
16
+ allOf?: MarkdownSchema[];
17
+ anyOf?: MarkdownSchema[];
18
+ oneOf?: MarkdownSchema[];
19
+ not?: MarkdownSchema;
20
+ properties: [string, MarkdownSchema][];
16
21
  required: ReadonlySet<string>;
17
- items?: MaybeRefSchemaObject;
22
+ items?: MarkdownSchema;
18
23
  minItems?: number;
19
24
  maxItems?: number;
20
25
  uniqueItems?: boolean;
26
+ minimum?: number;
27
+ maximum?: number;
28
+ exclusiveMinimum?: number;
29
+ exclusiveMaximum?: number;
30
+ multipleOf?: number;
31
+ minLength?: number;
32
+ maxLength?: number;
33
+ pattern?: string;
34
+ minProperties?: number;
35
+ maxProperties?: number;
36
+ additionalProperties?: MarkdownSchema;
37
+ discriminator?: {
38
+ propertyName: string;
39
+ mapping?: Record<string, string>;
40
+ };
41
+ /** Set for references to a structured schema that is rendered once per document and referred to afterwards. */
42
+ name?: string;
21
43
  };
22
- /** Schema normalization is cached separately from ancestry-dependent expansion. */
44
+ type RenderOptions = {
45
+ hideDescription?: boolean;
46
+ hideDetails?: boolean;
47
+ property?: boolean;
48
+ /** Record this root under the given name, for example in its own model section, even if it is not a reference. */
49
+ name?: string;
50
+ };
51
+ /**
52
+ * Schema normalization is cached separately from ancestry-dependent expansion.
53
+ *
54
+ * A renderer also tracks which shared schemas one output document already shows, so each one is
55
+ * expanded once and referred to afterwards. Use `forDocument` for every document: renders can
56
+ * run concurrently, and each needs its own record.
57
+ */
23
58
  export type SchemaRenderer = {
24
- view: (schema: MaybeRefSchemaObject) => SchemaView;
25
- render: (schema: MaybeRefSchemaObject, depth?: number, ancestors?: readonly unknown[]) => RootContent[];
26
- example: (schema: MaybeRefSchemaObject, xml?: boolean) => Code;
59
+ view: (schema: MarkdownSchema) => SchemaView;
60
+ render: (schema: MarkdownSchema, depth?: number, ancestors?: readonly unknown[], options?: RenderOptions) => RootContent[];
61
+ /** Restore the node budget, for example for each operation or model section. */
62
+ beginSection: () => void;
63
+ /** Start a document that shares normalized views, given the models it renders in its own sections. */
64
+ forDocument: (models?: Record<string, MarkdownSchema>) => SchemaRenderer;
65
+ };
66
+ type SchemaRendererOptions = {
67
+ /** Upper bound on expanded schema nodes per section, so no description can make a page unbounded. */
68
+ maxNodes?: number;
27
69
  };
28
70
  /** Keep merged reference siblings and sorted properties stable throughout an export. */
29
- export declare const createSchemaRenderer: () => SchemaRenderer;
71
+ export declare const createSchemaRenderer: ({ maxNodes }?: SchemaRendererOptions) => SchemaRenderer;
30
72
  export {};
31
73
  //# sourceMappingURL=render-schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render-schema.d.ts","sourceRoot":"","sources":["../src/render-schema.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,oDAAoD,CAAA;AAC5G,OAAO,KAAK,EAAE,IAAI,EAA6B,WAAW,EAAE,MAAM,OAAO,CAAA;AAIzE,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,YAAY,CAAA;IACpB,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,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,oBAAoB,EAAE,CAAA;IAC9B,KAAK,CAAC,EAAE,oBAAoB,EAAE,CAAA;IAC9B,KAAK,CAAC,EAAE,oBAAoB,EAAE,CAAA;IAC9B,GAAG,CAAC,EAAE,oBAAoB,CAAA;IAC1B,UAAU,EAAE,CAAC,MAAM,EAAE,oBAAoB,CAAC,EAAE,CAAA;IAC5C,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC7B,KAAK,CAAC,EAAE,oBAAoB,CAAA;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AAED,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,UAAU,CAAA;IAClD,MAAM,EAAE,CAAC,MAAM,EAAE,oBAAoB,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,SAAS,OAAO,EAAE,KAAK,WAAW,EAAE,CAAA;IACvG,OAAO,EAAE,CAAC,MAAM,EAAE,oBAAoB,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAC/D,CAAA;AAED,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,QAAO,cA0FvC,CAAA"}
1
+ {"version":3,"file":"render-schema.d.ts","sourceRoot":"","sources":["../src/render-schema.ts"],"names":[],"mappings":"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"}
@@ -1,98 +1,256 @@
1
- import { json2xml } from '@scalar/helpers/file/json2xml';
2
- import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
3
- import { resolve } from '@scalar/workspace-store/resolve';
1
+ import { unescapeJsonPointer } from '@scalar/helpers/json/unescape-json-pointer';
2
+ import { isObject } from '@scalar/helpers/object/is-object';
3
+ import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
4
4
  import { inlineCode, item, list, paragraph, strong, text } from './markdown-nodes.js';
5
+ /** Guards against stack exhaustion. Past it, references to models point to their own section instead. */
6
+ const MAX_DEPTH = 64;
7
+ /**
8
+ * Deduplicated output is proportional to the schema graph, so only a pathological description
9
+ * (for example a YAML alias graph without references) reaches this.
10
+ */
11
+ const MAX_NODES = 100_000;
12
+ /** Keywords that `render` expands, as opposed to annotations that `details` prints on one line. */
13
+ const structuralKeywords = new Set([
14
+ 'allOf',
15
+ 'anyOf',
16
+ 'oneOf',
17
+ 'not',
18
+ 'properties',
19
+ 'required',
20
+ 'items',
21
+ 'additionalProperties',
22
+ 'discriminator',
23
+ 'minItems',
24
+ 'maxItems',
25
+ 'uniqueItems',
26
+ ]);
27
+ /** Link bookkeeping that is not a sibling keyword of a reference. */
28
+ const referenceKeys = new Set(['$ref', '$ref-value', '$global', '$status']);
29
+ const emphasis = (...children) => ({ type: 'emphasis', children });
30
+ /** Prefer the component name, which is how model sections and other references identify the schema. */
31
+ const getReferenceName = (ref) => {
32
+ const match = /^#\/components\/schemas\/([^/]+)$/.exec(ref);
33
+ if (!match)
34
+ return ref;
35
+ try {
36
+ return unescapeJsonPointer(match[1]);
37
+ }
38
+ catch {
39
+ return ref;
40
+ }
41
+ };
42
+ /** Only references that add nothing structural can be replaced by the schema they point to. */
43
+ const getSharedName = (input, view) => {
44
+ const ref = isObject(input) ? input.$ref : undefined;
45
+ if (typeof ref !== 'string' || typeof view.schema !== 'object')
46
+ return undefined;
47
+ if (Object.keys(input).some((key) => structuralKeywords.has(key)))
48
+ return undefined;
49
+ const structured = Boolean(view.allOf?.length || view.anyOf?.length || view.oneOf?.length || view.properties.length) ||
50
+ view.not !== undefined ||
51
+ view.items !== undefined ||
52
+ view.additionalProperties !== undefined;
53
+ // Leaf schemas are as short as a reference to them, so they stay in place.
54
+ return structured ? getReferenceName(ref) : undefined;
55
+ };
56
+ /** Boolean targets still combine with adjacent schema keywords. */
57
+ const resolveMarkdownSchema = (input) => {
58
+ const target = getResolvedRef(input);
59
+ const merged = getResolvedRef(input, mergeSiblingReferences);
60
+ if (typeof target !== 'boolean')
61
+ return merged;
62
+ if (!isObject(merged) ||
63
+ !Object.keys(merged).some((key) => !['$ref', '$ref-value', '$global', '$status'].includes(key)))
64
+ return target;
65
+ if (target)
66
+ return merged;
67
+ // A false target remains impossible, even when siblings describe a type or annotations.
68
+ return { ...merged, allOf: [false, ...(Array.isArray(merged.allOf) ? merged.allOf : [])] };
69
+ };
5
70
  /** Keep merged reference siblings and sorted properties stable throughout an export. */
6
- export const createSchemaRenderer = () => {
71
+ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
7
72
  const views = new WeakMap();
8
73
  const view = (input) => {
9
- const cached = views.get(input);
74
+ const cached = typeof input === 'object' ? views.get(input) : undefined;
10
75
  if (cached)
11
76
  return cached;
12
- const schema = resolve.schema(input);
13
- const value = schema;
77
+ // Keep the linked document's nested identities and boolean schemas. Coercing a
78
+ // second time here would discard boolean children and copy recursive targets.
79
+ const schema = resolveMarkdownSchema(input);
80
+ const value = (typeof schema === 'object' ? schema : {});
14
81
  const required = new Set(value.required ?? []);
15
82
  const properties = Object.entries(value.properties ?? {})
16
- .filter((entry) => entry[1] && typeof entry[1] === 'object')
83
+ .filter(([, child]) => typeof child === 'boolean' || (child !== null && typeof child === 'object'))
17
84
  .sort(([a], [b]) => Number(required.has(b)) - Number(required.has(a)) || a.localeCompare(b));
18
- const result = { ...value, schema, required, properties };
19
- views.set(input, result);
85
+ const result = {
86
+ ...value,
87
+ type: typeof schema === 'boolean' ? (schema ? 'any' : 'never') : value.type,
88
+ schema: schema,
89
+ required,
90
+ properties,
91
+ };
92
+ result.name = getSharedName(input, result);
93
+ if (typeof input === 'object')
94
+ views.set(input, result);
20
95
  return result;
21
96
  };
22
- const details = (value, property = false) => {
97
+ const details = (value, property = false, hideDescription = false, showType = true) => {
98
+ if (typeof value.schema === 'boolean')
99
+ return [text(value.schema ? 'any (true schema)' : 'never (false schema)')];
23
100
  const type = Array.isArray(value.type) ? value.type.join(' | ') : value.type;
24
- const nodes = type || property ? [inlineCode(type || 'object')] : [];
25
- if (value.format)
26
- nodes.push(text(', format: '), inlineCode(value.format));
101
+ const nodes = showType && (type || property) ? [inlineCode(type || 'object')] : [];
102
+ const add = (label, entry) => {
103
+ if (entry !== undefined)
104
+ nodes.push(text(`${nodes.length ? ', ' : ''}${label}: `), inlineCode(entry));
105
+ };
106
+ // Later references to a shared schema point back to this name.
107
+ add('schema', value.name);
108
+ add('format', value.format);
27
109
  if (value.enum)
28
- nodes.push(text(', possible values: '), inlineCode(value.enum.map((entry) => JSON.stringify(entry)).join(', ')));
110
+ add('possible values', value.enum.map((entry) => JSON.stringify(entry)).join(', '));
111
+ if (value.const !== undefined)
112
+ add('const', JSON.stringify(value.const));
29
113
  if (value.default !== undefined)
30
- nodes.push(text(', default: '), inlineCode(JSON.stringify(value.default)));
31
- if (value.description)
32
- nodes.push(text(` — ${value.description}`));
33
- return nodes;
34
- };
35
- const render = (input, depth = 0, ancestors = []) => {
36
- // The previous renderer expanded a resolved root before tracking child reference strings.
37
- const identity = depth > 0 && '$ref' in input ? input.$ref : input;
38
- if (depth >= 10 || ancestors.includes(identity)) {
39
- return [paragraph({ type: 'emphasis', children: [text('[Circular Reference]')] })];
40
- }
41
- const value = view(input);
42
- const childAncestors = [...ancestors, identity];
43
- for (const [key, label] of [
44
- ['allOf', 'All of:'],
45
- ['anyOf', 'Any of:'],
46
- ['oneOf', 'One of:'],
47
- ]) {
114
+ add('default', JSON.stringify(value.default));
115
+ for (const key of [
116
+ 'minimum',
117
+ 'maximum',
118
+ 'exclusiveMinimum',
119
+ 'exclusiveMaximum',
120
+ 'multipleOf',
121
+ 'minLength',
122
+ 'maxLength',
123
+ 'pattern',
124
+ 'minProperties',
125
+ 'maxProperties',
126
+ ])
127
+ add(key, value[key]);
128
+ for (const key of ['readOnly', 'writeOnly']) {
48
129
  if (value[key])
49
- return [
50
- paragraph(strong(text(label))),
51
- ...value[key].flatMap((child) => render(child, depth + 1, childAncestors)),
52
- ];
130
+ nodes.push(text(`${nodes.length ? ', ' : ''}${key}`));
53
131
  }
54
- if (value.not)
55
- return [paragraph(strong(text('Not:'))), ...render(value.not, depth + 1, childAncestors)];
56
- if (value.type === 'object' || value.properties.length) {
57
- const properties = value.properties.map(([name, schema]) => {
58
- const child = view(schema);
59
- const label = [inlineCode(name)];
60
- if (value.required.has(name))
61
- label.push(text(' (required)'));
62
- const blocks = [paragraph(strong(...label)), paragraph(...details(child, true))];
63
- if (child.type === 'object' || child.properties.length) {
64
- blocks.push(...render(schema, depth + 1, childAncestors));
65
- }
66
- if (child.type === 'array' && child.items) {
67
- blocks.push(paragraph(strong(text('Items:'))), ...render(child.items, depth + 1, childAncestors));
132
+ if (!hideDescription && value.description)
133
+ nodes.push(text(`${nodes.length ? ' — ' : ''}${value.description}`));
134
+ return nodes;
135
+ };
136
+ const forDocument = (models = {}) => {
137
+ /** Shared schemas this document already expanded, with the name later references use. */
138
+ const shown = new Map();
139
+ /** Models that the document renders in their own sections after the operations. */
140
+ const sections = new Set(Object.values(models).map((model) => getResolvedRef(model) ?? model));
141
+ let nodeCount = 0;
142
+ /** Refer to a schema expanded elsewhere, keeping annotations the reference itself adds. */
143
+ const reference = (input, value, options, name, location) => {
144
+ const nodes = [];
145
+ const siblings = isObject(input) && Object.keys(input).some((key) => !referenceKeys.has(key));
146
+ if (siblings && !options.hideDetails) {
147
+ const annotations = details({ ...value, name: undefined }, false, options.hideDescription);
148
+ if (annotations.length)
149
+ nodes.push(paragraph(...annotations));
150
+ }
151
+ nodes.push(paragraph(emphasis(text('Schema '), inlineCode(name), text(` is shown ${location}.`))));
152
+ return nodes;
153
+ };
154
+ const render = (input, depth = 0, ancestors = [], options = {}) => {
155
+ // Follow the original target: merging reference siblings creates fresh objects.
156
+ const identity = getResolvedRef(input) ?? input;
157
+ if (typeof identity === 'object' && ancestors.includes(identity)) {
158
+ return [paragraph(emphasis(text('[Circular Reference]')))];
159
+ }
160
+ const value = view(input);
161
+ // A named model with structural reference siblings is its own schema, not another
162
+ // occurrence of its target. Otherwise either rendering order can hide properties.
163
+ const sharedIdentity = isObject(input) && '$ref' in input && Object.keys(input).some((key) => structuralKeywords.has(key))
164
+ ? input
165
+ : identity;
166
+ const shared = typeof sharedIdentity === 'object' && sharedIdentity !== null ? sharedIdentity : undefined;
167
+ const name = options.name ?? value.name;
168
+ if (shared && name !== undefined) {
169
+ // Expanding every path through a shared schema grows exponentially, so expand it once.
170
+ const previous = shown.get(shared);
171
+ if (previous !== undefined) {
172
+ // A model section already prints its own annotations above the schema.
173
+ const referenceOptions = options.name === undefined ? options : { ...options, hideDetails: true };
174
+ return reference(input, value, referenceOptions, previous, 'above');
68
175
  }
69
- return item(...blocks);
70
- });
71
- return properties.length ? [list(properties)] : [];
72
- }
73
- if (value.type === 'array' && value.items) {
74
- const nodes = [paragraph(strong(text('Array of:'))), ...render(value.items, depth + 1, childAncestors)];
176
+ if (value.name !== undefined && options.name === undefined && depth >= MAX_DEPTH && sections.has(shared))
177
+ return reference(input, value, options, value.name, 'below under Schemas');
178
+ }
179
+ if (depth >= MAX_DEPTH)
180
+ return [paragraph(text('[Maximum schema depth reached]'))];
181
+ if (nodeCount >= maxNodes)
182
+ return [paragraph(emphasis(text('[Schema output truncated]')))];
183
+ nodeCount++;
184
+ if (shared && name !== undefined && !shown.has(shared))
185
+ shown.set(shared, name);
186
+ if (typeof value.schema === 'boolean')
187
+ return options.hideDetails ? [] : [paragraph(...details(value))];
188
+ const childAncestors = [...ancestors, identity];
189
+ const nodes = [];
190
+ for (const [key, label] of [
191
+ ['allOf', 'All of:'],
192
+ ['anyOf', 'Any of:'],
193
+ ['oneOf', 'One of:'],
194
+ ]) {
195
+ if (value[key]?.length)
196
+ nodes.push(paragraph(strong(text(label))), ...value[key].flatMap((child) => render(child, depth + 1, childAncestors)));
197
+ }
198
+ if (value.not !== undefined)
199
+ nodes.push(paragraph(strong(text('Not:'))), ...render(value.not, depth + 1, childAncestors));
200
+ const array = value.type === 'array' || value.items !== undefined;
201
+ if (!options.hideDetails) {
202
+ // Child sections imply a single container type, but never its nullable alternatives.
203
+ const impliedType = (value.type === 'object' && value.properties.length > 0) ||
204
+ (value.type === 'array' && value.items !== undefined);
205
+ const annotations = details(value, false, options.hideDescription, !impliedType);
206
+ if (annotations.length)
207
+ nodes.push(paragraph(...annotations));
208
+ }
209
+ if (value.properties.length) {
210
+ const properties = value.properties.map(([name, schema]) => {
211
+ const child = view(schema);
212
+ const label = [inlineCode(name)];
213
+ if (value.required.has(name))
214
+ label.push(text(' (required)'));
215
+ const blocks = [paragraph(strong(...label)), paragraph(...details(child, true))];
216
+ blocks.push(...render(schema, depth + 1, childAncestors, {
217
+ hideDetails: true,
218
+ property: true,
219
+ }));
220
+ return item(...blocks);
221
+ });
222
+ nodes.push(list(properties));
223
+ }
224
+ if (array && value.items !== undefined) {
225
+ nodes.push(paragraph(strong(text(options.property ? 'Items:' : 'Array of:'))), ...render(value.items, depth + 1, childAncestors));
226
+ }
75
227
  const constraints = [];
76
228
  if (value.minItems !== undefined)
77
229
  constraints.push(item(paragraph(text('Min items: '), inlineCode(value.minItems))));
78
230
  if (value.maxItems !== undefined)
79
231
  constraints.push(item(paragraph(text('Max items: '), inlineCode(value.maxItems))));
80
- if (value.uniqueItems)
81
- constraints.push(item(paragraph(text('Unique items: '), inlineCode(true))));
232
+ if (value.uniqueItems !== undefined)
233
+ constraints.push(item(paragraph(text('Unique items: '), inlineCode(value.uniqueItems))));
82
234
  if (constraints.length)
83
235
  nodes.push(list(constraints));
236
+ if (value.additionalProperties !== undefined)
237
+ nodes.push(paragraph(strong(text('Additional properties:'))), ...render(value.additionalProperties, depth + 1, childAncestors));
238
+ if (value.discriminator) {
239
+ nodes.push(paragraph(strong(text('Discriminator:')), text(' '), inlineCode(value.discriminator.propertyName)));
240
+ const mappings = Object.entries(value.discriminator.mapping ?? {});
241
+ if (mappings.length)
242
+ nodes.push(list(mappings.map(([name, target]) => item(paragraph(inlineCode(name), text(': '), inlineCode(target))))));
243
+ }
84
244
  return nodes;
85
- }
86
- const nodes = details(value);
87
- return nodes.length ? [paragraph(...nodes)] : [];
88
- };
89
- const example = (input, xml = false) => {
90
- const value = getExampleFromSchema(view(input).schema, { xml });
245
+ };
91
246
  return {
92
- type: 'code',
93
- lang: xml ? 'xml' : 'json',
94
- value: xml ? json2xml(value) : (JSON.stringify(value, null, 2) ?? ''),
247
+ view,
248
+ render,
249
+ beginSection: () => {
250
+ nodeCount = 0;
251
+ },
252
+ forDocument,
95
253
  };
96
254
  };
97
- return { view, render, example };
255
+ return forDocument();
98
256
  };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The workspace schema currently casts boolean JSON Schemas into empty objects.
3
+ * Restore them only in schema positions, never in example payloads or metadata.
4
+ * TODO: Remove this bridge when the shared schema accepts boolean JSON Schemas.
5
+ */
6
+ export declare const restoreBooleanSchemas: (source: unknown, target: unknown) => void;
7
+ //# sourceMappingURL=restore-boolean-schemas.d.ts.map
@@ -0,0 +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,GAAI,QAAQ,OAAO,EAAE,QAAQ,OAAO,KAAG,IA0DxE,CAAA"}
@@ -0,0 +1,73 @@
1
+ import { isObject } from '@scalar/helpers/object/is-object';
2
+ /**
3
+ * The workspace schema currently casts boolean JSON Schemas into empty objects.
4
+ * Restore them only in schema positions, never in example payloads or metadata.
5
+ * TODO: Remove this bridge when the shared schema accepts boolean JSON Schemas.
6
+ */
7
+ export const restoreBooleanSchemas = (source, target) => {
8
+ const seen = new WeakSet();
9
+ const schemaMaps = new Set(['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']);
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') => {
44
+ if (context === 'schema' && typeof original === 'boolean')
45
+ return original;
46
+ if (!original || typeof original !== 'object' || !coerced || typeof coerced !== 'object' || seen.has(coerced))
47
+ return coerced;
48
+ seen.add(coerced);
49
+ if (Array.isArray(original) && Array.isArray(coerced)) {
50
+ for (const [index, value] of original.entries()) {
51
+ const property = Object.getOwnPropertyDescriptor(coerced, index);
52
+ if (!property || !('value' in property))
53
+ continue;
54
+ Object.defineProperty(coerced, index, { ...property, value: visit(value, property.value, context) });
55
+ }
56
+ }
57
+ else if (isObject(original) && isObject(coerced)) {
58
+ for (const [key, value] of Object.entries(original)) {
59
+ // Never follow inherited values or invoke setters, including __proto__.
60
+ // Defining an own data property still preserves those names in schema maps.
61
+ const property = Object.getOwnPropertyDescriptor(coerced, key);
62
+ if (!property || !('value' in property))
63
+ continue;
64
+ const childContext = getChildContext(context, key);
65
+ if (childContext === undefined)
66
+ continue;
67
+ Object.defineProperty(coerced, key, { ...property, value: visit(value, property.value, childContext) });
68
+ }
69
+ }
70
+ return coerced;
71
+ };
72
+ visit(source, target);
73
+ };
@@ -1,9 +1,9 @@
1
- import type { HttpMethod } from '@scalar/helpers/http/http-methods';
1
+ import type { OperationMethod } from '@scalar/workspace-store/schemas/navigation';
2
2
  import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
3
3
  /** Identify one operation by path and method, operation ID, or JSON pointer. */
4
4
  export type OperationSelector = {
5
5
  path: string;
6
- method: HttpMethod | Uppercase<HttpMethod>;
6
+ method: OperationMethod;
7
7
  } | {
8
8
  operationId: string;
9
9
  } | {
@@ -19,7 +19,7 @@ type PageSelectors = {
19
19
  model: string;
20
20
  webhook: {
21
21
  name: string;
22
- method: HttpMethod | Uppercase<HttpMethod>;
22
+ method: OperationMethod;
23
23
  };
24
24
  introduction: true;
25
25
  };
@@ -1 +1 @@
1
- {"version":3,"file":"select-document.d.ts","sourceRoot":"","sources":["../src/select-document.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAA;AAInE,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,8DAA8D,CAAA;AAEnH,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;CAC3C,GACD;IACE,WAAW,EAAE,MAAM,CAAA;CACpB,GACD;IACE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AACL,2EAA2E;AAC3E,MAAM,MAAM,oBAAoB,GAC5B;KACG,GAAG,IAAI,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,aAAa,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,aAAa,EAAE,GAAG,CAAC;CACnH,CAAC,MAAM,aAAa,CAAC,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,aAAa,EAAE,KAAK,CAAC,CAAC,CAAA;AAE/C,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,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;KAAE,CAAA;IACrE,YAAY,EAAE,IAAI,CAAA;CACnB,CAAA;AAmKD,sEAAsE;AACtE,eAAO,MAAM,cAAc,GAAI,UAAU,eAAe,EAAE,UAAS,oBAAyB,KAAG,eA+M9F,CAAA"}
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,GAC5B;KACG,GAAG,IAAI,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,aAAa,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,aAAa,EAAE,GAAG,CAAC;CACnH,CAAC,MAAM,aAAa,CAAC,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,aAAa,EAAE,KAAK,CAAC,CAAC,CAAA;AAE/C,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,eAuN9F,CAAA"}