@scalar/openapi-to-markdown 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @scalar/openapi-to-markdown
2
2
 
3
+ ## 1.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#10457](https://github.com/scalar/scalar/pull/10457): Render a selected operation, model, tag or webhook in time proportional to the selection instead of the whole document.
8
+
3
9
  ## 1.5.0
4
10
 
5
11
  ### Minor Changes
@@ -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;AAInG,OAAO,EAAE,KAAK,oBAAoB,EAAkB,MAAM,mBAAmB,CAAA;AAE7E,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,GAAU,OAAO,WAAW,KAAG,OAAO,CAAC,uBAAuB,CAOvG,CAAA;AAED,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,GACpC,OAAO,WAAW,EAClB,UAAU,oBAAoB,KAC7B,OAAO,CAAC,MAAM,CAGhB,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;AAInG,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,GAAU,OAAO,WAAW,KAAG,OAAO,CAAC,uBAAuB,CAQvG,CAAA;AAED,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,GACpC,OAAO,WAAW,EAClB,UAAU,oBAAoB,KAC7B,OAAO,CAAC,MAAM,CAGhB,CAAA"}
@@ -1,6 +1,6 @@
1
1
  import { loadDocument } from './load-document.js';
2
2
  import { createDocumentRenderer } from './render-document.js';
3
- import { selectDocument } from './select-document.js';
3
+ import { createDocumentLookup, selectDocument } from './select-document.js';
4
4
  /**
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.
@@ -8,7 +8,8 @@ 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), options);
11
+ const lookup = createDocumentLookup(content);
12
+ const render = async (options) => await renderDocument(selectDocument(content, options, lookup), options);
12
13
  return { render };
13
14
  };
14
15
  /** Generate Markdown from an API description, optionally scoped to a single page. */
@@ -1,5 +1,5 @@
1
1
  import type { OperationMethod } from '@scalar/workspace-store/schemas/navigation';
2
- import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
2
+ import type { OpenApiDocument, PathItemObject } 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;
@@ -40,7 +40,33 @@ type PageSelectors = {
40
40
  };
41
41
  introduction: true;
42
42
  };
43
- /** Scope after resolving references and migrating older documents. */
44
- export declare const selectDocument: (document: OpenApiDocument, options?: OpenApiRenderOptions) => OpenApiDocument;
43
+ type OperationMatch = {
44
+ path: string;
45
+ method: OperationMethod;
46
+ };
47
+ type TaggedPathItem = {
48
+ path: string;
49
+ pathItem: PathItemObject;
50
+ methods: string[];
51
+ };
52
+ /**
53
+ * Lookups that every selection from one document shares, built on first use.
54
+ * The document must not change while its lookup is in use.
55
+ */
56
+ type DocumentLookup = {
57
+ /** Operations by operation ID, in document order. */
58
+ operationsById: () => ReadonlyMap<string, OperationMatch[]>;
59
+ /** Path items with the methods that carry each tag, in document order. */
60
+ operationsByTag: () => ReadonlyMap<string, TaggedPathItem[]>;
61
+ /** The position of each schema in `components.schemas`. */
62
+ schemaPositions: () => ReadonlyMap<string, number>;
63
+ };
64
+ /** Index a document once so that each selection costs time in proportion to what it selects. */
65
+ export declare const createDocumentLookup: (document: OpenApiDocument) => DocumentLookup;
66
+ /**
67
+ * Scope after resolving references and migrating older documents.
68
+ * Pass the same lookup to every selection from one document to reuse its indexes.
69
+ */
70
+ export declare const selectDocument: (document: OpenApiDocument, options?: OpenApiRenderOptions, lookup?: DocumentLookup) => OpenApiDocument;
45
71
  export {};
46
72
  //# sourceMappingURL=select-document.d.ts.map
@@ -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,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;QAC7E;;;WAGG;QACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAC3B,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,eA4N9F,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,EAAE,cAAc,EAAE,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;QAC7E;;;WAGG;QACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAC3B,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;AACD,KAAK,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAA;AA+G/D,KAAK,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEnF;;;GAGG;AACH,KAAK,cAAc,GAAG;IACpB,qDAAqD;IACrD,cAAc,EAAE,MAAM,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;IAC3D,0EAA0E;IAC1E,eAAe,EAAE,MAAM,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;IAC5D,2DAA2D;IAC3D,eAAe,EAAE,MAAM,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnD,CAAA;AA8BD,gGAAgG;AAChG,eAAO,MAAM,oBAAoB,GAAI,UAAU,eAAe,KAAG,cAWhE,CAAA;AAwDD;;;GAGG;AACH,eAAO,MAAM,cAAc,GACzB,UAAU,eAAe,EACzB,UAAS,oBAAyB,EAClC,SAAQ,cAA+C,KACtD,eA2NF,CAAA"}
@@ -78,16 +78,43 @@ const findOperationByPathAndMethod = (document, selector) => {
78
78
  method,
79
79
  };
80
80
  };
81
- const findOperationsByOperationId = (document, operationId) => getPathEntries(document).flatMap(([path, pathItem]) => {
82
- const matches = [];
83
- forEachPathItemOperation(pathItem, (method, operation) => {
84
- if (getResolvedRef(operation)?.operationId === operationId) {
85
- matches.push({ path, method });
86
- }
87
- });
88
- return matches;
89
- });
90
- const resolveOperationMatch = (document, selector) => {
81
+ const indexOperations = (document) => {
82
+ const byId = new Map();
83
+ const byTag = new Map();
84
+ for (const [path, pathItem] of getPathEntries(document)) {
85
+ forEachPathItemOperation(pathItem, (method, operationRef) => {
86
+ const operation = getResolvedRef(operationRef);
87
+ if (typeof operation?.operationId === 'string') {
88
+ const matches = byId.get(operation.operationId) ?? [];
89
+ matches.push({ path, method });
90
+ byId.set(operation.operationId, matches);
91
+ }
92
+ for (const tag of new Set(operation?.tags)) {
93
+ const entries = byTag.get(tag) ?? [];
94
+ const last = entries.at(-1);
95
+ if (last?.path === path) {
96
+ last.methods.push(method);
97
+ }
98
+ else {
99
+ entries.push({ path, pathItem, methods: [method] });
100
+ }
101
+ byTag.set(tag, entries);
102
+ }
103
+ });
104
+ }
105
+ return { byId, byTag };
106
+ };
107
+ /** Index a document once so that each selection costs time in proportion to what it selects. */
108
+ export const createDocumentLookup = (document) => {
109
+ let operations;
110
+ let schemaPositions;
111
+ return {
112
+ operationsById: () => (operations ??= indexOperations(document)).byId,
113
+ operationsByTag: () => (operations ??= indexOperations(document)).byTag,
114
+ schemaPositions: () => (schemaPositions ??= new Map(Object.keys(document.components?.schemas ?? {}).map((name, position) => [name, position]))),
115
+ };
116
+ };
117
+ const resolveOperationMatch = (document, selector, lookup) => {
91
118
  if ('pointer' in selector) {
92
119
  const match = getOperationSelectorFromPointer(selector.pointer);
93
120
  if (!getPathItemOperation(document.paths?.[match.path], match.method)) {
@@ -96,7 +123,7 @@ const resolveOperationMatch = (document, selector) => {
96
123
  return match;
97
124
  }
98
125
  if ('operationId' in selector) {
99
- const matches = findOperationsByOperationId(document, selector.operationId);
126
+ const matches = lookup.operationsById().get(selector.operationId) ?? [];
100
127
  if (!matches.length) {
101
128
  throw new Error(`Operation with operationId "${selector.operationId}" was not found`);
102
129
  }
@@ -108,9 +135,9 @@ const resolveOperationMatch = (document, selector) => {
108
135
  }
109
136
  return findOperationByPathAndMethod(document, selector);
110
137
  };
111
- const filterDocumentByOperation = (document, selector) => {
112
- const match = resolveOperationMatch(document, selector);
113
- const pathItem = getPathEntries(document).find(([path]) => path === match.path)?.[1];
138
+ const filterDocumentByOperation = (document, selector, lookup) => {
139
+ const match = resolveOperationMatch(document, selector, lookup);
140
+ const pathItem = getResolvedPathItem(document.paths?.[match.path]);
114
141
  if (!pathItem) {
115
142
  throw new Error(`Operation not found for path "${match.path}" and method "${match.method.toUpperCase()}"`);
116
143
  }
@@ -121,8 +148,11 @@ const filterDocumentByOperation = (document, selector) => {
121
148
  },
122
149
  };
123
150
  };
124
- /** Scope after resolving references and migrating older documents. */
125
- export const selectDocument = (document, options = {}) => {
151
+ /**
152
+ * Scope after resolving references and migrating older documents.
153
+ * Pass the same lookup to every selection from one document to reuse its indexes.
154
+ */
155
+ export const selectDocument = (document, options = {}, lookup = createDocumentLookup(document)) => {
126
156
  if (!isObject(options)) {
127
157
  throw new Error('Render options must be an object');
128
158
  }
@@ -159,7 +189,7 @@ export const selectDocument = (document, options = {}) => {
159
189
  const selected = { ...document, paths: {}, webhooks: {}, tags: [] };
160
190
  const modelRoots = [];
161
191
  if (options.operation) {
162
- selected.paths = filterDocumentByOperation(document, options.operation).paths;
192
+ selected.paths = filterDocumentByOperation(document, options.operation, lookup).paths;
163
193
  }
164
194
  if (options.tag !== undefined) {
165
195
  const metadata = document.tags?.filter((tag) => tag.name === options.tag) ?? [];
@@ -167,16 +197,8 @@ export const selectDocument = (document, options = {}) => {
167
197
  throw new Error(`Multiple tags found for "${options.tag}"`);
168
198
  }
169
199
  selected.tags = metadata.length ? metadata : [{ name: options.tag }];
170
- for (const [path, item] of getPathEntries(document)) {
171
- const methods = [];
172
- forEachPathItemOperation(item, (method, operation) => {
173
- if (getResolvedRef(operation)?.tags?.includes(options.tag)) {
174
- methods.push(method);
175
- }
176
- });
177
- if (methods.length) {
178
- selected.paths[path] = filterPathItemOperations(item, methods);
179
- }
200
+ for (const { path, pathItem, methods } of lookup.operationsByTag().get(options.tag) ?? []) {
201
+ selected.paths[path] = filterPathItemOperations(pathItem, methods);
180
202
  }
181
203
  if (!metadata.length && !Object.keys(selected.paths ?? {}).length) {
182
204
  throw new Error(`Tag "${options.tag}" was not found`);
@@ -327,9 +349,14 @@ export const selectDocument = (document, options = {}) => {
327
349
  visit(root);
328
350
  }
329
351
  }
352
+ const positions = lookup.schemaPositions();
330
353
  selected.components = {
331
354
  ...document.components,
332
- schemas: Object.fromEntries(Object.entries(schemas).filter(([name]) => needed.has(name))),
355
+ // Keep the document's schema order, which decides the order of the page's model sections.
356
+ schemas: Object.fromEntries([...needed]
357
+ .filter((name) => positions.has(name))
358
+ .sort((a, b) => positions.get(a) - positions.get(b))
359
+ .map((name) => [name, schemas[name]])),
333
360
  securitySchemes: Object.fromEntries(Object.entries(document.components?.securitySchemes ?? {}).filter(([name]) => securityNames.has(name))),
334
361
  };
335
362
  return selected;
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "llm",
17
17
  "swagger"
18
18
  ],
19
- "version": "1.5.0",
19
+ "version": "1.5.1",
20
20
  "engines": {
21
21
  "node": ">=22"
22
22
  },
@@ -40,11 +40,11 @@
40
40
  "CHANGELOG.md"
41
41
  ],
42
42
  "dependencies": {
43
- "@scalar/code-highlight": "0.4.7",
43
+ "@scalar/code-highlight": "0.4.8",
44
44
  "@scalar/helpers": "0.16.0",
45
- "@scalar/json-magic": "0.15.3",
45
+ "@scalar/json-magic": "0.15.4",
46
46
  "@scalar/openapi-upgrader": "0.4.1",
47
- "@scalar/workspace-store": "0.67.2",
47
+ "@scalar/workspace-store": "0.68.0",
48
48
  "rehype-parse": "^9.0.1",
49
49
  "rehype-remark": "^10.0.1",
50
50
  "rehype-sanitize": "^6.0.0",
@@ -54,7 +54,7 @@
54
54
  "unified": "^11.0.5"
55
55
  },
56
56
  "devDependencies": {
57
- "@scalar/components": "0.30.5",
57
+ "@scalar/components": "0.30.6",
58
58
  "@scalar/galaxy": "0.7.1",
59
59
  "@scalar/themes": "0.18.1",
60
60
  "@tailwindcss/vite": "^4.3.3",