@scalar/openapi-to-markdown 1.5.4 → 1.6.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 +22 -0
- package/README.md +27 -1
- package/dist/create-markdown-from-openapi.d.ts +5 -2
- package/dist/create-markdown-from-openapi.d.ts.map +1 -1
- package/dist/create-markdown-from-openapi.js +7 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/load-document.d.ts +8 -1
- package/dist/load-document.d.ts.map +1 -1
- package/dist/load-document.js +18 -13
- package/dist/markdown/index.d.ts +5 -0
- package/dist/markdown/index.d.ts.map +1 -0
- package/dist/markdown/index.js +4 -0
- package/dist/markdown-nodes.d.ts.map +1 -1
- package/dist/markdown-nodes.js +1 -0
- package/dist/render-document.d.ts.map +1 -1
- package/dist/render-document.js +2 -2
- package/dist/render-tags.d.ts +7 -0
- package/dist/render-tags.d.ts.map +1 -0
- package/dist/render-tags.js +73 -0
- package/package.json +14 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @scalar/openapi-to-markdown
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#10586](https://github.com/scalar/scalar/pull/10586): Add AsyncAPI Markdown conversion with reusable renderers and individual reference pages. Share Markdown description parsing between the OpenAPI and AsyncAPI converters.
|
|
8
|
+
|
|
9
|
+
Include shared model links, contents navigation, tag pages, generated examples, and an interactive playground.
|
|
10
|
+
|
|
11
|
+
Preserve discriminators and linked schema descriptions, generate examples from primitive aliases, and compact property annotations.
|
|
12
|
+
|
|
13
|
+
Include full-export tag documentation, preserve XML schema examples, and normalize annotated primitive aliases.
|
|
14
|
+
|
|
15
|
+
Infer enum and const type labels and honor primitive-link settings in generated model examples.
|
|
16
|
+
|
|
17
|
+
Preserve AsyncAPI application and schema metadata, explain security alternatives, and render reply and unknown-address semantics accurately.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#10606](https://github.com/scalar/scalar/pull/10606): Block private-network document requests by default and require an explicit basePath for local files. Apply the same permissions to external references, with an opt-in for trusted private-network inputs.
|
|
22
|
+
- [#10598](https://github.com/scalar/scalar/pull/10598): Preserve OpenAPI 3.2 tag ancestry in Markdown exports with parent-first breadcrumb headings.
|
|
23
|
+
- [#10614](https://github.com/scalar/scalar/pull/10614): Clean up lint warnings while preserving runtime behavior, and document intentional schema and type syntax.
|
|
24
|
+
|
|
3
25
|
## 1.5.4
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -68,6 +68,27 @@ const operationMarkdownByPointer = await createMarkdownFromOpenApi(content, {
|
|
|
68
68
|
})
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
### Document loading permissions
|
|
72
|
+
|
|
73
|
+
URL inputs and external references block private, loopback, and link-local addresses by default.
|
|
74
|
+
Protected requests also reject redirects. File access is disabled unless you provide an approved
|
|
75
|
+
`basePath`; paths and symlinks outside that directory are rejected.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const markdown = await createMarkdownFromOpenApi('./descriptions/openapi.yaml', {
|
|
79
|
+
documentLoading: { basePath: './descriptions' },
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const renderer = await createOpenApiMarkdownRenderer('./descriptions/openapi.yaml', {
|
|
83
|
+
basePath: './descriptions',
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For trusted local development inputs, set `allowPrivateNetworks: true` in the same loading options.
|
|
88
|
+
This also permits redirects. Keep these permissions under application control, rather than taking
|
|
89
|
+
them from uploaded API descriptions or request parameters. An approved directory applies to all
|
|
90
|
+
files loaded during that call, including references in remote documents.
|
|
91
|
+
|
|
71
92
|
### Render multiple pages
|
|
72
93
|
|
|
73
94
|
Create a reusable renderer when exporting several pages from the same API description.
|
|
@@ -144,7 +165,12 @@ schema expansion still tracks ancestors and stops at a depth of ten. Output may
|
|
|
144
165
|
list spacing and normalized Markdown escaping compared with earlier versions.
|
|
145
166
|
|
|
146
167
|
For OpenAPI 3.2 descriptions, tag headings use `summary` when present and otherwise
|
|
147
|
-
use `name`.
|
|
168
|
+
use `name`. Nested tags appear parent-first with breadcrumb headings such as
|
|
169
|
+
`### Accounts › Billing › Invoices`. Every tag uses the same heading level, including
|
|
170
|
+
deeply nested tags. Parent relationships use tag names, while breadcrumb labels use
|
|
171
|
+
summaries when available. Missing parents and circular relationships fall back to flat
|
|
172
|
+
headings. Tag selectors and operation tag metadata continue to use `name`; selecting
|
|
173
|
+
one tag still exports only that tag and its directly tagged operations.
|
|
148
174
|
|
|
149
175
|
The package only generates Markdown. To produce HTML, pass the Markdown output to a
|
|
150
176
|
Markdown renderer in your application.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
2
|
+
import { type DocumentLoadOptions } from './load-document.js';
|
|
2
3
|
import { type OpenApiRenderOptions } from './select-document.js';
|
|
3
4
|
type AnyDocument = OpenApiDocument | Record<string, unknown> | string;
|
|
4
5
|
/** A resolved API description that can render multiple pages without loading it again. */
|
|
@@ -9,8 +10,10 @@ export type OpenApiMarkdownRenderer = {
|
|
|
9
10
|
* Load and resolve an API description once, then render any number of selections.
|
|
10
11
|
* Each renderer owns its document; create a new renderer to pick up source changes.
|
|
11
12
|
*/
|
|
12
|
-
export declare const createOpenApiMarkdownRenderer: (input: AnyDocument) => Promise<OpenApiMarkdownRenderer>;
|
|
13
|
+
export declare const createOpenApiMarkdownRenderer: (input: AnyDocument, options?: DocumentLoadOptions) => Promise<OpenApiMarkdownRenderer>;
|
|
13
14
|
/** Generate Markdown from an API description, optionally scoped to a single page. */
|
|
14
|
-
export declare const createMarkdownFromOpenApi: (input: AnyDocument, options?: OpenApiRenderOptions
|
|
15
|
+
export declare const createMarkdownFromOpenApi: (input: AnyDocument, options?: OpenApiRenderOptions & {
|
|
16
|
+
documentLoading?: DocumentLoadOptions;
|
|
17
|
+
}) => Promise<string>;
|
|
15
18
|
export {};
|
|
16
19
|
//# sourceMappingURL=create-markdown-from-openapi.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-markdown-from-openapi.d.ts","sourceRoot":"","sources":["../src/create-markdown-from-openapi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;
|
|
1
|
+
{"version":3,"file":"create-markdown-from-openapi.d.ts","sourceRoot":"","sources":["../src/create-markdown-from-openapi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AAEnG,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,iBAAiB,CAAA;AAExE,OAAO,EAAE,KAAK,oBAAoB,EAAwC,MAAM,mBAAmB,CAAA;AAEnG,KAAK,WAAW,GAAG,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAA;AACrE,0FAA0F;AAC1F,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,oBAAoB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC5D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,6BAA6B,GACxC,OAAO,WAAW,EAClB,UAAU,mBAAmB,KAC5B,OAAO,CAAC,uBAAuB,CAQjC,CAAA;AAED,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,GACpC,OAAO,WAAW,EAClB,UAAU,oBAAoB,GAAG;IAAE,eAAe,CAAC,EAAE,mBAAmB,CAAA;CAAE,KACzE,OAAO,CAAC,MAAM,CAOhB,CAAA"}
|
|
@@ -5,8 +5,8 @@ import { createDocumentLookup, selectDocument } from './select-document.js';
|
|
|
5
5
|
* Load and resolve an API description once, then render any number of selections.
|
|
6
6
|
* Each renderer owns its document; create a new renderer to pick up source changes.
|
|
7
7
|
*/
|
|
8
|
-
export const createOpenApiMarkdownRenderer = async (input) => {
|
|
9
|
-
const content = await loadDocument(input);
|
|
8
|
+
export const createOpenApiMarkdownRenderer = async (input, options) => {
|
|
9
|
+
const content = await loadDocument(input, options);
|
|
10
10
|
const renderDocument = createDocumentRenderer();
|
|
11
11
|
const lookup = createDocumentLookup(content);
|
|
12
12
|
const render = async (options) => await renderDocument(selectDocument(content, options, lookup), options);
|
|
@@ -14,6 +14,10 @@ export const createOpenApiMarkdownRenderer = async (input) => {
|
|
|
14
14
|
};
|
|
15
15
|
/** Generate Markdown from an API description, optionally scoped to a single page. */
|
|
16
16
|
export const createMarkdownFromOpenApi = async (input, options) => {
|
|
17
|
-
const renderer = await createOpenApiMarkdownRenderer(input);
|
|
17
|
+
const renderer = await createOpenApiMarkdownRenderer(input, options?.documentLoading);
|
|
18
|
+
if (options && 'documentLoading' in options) {
|
|
19
|
+
const { documentLoading: _, ...renderOptions } = options;
|
|
20
|
+
return renderer.render(renderOptions);
|
|
21
|
+
}
|
|
18
22
|
return renderer.render(options);
|
|
19
23
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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 { DocumentLoadOptions } from './load-document.js';
|
|
4
5
|
export type { OpenApiRenderOptions, OperationSelector, SchemaReferenceOptions } from './select-document.js';
|
|
5
6
|
//# 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,sBAAsB,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,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA"}
|
package/dist/load-document.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type OpenApiDocument } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
2
|
+
/** Permissions for loading an API description and its external references. */
|
|
3
|
+
export type DocumentLoadOptions = {
|
|
4
|
+
/** Allow private-network requests only for trusted input. Defaults to false. */
|
|
5
|
+
allowPrivateNetworks?: boolean;
|
|
6
|
+
/** Approved directory for local files and references. File access is disabled when omitted. */
|
|
7
|
+
basePath?: string;
|
|
8
|
+
};
|
|
2
9
|
/** Coerce one plain document and keep references linked to shared targets. */
|
|
3
|
-
export declare const loadDocument: (input: OpenApiDocument | Record<string, unknown> | string) => Promise<OpenApiDocument>;
|
|
10
|
+
export declare const loadDocument: (input: OpenApiDocument | Record<string, unknown> | string, options?: DocumentLoadOptions) => Promise<OpenApiDocument>;
|
|
4
11
|
//# sourceMappingURL=load-document.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
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;AAiMrE,8EAA8E;AAC9E,eAAO,MAAM,YAAY,GACvB,OAAO,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,
|
|
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;AAiMrE,8EAA8E;AAC9E,MAAM,MAAM,mBAAmB,GAAG;IAChC,gFAAgF;IAChF,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,8EAA8E;AAC9E,eAAO,MAAM,YAAY,GACvB,OAAO,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EACzD,UAAS,mBAAwB,KAChC,OAAO,CAAC,eAAe,CAoGzB,CAAA"}
|
package/dist/load-document.js
CHANGED
|
@@ -153,9 +153,12 @@ const attachRefValues = (document, schemas = getSchemas(document), { detectOnly
|
|
|
153
153
|
return hasExternalReferences;
|
|
154
154
|
};
|
|
155
155
|
/** Coerce one plain document and keep references linked to shared targets. */
|
|
156
|
-
export const loadDocument = async (input) => {
|
|
156
|
+
export const loadDocument = async (input, options = {}) => {
|
|
157
157
|
let raw;
|
|
158
158
|
let origin;
|
|
159
|
+
const { fetchUrls, readFiles } = await import('@scalar/json-magic/bundle/plugins/node');
|
|
160
|
+
const urlLoader = fetchUrls({ blockPrivateNetworks: !options.allowPrivateNetworks });
|
|
161
|
+
const fileLoader = options.basePath === undefined ? undefined : readFiles({ basePath: options.basePath });
|
|
159
162
|
// Reference links are attached below, so never mutate the caller's document.
|
|
160
163
|
if (typeof input !== 'string') {
|
|
161
164
|
raw = deepClone(getRaw(input));
|
|
@@ -167,17 +170,24 @@ export const loadDocument = async (input) => {
|
|
|
167
170
|
raw = normalized;
|
|
168
171
|
}
|
|
169
172
|
else if (/^https?:\/\//i.test(input)) {
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
172
|
-
throw new Error(
|
|
173
|
+
const result = await urlLoader.exec(input);
|
|
174
|
+
if (!result.ok) {
|
|
175
|
+
throw new Error('Failed to load OpenAPI document');
|
|
173
176
|
}
|
|
174
|
-
raw =
|
|
177
|
+
raw = result.data;
|
|
175
178
|
origin = input;
|
|
176
179
|
}
|
|
177
180
|
else {
|
|
178
|
-
|
|
181
|
+
if (!fileLoader) {
|
|
182
|
+
throw new Error('Local file access requires an explicit basePath');
|
|
183
|
+
}
|
|
184
|
+
const { resolve } = await import('node:path');
|
|
179
185
|
origin = resolve(input);
|
|
180
|
-
|
|
186
|
+
const result = await fileLoader.exec(origin);
|
|
187
|
+
if (!result.ok) {
|
|
188
|
+
throw new Error('Failed to load OpenAPI document');
|
|
189
|
+
}
|
|
190
|
+
raw = result.data;
|
|
181
191
|
}
|
|
182
192
|
}
|
|
183
193
|
if (!isObject(raw)) {
|
|
@@ -193,12 +203,7 @@ export const loadDocument = async (input) => {
|
|
|
193
203
|
// Resolve external targets only when the initial local-reference pass finds them.
|
|
194
204
|
if (hasExternalReferences) {
|
|
195
205
|
const errors = [];
|
|
196
|
-
const plugins =
|
|
197
|
-
? [(await import('@scalar/json-magic/bundle/plugins/browser')).fetchUrls()]
|
|
198
|
-
: [
|
|
199
|
-
(await import('@scalar/json-magic/bundle/plugins/node')).fetchUrls(),
|
|
200
|
-
(await import('@scalar/json-magic/bundle/plugins/node')).readFiles(),
|
|
201
|
-
];
|
|
206
|
+
const plugins = fileLoader ? [urlLoader, fileLoader] : [urlLoader];
|
|
202
207
|
document = await bundle(upgraded, {
|
|
203
208
|
plugins,
|
|
204
209
|
origin,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Format-neutral Markdown building blocks shared by API description renderers. */
|
|
2
|
+
export { anchor, createDocumentAnchors } from '../document-anchors.js';
|
|
3
|
+
export { describe, field, heading, inlineCode, item, link, list, paragraph, strong, text } from '../markdown-nodes.js';
|
|
4
|
+
export { createDescriptionParser, expandDescriptions } from '../parse-description.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/markdown/index.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAEnF,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAA;AACnH,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Format-neutral Markdown building blocks shared by API description renderers. */
|
|
2
|
+
export { anchor, createDocumentAnchors } from '../document-anchors.js';
|
|
3
|
+
export { describe, field, heading, inlineCode, item, link, list, paragraph, strong, text } from '../markdown-nodes.js';
|
|
4
|
+
export { createDescriptionParser, expandDescriptions } from '../parse-description.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-nodes.d.ts","sourceRoot":"","sources":["../src/markdown-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,OAAO,EACP,UAAU,EACV,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,eAAe,EACf,MAAM,EACN,IAAI,EACL,MAAM,OAAO,CAAA;AAEd;;;GAGG;AACH,KAAK,sBAAsB,GAAG,IAAI,GAAG;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtF,OAAO,QAAQ,OAAO,CAAC;IACrB,UAAU,eAAe;QACvB,sBAAsB,EAAE,sBAAsB,CAAA;KAC/C;IACD,UAAU,cAAc;QACtB,sBAAsB,EAAE,sBAAsB,CAAA;KAC/C;CACF;AAED,gGAAgG;AAChG,eAAO,MAAM,IAAI,GAAI,OAAO,OAAO,KAAG,IAGpC,CAAA;AACF,uDAAuD;AACvD,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,UAAkE,CAAA;AAC9G,iDAAiD;AACjD,eAAO,MAAM,SAAS,GAAI,GAAG,UAAU,eAAe,EAAE,KAAG,SAA8C,CAAA;AACzG,qCAAqC;AACrC,eAAO,MAAM,MAAM,GAAI,GAAG,UAAU,eAAe,EAAE,KAAG,MAAwC,CAAA;AAChG,oEAAoE;AACpE,eAAO,MAAM,QAAQ,GAAI,GAAG,UAAU,eAAe,EAAE,KAAG,QAA4C,CAAA;AACtG,mCAAmC;AACnC,eAAO,MAAM,OAAO,GAAI,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,UAAU,eAAe,EAAE,KAAG,OAIhF,CAAA;AACF,4DAA4D;AAC5D,eAAO,MAAM,IAAI,GAAI,GAAG,UAAU,QAAQ,CAAC,UAAU,CAAC,KAAG,QAA2D,CAAA;AACpH,mCAAmC;AACnC,eAAO,MAAM,IAAI,GAAI,UAAU,QAAQ,EAAE,KAAG,IAAmE,CAAA;AAC/G;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,
|
|
1
|
+
{"version":3,"file":"markdown-nodes.d.ts","sourceRoot":"","sources":["../src/markdown-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,OAAO,EACP,UAAU,EACV,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,eAAe,EACf,MAAM,EACN,IAAI,EACL,MAAM,OAAO,CAAA;AAEd;;;GAGG;AACH,KAAK,sBAAsB,GAAG,IAAI,GAAG;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtF,OAAO,QAAQ,OAAO,CAAC;IACrB,UAAU,eAAe;QACvB,sBAAsB,EAAE,sBAAsB,CAAA;KAC/C;IACD,UAAU,cAAc;QACtB,sBAAsB,EAAE,sBAAsB,CAAA;KAC/C;CACF;AAED,gGAAgG;AAChG,eAAO,MAAM,IAAI,GAAI,OAAO,OAAO,KAAG,IAGpC,CAAA;AACF,uDAAuD;AACvD,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,UAAkE,CAAA;AAC9G,iDAAiD;AACjD,eAAO,MAAM,SAAS,GAAI,GAAG,UAAU,eAAe,EAAE,KAAG,SAA8C,CAAA;AACzG,qCAAqC;AACrC,eAAO,MAAM,MAAM,GAAI,GAAG,UAAU,eAAe,EAAE,KAAG,MAAwC,CAAA;AAChG,oEAAoE;AACpE,eAAO,MAAM,QAAQ,GAAI,GAAG,UAAU,eAAe,EAAE,KAAG,QAA4C,CAAA;AACtG,mCAAmC;AACnC,eAAO,MAAM,OAAO,GAAI,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,UAAU,eAAe,EAAE,KAAG,OAIhF,CAAA;AACF,4DAA4D;AAC5D,eAAO,MAAM,IAAI,GAAI,GAAG,UAAU,QAAQ,CAAC,UAAU,CAAC,KAAG,QAA2D,CAAA;AACpH,mCAAmC;AACnC,eAAO,MAAM,IAAI,GAAI,UAAU,QAAQ,EAAE,KAAG,IAAmE,CAAA;AAC/G;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,MAKrC,CAAA;AACD,8EAA8E;AAC9E,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,OAAO,MAAM,KAAG,IAAsE,CAAA;AACxH,oFAAoF;AACpF,eAAO,MAAM,KAAK,GAAI,OAAO,MAAM,EAAE,OAAO,eAAe,KAAG,QACK,CAAA;AACnE,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,sBAAsB,EAC8B,CAAA"}
|
package/dist/markdown-nodes.js
CHANGED
|
@@ -26,6 +26,7 @@ export const list = (children) => ({ type: 'list', ordered: false, spread: false
|
|
|
26
26
|
* additional schemes and excludes IRC/XMPP, so it is not compatible here.
|
|
27
27
|
*/
|
|
28
28
|
export const safeUrl = (url) => {
|
|
29
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Control characters are intentionally matched to validate or sanitize input.
|
|
29
30
|
const normalized = url.replace(/[\u0000-\u0020\u007f-\u009f]/g, '');
|
|
30
31
|
const protocol = /^([^/?#]*):/.exec(normalized)?.[1];
|
|
31
32
|
return protocol && !['http', 'https', 'irc', 'ircs', 'mailto', 'xmpp'].includes(protocol.toLowerCase()) ? '' : url;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-document.d.ts","sourceRoot":"","sources":["../src/render-document.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,8DAA8D,CAAA;
|
|
1
|
+
{"version":3,"file":"render-document.d.ts","sourceRoot":"","sources":["../src/render-document.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,8DAA8D,CAAA;AAerE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAwD7D,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,QAAO,CAAC,CACzC,QAAQ,EAAE,eAAe,EACzB,OAAO,CAAC,EAAE,oBAAoB,KAC3B,OAAO,CAAC,MAAM,CAAC,CAyRnB,CAAA"}
|
package/dist/render-document.js
CHANGED
|
@@ -12,6 +12,7 @@ import { renderExamples } from './render-examples.js';
|
|
|
12
12
|
import { formatOperationMethod, renderOperation } from './render-operation.js';
|
|
13
13
|
import { createSchemaRenderer } from './render-schema.js';
|
|
14
14
|
import { renderSecurity } from './render-security.js';
|
|
15
|
+
import { getTagSections } from './render-tags.js';
|
|
15
16
|
const serializer = unified()
|
|
16
17
|
.use(remarkGfm)
|
|
17
18
|
.use(remarkStringify, {
|
|
@@ -185,8 +186,7 @@ export const createDocumentRenderer = () => {
|
|
|
185
186
|
nodes.push(...(await renderSecurity(document.security, document.components?.securitySchemes, description, 2)));
|
|
186
187
|
if (document.tags?.length) {
|
|
187
188
|
nodes.push(heading(2, text('Tags')));
|
|
188
|
-
for (const tag of document.tags) {
|
|
189
|
-
const title = /^3\.2\./.test(openapiVersion) ? (tag.summary ?? tag.name) : tag.name;
|
|
189
|
+
for (const { tag, title } of getTagSections(document.tags, openapiVersion)) {
|
|
190
190
|
nodes.push(heading(3, text(title)), ...(await description(tag.description)));
|
|
191
191
|
if (tag.externalDocs) {
|
|
192
192
|
nodes.push(paragraph(link(tag.externalDocs.url, tag.externalDocs.description ?? tag.externalDocs.url)));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TagObject } from '@scalar/workspace-store/schemas/v3.2/strict/tag';
|
|
2
|
+
/** Keep tag ancestry visible without exceeding Markdown's six heading levels. */
|
|
3
|
+
export declare const getTagSections: (tags: TagObject[], openapiVersion: string) => {
|
|
4
|
+
tag: TagObject;
|
|
5
|
+
title: string;
|
|
6
|
+
}[];
|
|
7
|
+
//# sourceMappingURL=render-tags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-tags.d.ts","sourceRoot":"","sources":["../src/render-tags.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iDAAiD,CAAA;AAEhF,iFAAiF;AACjF,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,EAAE,EAAE,gBAAgB,MAAM,KAAG;IAAE,GAAG,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAwE3G,CAAA"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Keep tag ancestry visible without exceeding Markdown's six heading levels. */
|
|
2
|
+
export const getTagSections = (tags, openapiVersion) => {
|
|
3
|
+
const nested = /^3\.2\./.test(openapiVersion);
|
|
4
|
+
const label = (tag) => (nested ? (tag.summary ?? tag.name) : tag.name);
|
|
5
|
+
const byName = new Map(tags.map((tag) => [tag.name, tag]));
|
|
6
|
+
// Duplicate names cannot identify a parent unambiguously. Preserve every declaration.
|
|
7
|
+
if (!nested || byName.size !== tags.length) {
|
|
8
|
+
return tags.map((tag) => ({ tag, title: label(tag) }));
|
|
9
|
+
}
|
|
10
|
+
const titles = new Map();
|
|
11
|
+
const parents = new Map();
|
|
12
|
+
const invalid = new Set();
|
|
13
|
+
for (const tag of tags) {
|
|
14
|
+
const chain = [];
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const pending = [tag];
|
|
17
|
+
while (pending.length) {
|
|
18
|
+
const current = pending.pop();
|
|
19
|
+
if (seen.has(current.name) || invalid.has(current.name)) {
|
|
20
|
+
// Invalid cycles and chains leading into them stay flat, like reference navigation.
|
|
21
|
+
for (const member of chain) {
|
|
22
|
+
titles.set(member.name, label(member));
|
|
23
|
+
invalid.add(member.name);
|
|
24
|
+
}
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
if (titles.has(current.name)) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
seen.add(current.name);
|
|
31
|
+
chain.push(current);
|
|
32
|
+
const parent = current.parent === undefined ? undefined : byName.get(current.parent);
|
|
33
|
+
if (parent) {
|
|
34
|
+
pending.push(parent);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const current of chain.reverse()) {
|
|
38
|
+
if (titles.has(current.name)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const parentTitle = current.parent === undefined ? undefined : titles.get(current.parent);
|
|
42
|
+
titles.set(current.name, parentTitle === undefined ? label(current) : `${parentTitle} › ${label(current)}`);
|
|
43
|
+
if (parentTitle !== undefined && current.parent !== undefined) {
|
|
44
|
+
parents.set(current.name, current.parent);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const children = new Map();
|
|
49
|
+
const roots = [];
|
|
50
|
+
for (const tag of tags) {
|
|
51
|
+
const parent = parents.get(tag.name);
|
|
52
|
+
if (parent === undefined) {
|
|
53
|
+
roots.push(tag);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const siblings = children.get(parent) ?? [];
|
|
57
|
+
siblings.push(tag);
|
|
58
|
+
children.set(parent, siblings);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const sections = [];
|
|
62
|
+
const pending = roots.reverse();
|
|
63
|
+
while (pending.length) {
|
|
64
|
+
const tag = pending.pop();
|
|
65
|
+
sections.push({ tag, title: titles.get(tag.name) });
|
|
66
|
+
// Use a stack instead of recursion so deeply nested descriptions cannot exhaust the call stack.
|
|
67
|
+
const siblings = children.get(tag.name) ?? [];
|
|
68
|
+
for (const child of [...siblings].reverse()) {
|
|
69
|
+
pending.push(child);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return sections;
|
|
73
|
+
};
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"llm",
|
|
17
17
|
"swagger"
|
|
18
18
|
],
|
|
19
|
-
"version": "1.
|
|
19
|
+
"version": "1.6.0",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"import": "./dist/browser.js",
|
|
34
34
|
"types": "./dist/browser.d.ts",
|
|
35
35
|
"default": "./dist/browser.js"
|
|
36
|
+
},
|
|
37
|
+
"./markdown": {
|
|
38
|
+
"import": "./dist/markdown/index.js",
|
|
39
|
+
"types": "./dist/markdown/index.d.ts",
|
|
40
|
+
"default": "./dist/markdown/index.js"
|
|
36
41
|
}
|
|
37
42
|
},
|
|
38
43
|
"files": [
|
|
@@ -40,11 +45,11 @@
|
|
|
40
45
|
"CHANGELOG.md"
|
|
41
46
|
],
|
|
42
47
|
"dependencies": {
|
|
43
|
-
"@scalar/code-highlight": "0.4.
|
|
44
|
-
"@scalar/helpers": "0.16.
|
|
45
|
-
"@scalar/json-magic": "0.
|
|
46
|
-
"@scalar/openapi-upgrader": "0.4.
|
|
47
|
-
"@scalar/workspace-store": "0.68.
|
|
48
|
+
"@scalar/code-highlight": "0.4.10",
|
|
49
|
+
"@scalar/helpers": "0.16.4",
|
|
50
|
+
"@scalar/json-magic": "0.16.0",
|
|
51
|
+
"@scalar/openapi-upgrader": "0.4.5",
|
|
52
|
+
"@scalar/workspace-store": "0.68.4",
|
|
48
53
|
"rehype-parse": "^9.0.1",
|
|
49
54
|
"rehype-remark": "^10.0.1",
|
|
50
55
|
"rehype-sanitize": "^6.0.0",
|
|
@@ -54,9 +59,9 @@
|
|
|
54
59
|
"unified": "^11.0.5"
|
|
55
60
|
},
|
|
56
61
|
"devDependencies": {
|
|
57
|
-
"@scalar/components": "0.
|
|
58
|
-
"@scalar/galaxy": "0.7.
|
|
59
|
-
"@scalar/themes": "0.19.
|
|
62
|
+
"@scalar/components": "0.32.0",
|
|
63
|
+
"@scalar/galaxy": "0.7.2",
|
|
64
|
+
"@scalar/themes": "0.19.2",
|
|
60
65
|
"@tailwindcss/vite": "^4.3.3",
|
|
61
66
|
"@types/mdast": "^4.0.4",
|
|
62
67
|
"@vitejs/plugin-vue": "^6.0.8",
|