rapidoc 11.0.1 → 12.1.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 (96) hide show
  1. package/README.md +171 -54
  2. package/dist/rapidoc-flow-min.js +580 -0
  3. package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
  4. package/dist/rapidoc-min.js +645 -202
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +57 -42
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +912 -0
  10. package/src/components/api-response.ts +514 -0
  11. package/src/components/json-tree.ts +171 -0
  12. package/src/components/schema-base-element.ts +329 -0
  13. package/src/components/schema-table.ts +475 -0
  14. package/src/components/schema-tree.ts +393 -0
  15. package/src/components/tag-input.ts +180 -0
  16. package/src/flow.js +3 -0
  17. package/src/index.js +6 -0
  18. package/src/oauth-receiver.ts +29 -0
  19. package/src/rapi-flow.ts +1178 -0
  20. package/src/rapidoc-mini.ts +603 -0
  21. package/src/rapidoc.ts +1854 -0
  22. package/src/styles/README.md +96 -0
  23. package/src/styles/auth-styles.js +318 -0
  24. package/src/styles/border-styles.js +24 -0
  25. package/src/styles/css-bundle.js +129 -0
  26. package/src/styles/custom-styles.js +8 -0
  27. package/src/styles/dialog-box-styles.js +102 -0
  28. package/src/styles/endpoint-styles.js +564 -0
  29. package/src/styles/flex-styles.js +16 -0
  30. package/src/styles/flow-styles.js +248 -0
  31. package/src/styles/font-styles.js +432 -0
  32. package/src/styles/info-styles.js +36 -0
  33. package/src/styles/input-styles.js +555 -0
  34. package/src/styles/layout-styles.js +396 -0
  35. package/src/styles/microlighter-styles.js +99 -0
  36. package/src/styles/nav-styles.js +633 -0
  37. package/src/styles/param-list-styles.js +261 -0
  38. package/src/styles/schema-styles.js +296 -0
  39. package/src/styles/tab-styles.js +106 -0
  40. package/src/styles/theme-tokens.js +582 -0
  41. package/src/templates/README.md +89 -0
  42. package/src/templates/advanced-search-dialog-template.js +94 -0
  43. package/src/templates/api-response-template.js +223 -0
  44. package/src/templates/callback-template.js +86 -0
  45. package/src/templates/code-samples-template.js +52 -0
  46. package/src/templates/components-template.js +86 -0
  47. package/src/templates/downloads-template.js +70 -0
  48. package/src/templates/endpoint-template.js +377 -0
  49. package/src/templates/expanded-endpoint-template.js +270 -0
  50. package/src/templates/focused-endpoint-template.js +146 -0
  51. package/src/templates/header-template.js +92 -0
  52. package/src/templates/logo-template.js +21 -0
  53. package/src/templates/main-body-template.js +102 -0
  54. package/src/templates/navbar-template.js +498 -0
  55. package/src/templates/overview-template.js +104 -0
  56. package/src/templates/request-body-template.js +673 -0
  57. package/src/templates/request-params-template.js +485 -0
  58. package/src/templates/security-scheme-template.js +1272 -0
  59. package/src/templates/server-template.js +153 -0
  60. package/src/utils/README.md +146 -0
  61. package/src/utils/arazzo/README.md +119 -0
  62. package/src/utils/arazzo/components.js +15 -0
  63. package/src/utils/arazzo/graph.js +269 -0
  64. package/src/utils/arazzo/index.js +52 -0
  65. package/src/utils/arazzo/operations.js +106 -0
  66. package/src/utils/arazzo/parse.js +196 -0
  67. package/src/utils/arazzo/refs.js +40 -0
  68. package/src/utils/arazzo/svg.js +707 -0
  69. package/src/utils/auto-color-scheme.js +99 -0
  70. package/src/utils/bind-navbar.js +175 -0
  71. package/src/utils/color-utils.js +59 -0
  72. package/src/utils/common-utils.js +191 -0
  73. package/src/utils/curl-utils.js +126 -0
  74. package/src/utils/highlighter.js +419 -0
  75. package/src/utils/markdown-utils.js +110 -0
  76. package/src/utils/mcp/README.md +155 -0
  77. package/src/utils/mcp/common.js +166 -0
  78. package/src/utils/mcp/manifest.js +382 -0
  79. package/src/utils/mcp/project/server.ts +53 -0
  80. package/src/utils/mcp/project.js +88 -0
  81. package/src/utils/mcp/runtime.js +289 -0
  82. package/src/utils/mcp/schema.js +98 -0
  83. package/src/utils/mcp/workflow-runtime.js +374 -0
  84. package/src/utils/mcp/workflows.js +240 -0
  85. package/src/utils/mcp/zip.js +102 -0
  86. package/src/utils/mcp-generator.js +47 -0
  87. package/src/utils/mock-interceptor.js +557 -0
  88. package/src/utils/oauth-utils.js +87 -0
  89. package/src/utils/request-utils.js +264 -0
  90. package/src/utils/response-utils.js +203 -0
  91. package/src/utils/sanitize.js +57 -0
  92. package/src/utils/schema-utils.js +1954 -0
  93. package/src/utils/slugger.js +187 -0
  94. package/src/utils/spec-parser.js +941 -0
  95. package/src/utils/url-utils.js +246 -0
  96. package/src/utils/xml-utils.js +61 -0
@@ -0,0 +1,941 @@
1
+ import { dereference, load, upgrade } from "@scalar/openapi-parser";
2
+ import { parse as parseYaml } from "yaml";
3
+ import { marked } from "marked";
4
+ import { invalidCharsRegEx, rapidocApiKey, sleep } from "./common-utils.js";
5
+ import { OperationSlugger, toKebabCase } from "./slugger.js";
6
+
7
+ function toPascalCase(str) {
8
+ return String(str || "")
9
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
10
+ .replace(/^[a-z]/, (chr) => chr.toUpperCase());
11
+ }
12
+
13
+ function isUrlLike(value) {
14
+ if (typeof value !== "string") {
15
+ return false;
16
+ }
17
+ const trimmed = value.trim();
18
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
19
+ return false;
20
+ }
21
+ if (trimmed.includes("\n")) {
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+
27
+ // The page URL that relative spec URLs are resolved against; guard for non-browser environments (SSR / test runners)
28
+ function pageUrl() {
29
+ return typeof window !== "undefined" && window.location?.href
30
+ ? window.location.href
31
+ : "http://localhost/";
32
+ }
33
+
34
+ /**
35
+ * Custom browser fetch plugin for `@scalar/openapi-parser`'s `load()` method.
36
+ *
37
+ * Provides browser-native `fetch` support for loading specifications and resolving
38
+ * relative and external `$ref` files across browser and SSR/Node environments.
39
+ *
40
+ * @param {string} [baseUrl] - The base URL or document path to resolve relative references against.
41
+ * @returns {import('@scalar/openapi-parser').LoadPlugin} A plugin object conforming to the `@scalar/openapi-parser` plugin interface:
42
+ * - `check(value)`: Determines whether this plugin handles the given reference/URL.
43
+ * - `resolvePath(parentPath, reference)`: Resolves relative reference paths against parent document URLs.
44
+ * - `get(value)`: Fetches file content as text using browser `fetch()`.
45
+ */
46
+ function createBrowserFetchPlugin(baseUrl) {
47
+ const base = new URL(baseUrl || pageUrl(), pageUrl());
48
+
49
+ return {
50
+ // Determines if the reference should be fetched by this plugin (ignores inlined JSON/YAML and '#/...' local refs)
51
+ check(value) {
52
+ if (typeof value !== "string" || value.startsWith("#")) {
53
+ return false;
54
+ }
55
+ return isUrlLike(value);
56
+ },
57
+ // Resolves relative $ref paths (e.g. '../common/models.yaml') against their parent document URL
58
+ resolvePath(parentPath, reference) {
59
+ if (
60
+ typeof reference === "string" &&
61
+ (reference.startsWith("http://") || reference.startsWith("https://"))
62
+ ) {
63
+ return reference;
64
+ }
65
+ const isParentString = typeof parentPath === "string";
66
+ const parentUrl =
67
+ isParentString && (parentPath.startsWith("http://") || parentPath.startsWith("https://"))
68
+ ? new URL(parentPath)
69
+ : base;
70
+ return new URL(reference, parentUrl).href;
71
+ },
72
+ // Fetches the target URL via native fetch and returns the parsed spec or raw text
73
+ async get(value) {
74
+ const targetUrl =
75
+ value.startsWith("http://") || value.startsWith("https://")
76
+ ? value
77
+ : new URL(value, base).href;
78
+ const resp = await fetch(targetUrl);
79
+ if (!resp.ok) {
80
+ const error = new Error(`Failed to load ${targetUrl} (${resp.status})`);
81
+ error.status = resp.status;
82
+ error.statusCode = resp.status;
83
+ error.response = resp;
84
+ error.url = targetUrl;
85
+ throw error;
86
+ }
87
+ const text = await resp.text();
88
+ try {
89
+ return JSON.parse(text);
90
+ } catch {
91
+ try {
92
+ return parseYaml(text, { maxAliasCount: -1, merge: true });
93
+ } catch {
94
+ return text;
95
+ }
96
+ }
97
+ },
98
+ };
99
+ }
100
+
101
+ function breakCircularRefs(root) {
102
+ const seen = new WeakMap();
103
+ const ancestors = new WeakSet();
104
+
105
+ function walk(node, activeRefs = new Set(), currentRef = null) {
106
+ if (!node || typeof node !== "object") {
107
+ return node;
108
+ }
109
+
110
+ const ref = node["x-ref"];
111
+ if (ref) {
112
+ if (activeRefs.has(ref)) {
113
+ return { $ref: ref };
114
+ }
115
+ }
116
+
117
+ // Cycle detection via object identity (in active ancestor chain)
118
+ if (ancestors.has(node)) {
119
+ return { $ref: ref || currentRef || "#/components/schemas/Circular" };
120
+ }
121
+
122
+ // Memoization for non-cycle nodes (preserves DAG sharing & linear O(V+E) time)
123
+ if (seen.has(node)) {
124
+ return seen.get(node);
125
+ }
126
+
127
+ ancestors.add(node);
128
+ let childActiveRefs = activeRefs;
129
+ if (ref) {
130
+ childActiveRefs = new Set(activeRefs);
131
+ childActiveRefs.add(ref);
132
+ }
133
+
134
+ let clone;
135
+ if (Array.isArray(node)) {
136
+ clone = [];
137
+ seen.set(node, clone);
138
+ for (let i = 0; i < node.length; i++) {
139
+ clone.push(walk(node[i], childActiveRefs, ref || currentRef));
140
+ }
141
+ } else {
142
+ clone = {};
143
+ seen.set(node, clone);
144
+ for (const key of Object.keys(node)) {
145
+ if (key === "x-ref") {
146
+ continue;
147
+ }
148
+ clone[key] = walk(node[key], childActiveRefs, ref || currentRef);
149
+ }
150
+ }
151
+
152
+ ancestors.delete(node);
153
+ return clone;
154
+ }
155
+
156
+ return walk(root);
157
+ }
158
+
159
+ export default async function ProcessSpec(
160
+ specUrl,
161
+ generateMissingTags = false,
162
+ sortTags = false,
163
+ sortSchemas = false,
164
+ sortEndpointsBy = "none",
165
+ attrApiKey = "",
166
+ attrApiKeyLocation = "",
167
+ attrApiKeyValue = "",
168
+ serverUrl = "",
169
+ matchPaths = "",
170
+ matchType = "",
171
+ removeEndpointsWithBadgeLabelAs = "",
172
+ showUnusedTags = false,
173
+ ) {
174
+ let jsonParsedSpec;
175
+ try {
176
+ this?.requestUpdate?.(); // important to show the initial loader
177
+
178
+ let inputSpec = specUrl;
179
+ let specLocation;
180
+ if (typeof specUrl === "string" && isUrlLike(specUrl)) {
181
+ // Resolved against the page once, so the spec and its relative $refs are fetched from the same base
182
+ // (passing the relative path on made it relative to itself: `a/z.yaml` was fetched as `a/a/z.yaml`)
183
+ try {
184
+ specLocation = new URL(specUrl.trim(), pageUrl()).href;
185
+ inputSpec = specLocation;
186
+ } catch {
187
+ specLocation = specUrl;
188
+ }
189
+ } else if (typeof specUrl === "string") {
190
+ try {
191
+ inputSpec = JSON.parse(specUrl);
192
+ } catch {
193
+ try {
194
+ inputSpec = parseYaml(specUrl, { maxAliasCount: -1, merge: true });
195
+ } catch {
196
+ inputSpec = specUrl;
197
+ }
198
+ }
199
+ }
200
+
201
+ const fetchPlugin = createBrowserFetchPlugin(specLocation);
202
+ const loaded = await load(inputSpec, { plugins: [fetchPlugin] });
203
+
204
+ if (loaded.errors && loaded.errors.length > 0 && !loaded.specification) {
205
+ const firstErr = loaded.errors[0];
206
+ const error = new Error(firstErr.message || "Error loading specification");
207
+ error.status = 404;
208
+ throw error;
209
+ }
210
+
211
+ const dereferenceOptions = {
212
+ onDereference: ({ schema: dereferencedSchema, ref }) => {
213
+ dereferencedSchema["x-ref"] = ref;
214
+ },
215
+ };
216
+
217
+ let schema;
218
+ if (loaded.specification?.swagger && String(loaded.specification.swagger).startsWith("2")) {
219
+ const upgraded = upgrade(loaded.filesystem);
220
+ const derefResult = await dereference(upgraded.specification, dereferenceOptions);
221
+ schema = breakCircularRefs(derefResult.schema);
222
+ } else {
223
+ const derefResult = await dereference(loaded.filesystem, dereferenceOptions);
224
+ schema = breakCircularRefs(derefResult.schema);
225
+ }
226
+
227
+ await sleep(0); // important to show the initial loader (allows for rendering updates)
228
+
229
+ // If RapiDoc or RapiDocMini
230
+ if (
231
+ schema &&
232
+ (schema.components || schema.info || schema.servers || schema.tags || schema.paths)
233
+ ) {
234
+ jsonParsedSpec = filterPaths(schema, matchPaths, matchType, removeEndpointsWithBadgeLabelAs);
235
+ this?.dispatchEvent?.(new CustomEvent("before-render", { detail: { spec: jsonParsedSpec } }));
236
+ } else {
237
+ console.info("RapiDoc: %c Invalid Spec Definition %o ", "color:orangered", schema);
238
+ return {
239
+ specLoadError: true,
240
+ isSpecLoading: false,
241
+ info: {
242
+ title: "Invalid Spec Definition",
243
+ description: "Invalid Spec Definition",
244
+ version: "",
245
+ },
246
+ tags: [],
247
+ tagGroups: [],
248
+ };
249
+ }
250
+ } catch (err) {
251
+ let errDescr;
252
+ console.info(
253
+ "RapiDoc: %c There was an issue while loading/parsing the spec %o ",
254
+ "color:orangered",
255
+ err,
256
+ );
257
+ if (err.statusCode === 404 || err.status === 404) {
258
+ errDescr =
259
+ err.response?.url || err.url
260
+ ? `${err.response?.url || err.url} ┃ Not Found (${err.response?.status || err.status})`
261
+ : "Spec Not Found ┃ 404";
262
+ } else {
263
+ errDescr = `Unable to load ${err.response?.url || err.url || (typeof specUrl === "string" ? specUrl : "")} ┃ ${err.response?.status || err.status || err.message}`;
264
+ }
265
+ return {
266
+ specLoadError: true,
267
+ isSpecLoading: false,
268
+ info: {
269
+ title: "Error loading the spec",
270
+ description: errDescr,
271
+ version: "",
272
+ },
273
+ tags: [],
274
+ tagGroups: [],
275
+ };
276
+ }
277
+
278
+ // Everything after loading is also guarded: a spec that parses but trips the grouping code
279
+ // (a server without `url`, a tag without `name`, ...) still yields a `specLoadError` result
280
+ try {
281
+ const hasTagGroups = Boolean(
282
+ (jsonParsedSpec?.["x-tagGroups"] && jsonParsedSpec["x-tagGroups"].length > 0) ||
283
+ (jsonParsedSpec?.["x-tag-groups"] && jsonParsedSpec["x-tag-groups"].length > 0),
284
+ );
285
+
286
+ // Tags with Paths and WebHooks
287
+ const tags = groupByTags(
288
+ jsonParsedSpec,
289
+ sortEndpointsBy,
290
+ generateMissingTags,
291
+ sortTags,
292
+ hasTagGroups,
293
+ showUnusedTags,
294
+ );
295
+
296
+ // 3-Level Tag Groups (x-tagGroups / x-tag-groups)
297
+ const tagGroups = groupByTagGroups(jsonParsedSpec, tags);
298
+
299
+ // Components
300
+ const components = getComponents(jsonParsedSpec, sortSchemas);
301
+
302
+ // Info Description Headers
303
+ const infoDescriptionHeaders = jsonParsedSpec.info?.description
304
+ ? getHeadersFromMarkdown(jsonParsedSpec.info.description)
305
+ : [];
306
+
307
+ // Security Scheme
308
+ const securitySchemes = [];
309
+ if (jsonParsedSpec.components?.securitySchemes) {
310
+ const securitySchemeSet = new Set();
311
+ Object.entries(jsonParsedSpec.components.securitySchemes).forEach((kv) => {
312
+ if (!securitySchemeSet.has(kv[0])) {
313
+ securitySchemeSet.add(kv[0]);
314
+ const securityObj = { securitySchemeId: kv[0], ...kv[1] };
315
+ securityObj.in = "header";
316
+ securityObj.name = "Authorization"; // Name of the header/cookie/api-key
317
+ securityObj.nameId = kv[1].name || kv[0]; // Name of the security-scheme
318
+ securityObj.user = "";
319
+ securityObj.password = "";
320
+ securityObj.clientId = "";
321
+ securityObj.clientSecret = "";
322
+ securityObj.value = "";
323
+ securityObj.finalKeyValue = "";
324
+ if (kv[1].type === "apiKey") {
325
+ securityObj.in = kv[1].in || "header";
326
+ securityObj.name = kv[1].name || "Authorization";
327
+ }
328
+ securitySchemes.push(securityObj);
329
+ }
330
+ });
331
+ }
332
+
333
+ if (attrApiKey && attrApiKeyLocation && attrApiKeyValue) {
334
+ securitySchemes.push({
335
+ securitySchemeId: rapidocApiKey,
336
+ description: "api-key provided in rapidoc element attributes",
337
+ type: "apiKey",
338
+ oAuthFlow: "",
339
+ name: attrApiKey, // Name of the header/cookie/api-key
340
+ nameId: attrApiKey, // Name of the security-scheme
341
+ in: attrApiKeyLocation,
342
+ value: attrApiKeyValue,
343
+ finalKeyValue: attrApiKeyValue,
344
+ });
345
+ }
346
+
347
+ // Updated Security Type Display Text based on Type
348
+ securitySchemes.forEach((v) => {
349
+ if (v.type === "http") {
350
+ v.typeDisplay = v.scheme === "basic" ? "HTTP Basic" : `HTTP Bearer ${v.nameId}`;
351
+ } else if (v.type === "apiKey") {
352
+ v.typeDisplay = `API Key (${v.name})`;
353
+ } else if (v.type === "oauth2") {
354
+ v.typeDisplay = `OAuth (${v.securitySchemeId})`;
355
+ } else {
356
+ v.typeDisplay = v.type || "None";
357
+ }
358
+ });
359
+
360
+ // Servers
361
+ let servers = [];
362
+ if (
363
+ jsonParsedSpec.servers &&
364
+ Array.isArray(jsonParsedSpec.servers) &&
365
+ jsonParsedSpec.servers.length > 0
366
+ ) {
367
+ jsonParsedSpec.servers.forEach((v) => {
368
+ let computedUrl = String(v.url ?? "").trim();
369
+ if (
370
+ !(
371
+ computedUrl.startsWith("http") ||
372
+ computedUrl.startsWith("//") ||
373
+ computedUrl.startsWith("{")
374
+ )
375
+ ) {
376
+ if (typeof window !== "undefined" && window.location?.origin?.startsWith("http")) {
377
+ v.url = window.location.origin + v.url;
378
+ computedUrl = v.url;
379
+ }
380
+ }
381
+ // Apply server-variables to generate final computed-url
382
+ if (v.variables) {
383
+ Object.entries(v.variables).forEach((kv) => {
384
+ const regex = new RegExp(`{${kv[0]}}`, "g");
385
+ computedUrl = computedUrl.replace(regex, kv[1].default || "");
386
+ kv[1].value = kv[1].default || "";
387
+ });
388
+ }
389
+ v.computedUrl = computedUrl;
390
+ });
391
+ if (serverUrl) {
392
+ jsonParsedSpec.servers.push({ url: serverUrl, computedUrl: serverUrl });
393
+ }
394
+ } else if (serverUrl) {
395
+ jsonParsedSpec.servers = [{ url: serverUrl, computedUrl: serverUrl }];
396
+ } else if (typeof window !== "undefined" && window.location?.origin?.startsWith("http")) {
397
+ jsonParsedSpec.servers = [
398
+ { url: window.location.origin, computedUrl: window.location.origin },
399
+ ];
400
+ } else {
401
+ jsonParsedSpec.servers = [{ url: "http://localhost", computedUrl: "http://localhost" }];
402
+ }
403
+ servers = jsonParsedSpec.servers;
404
+ const parsedSpec = {
405
+ specLoadError: false,
406
+ isSpecLoading: false,
407
+ info: jsonParsedSpec.info,
408
+ infoDescriptionHeaders,
409
+ tags,
410
+ tagGroups,
411
+ components,
412
+ externalDocs: jsonParsedSpec.externalDocs,
413
+ securitySchemes,
414
+ servers,
415
+ };
416
+ return parsedSpec;
417
+ } catch (err) {
418
+ console.info(
419
+ "RapiDoc: %c There was an issue while processing the spec %o ",
420
+ "color:orangered",
421
+ err,
422
+ );
423
+ return {
424
+ specLoadError: true,
425
+ isSpecLoading: false,
426
+ info: {
427
+ title: "Error processing the spec",
428
+ description: `Unable to process the spec ┃ ${err?.message || err}`,
429
+ version: "",
430
+ },
431
+ tags: [],
432
+ tagGroups: [],
433
+ };
434
+ }
435
+ }
436
+
437
+ function filterPaths(
438
+ openApiObject,
439
+ matchPaths = "",
440
+ matchType = "",
441
+ removeEndpointsWithBadgeLabelAs = "",
442
+ ) {
443
+ const filteredPaths = {};
444
+
445
+ // Convert the removePathsWithBadgeLabeledAs to an array if provided
446
+ const labelsToRemove = removeEndpointsWithBadgeLabelAs
447
+ .split(",")
448
+ .map((label) => label.trim().toLowerCase())
449
+ .filter(Boolean);
450
+
451
+ // Helper function to check if a path should be included based on matchPaths
452
+ function pathMatches(pathsKey, httpMethod) {
453
+ if (!matchPaths) {
454
+ return true; // If no matchPaths provided, include everything
455
+ }
456
+ const fullPath = `${httpMethod} ${pathsKey}`.toLowerCase(); // Construct "method path" string
457
+ if (matchType === "regex") {
458
+ const matchPathsRegex = new RegExp(matchPaths, "i");
459
+ return matchPathsRegex.test(fullPath);
460
+ }
461
+ return fullPath.includes(matchPaths.toLowerCase());
462
+ }
463
+
464
+ // Helper function to check if the badges contain any label that needs to be removed
465
+ function containsLabelToRemove(badges) {
466
+ return badges.some((badge) => labelsToRemove.includes(badge?.label.toLowerCase()));
467
+ }
468
+
469
+ // Loop through the paths in the openApiObject
470
+ Object.entries(openApiObject.paths).forEach(([pathsKey, methods]) => {
471
+ const filteredMethods = {};
472
+
473
+ Object.entries(methods).forEach(([httpMethod, methodDetails]) => {
474
+ if (
475
+ httpMethod === "additionalOperations" &&
476
+ typeof methodDetails === "object" &&
477
+ methodDetails !== null
478
+ ) {
479
+ const filteredAdditional = {};
480
+ Object.entries(methodDetails).forEach(([customMethod, customDetails]) => {
481
+ const badges = customDetails?.["x-badges"];
482
+ if (pathMatches(pathsKey, customMethod)) {
483
+ if (badges && Array.isArray(badges)) {
484
+ if (!containsLabelToRemove(badges)) {
485
+ filteredAdditional[customMethod] = customDetails;
486
+ }
487
+ } else {
488
+ filteredAdditional[customMethod] = customDetails;
489
+ }
490
+ }
491
+ });
492
+ if (Object.keys(filteredAdditional).length > 0) {
493
+ filteredMethods.additionalOperations = filteredAdditional;
494
+ }
495
+ return;
496
+ }
497
+
498
+ if (["parameters", "servers", "summary", "description"].includes(httpMethod)) {
499
+ filteredMethods[httpMethod] = methodDetails;
500
+ return;
501
+ }
502
+
503
+ const badges = methodDetails?.["x-badges"];
504
+
505
+ // Filter by matchPaths
506
+ if (pathMatches(pathsKey, httpMethod)) {
507
+ if (badges && Array.isArray(badges)) {
508
+ // Filter out based on removePathsWithBadgeLabeledAs
509
+ if (!containsLabelToRemove(badges)) {
510
+ filteredMethods[httpMethod] = methodDetails;
511
+ }
512
+ } else {
513
+ // No badges present, include the method
514
+ filteredMethods[httpMethod] = methodDetails;
515
+ }
516
+ }
517
+ });
518
+
519
+ if (Object.keys(filteredMethods).length > 0) {
520
+ filteredPaths[pathsKey] = filteredMethods;
521
+ }
522
+ });
523
+
524
+ openApiObject.paths = filteredPaths;
525
+ return openApiObject;
526
+ }
527
+
528
+ function getHeadersFromMarkdown(markdownContent) {
529
+ const tokens = marked.lexer(markdownContent);
530
+ const headers = tokens.filter((v) => v.type === "heading" && v.depth <= 2);
531
+ return headers || [];
532
+ }
533
+
534
+ function getComponents(openApiSpec, sortSchemas = false) {
535
+ if (!openApiSpec.components) {
536
+ return [];
537
+ }
538
+ const components = [];
539
+ for (const component in openApiSpec.components) {
540
+ const subComponents = [];
541
+ for (const sComponent in openApiSpec.components[component]) {
542
+ const scmp = {
543
+ show: true,
544
+ id: `${component.toLowerCase()}-${sComponent.toLowerCase()}`.replace(
545
+ invalidCharsRegEx,
546
+ "-",
547
+ ),
548
+ name: sComponent,
549
+ component: openApiSpec.components[component][sComponent],
550
+ };
551
+ subComponents.push(scmp);
552
+ }
553
+
554
+ let cmpDescription = component;
555
+ let cmpName = component;
556
+
557
+ switch (component) {
558
+ case "schemas":
559
+ if (sortSchemas) {
560
+ subComponents.sort((c1, c2) => c1.name.localeCompare(c2.name));
561
+ }
562
+
563
+ cmpName = "Schemas";
564
+ cmpDescription =
565
+ "Schemas allows the definition of input and output data types. These types can be objects, but also primitives and arrays.";
566
+ break;
567
+ case "responses":
568
+ cmpName = "Responses";
569
+ cmpDescription =
570
+ "Describes responses from an API Operation, including design-time, static links to operations based on the response.";
571
+ break;
572
+ case "parameters":
573
+ cmpName = "Parameters";
574
+ cmpDescription =
575
+ "Describes operation parameters. A unique parameter is defined by a combination of a name and location.";
576
+ break;
577
+ case "examples":
578
+ cmpName = "Examples";
579
+ cmpDescription =
580
+ "List of Examples for operations, can be requests, responses and objects examples.";
581
+ break;
582
+ case "requestBodies":
583
+ cmpName = "Request Bodies";
584
+ cmpDescription = "Describes common request bodies that are used across the API operations.";
585
+ break;
586
+ case "headers":
587
+ cmpName = "Headers";
588
+ cmpDescription =
589
+ 'Headers follows the structure of the Parameters but they are explicitly in "header"';
590
+ break;
591
+ case "securitySchemes":
592
+ cmpName = "Security Schemes";
593
+
594
+ cmpDescription =
595
+ "Defines a security scheme that can be used by the operations. Supported schemes are HTTP authentication, an API key (either as a header, a cookie parameter or as a query parameter), OAuth2's common flows(implicit, password, client credentials and authorization code) as defined in RFC6749, and OpenID Connect Discovery.";
596
+ break;
597
+ case "links":
598
+ cmpName = "Links";
599
+ cmpDescription =
600
+ "Links represent a possible design-time link for a response. The presence of a link does not guarantee the caller's ability to successfully invoke it, rather it provides a known relationship and traversal mechanism between responses and other operations.";
601
+ break;
602
+ case "callbacks":
603
+ cmpName = "Callbacks";
604
+
605
+ cmpDescription =
606
+ "A map of possible out-of band callbacks related to the parent operation. Each value in the map is a Path Item Object that describes a set of requests that may be initiated by the API provider and the expected responses. The key value used to identify the path item object is an expression, evaluated at runtime, that identifies a URL to use for the callback operation.";
607
+ break;
608
+ default:
609
+ cmpName = component;
610
+ cmpDescription = component;
611
+ break;
612
+ }
613
+
614
+ const cmp = {
615
+ show: true,
616
+ name: cmpName,
617
+ description: cmpDescription,
618
+ subComponents,
619
+ };
620
+ components.push(cmp);
621
+ }
622
+
623
+ return components || [];
624
+ }
625
+
626
+ export function isTagExpanded(obj) {
627
+ if (!obj) {
628
+ return true;
629
+ }
630
+ const val = obj["x-tag-expanded"] ?? obj["x-tagExpanded"] ?? obj["x-expanded"] ?? obj.expanded;
631
+ if (val === false || val === "false") {
632
+ return false;
633
+ }
634
+ return true;
635
+ }
636
+
637
+ function groupByTags(
638
+ openApiSpec,
639
+ sortEndpointsBy = "none",
640
+ generateMissingTags = false,
641
+ sortTags = false,
642
+ hasTagGroups = false,
643
+ showUnusedTags = false,
644
+ ) {
645
+ const supportedMethods = [
646
+ "get",
647
+ "put",
648
+ "post",
649
+ "delete",
650
+ "patch",
651
+ "head",
652
+ "options",
653
+ "trace",
654
+ "query",
655
+ ]; // this is also used for ordering endpoints by methods
656
+ const slugger = new OperationSlugger();
657
+
658
+ const tags =
659
+ openApiSpec.tags && Array.isArray(openApiSpec.tags) && openApiSpec.tags.length > 0
660
+ ? openApiSpec.tags.map((v) => ({
661
+ show: true,
662
+ elementId: slugger.slugTag(v.name),
663
+ name: v.name,
664
+ displayName: v["x-displayName"] || v.name,
665
+ description: v.description || "",
666
+ headers: v.description ? getHeadersFromMarkdown(v.description) : [],
667
+ paths: [],
668
+ expanded: isTagExpanded(v),
669
+ xInternal: v["x-internal"] ?? undefined,
670
+ xHidden: v["x-hidden"] ?? undefined,
671
+ }))
672
+ : [];
673
+
674
+ // Paths first, then webhooks; a webhook named like a path does not replace it and the spec is not mutated
675
+ const pathsAndWebhooks = [
676
+ ...Object.entries(openApiSpec.paths || {}).map(([name, item]) => [name, item, false]),
677
+ ...Object.entries(openApiSpec.webhooks || {}).map(([name, item]) => [name, item, true]),
678
+ ];
679
+ // For each path find the tag and push it into the corresponding tag
680
+ for (const [pathOrHookName, pathItem, isWebhook] of pathsAndWebhooks) {
681
+ const commonParams = pathItem.parameters;
682
+ const commonPathProp = {
683
+ servers: pathItem.servers || [],
684
+ parameters: pathItem.parameters || [],
685
+ };
686
+ const operationsToProcess = [];
687
+ supportedMethods.forEach((methodName) => {
688
+ if (pathItem[methodName]) {
689
+ operationsToProcess.push({
690
+ methodName,
691
+ pathOrHookObj: pathItem[methodName],
692
+ });
693
+ }
694
+ });
695
+
696
+ if (pathItem.additionalOperations && typeof pathItem.additionalOperations === "object") {
697
+ Object.entries(pathItem.additionalOperations).forEach(([customMethod, customObj]) => {
698
+ if (customObj && typeof customObj === "object") {
699
+ operationsToProcess.push({
700
+ methodName: customMethod.toLowerCase(),
701
+ pathOrHookObj: customObj,
702
+ });
703
+ }
704
+ });
705
+ }
706
+
707
+ operationsToProcess.forEach(({ methodName, pathOrHookObj }) => {
708
+ // If path.methods are tagged, else generate it from path
709
+ const pathTags = pathOrHookObj.tags || [];
710
+ if (pathTags.length === 0) {
711
+ if (generateMissingTags) {
712
+ const pathOrHookNameKey = pathOrHookName.replace(/^\/+|\/+$/g, "");
713
+ const firstWordEndIndex = pathOrHookNameKey.indexOf("/");
714
+ if (firstWordEndIndex === -1) {
715
+ pathTags.push(pathOrHookNameKey);
716
+ } else {
717
+ // firstWordEndIndex -= 1;
718
+ pathTags.push(pathOrHookNameKey.substring(0, firstWordEndIndex));
719
+ }
720
+ } else {
721
+ pathTags.push("General");
722
+ }
723
+ }
724
+
725
+ pathTags.forEach((tag) => {
726
+ let tagObj;
727
+ let specTagsItem;
728
+
729
+ if (openApiSpec.tags) {
730
+ specTagsItem = openApiSpec.tags.find((v) => v.name?.toLowerCase() === tag.toLowerCase());
731
+ }
732
+
733
+ tagObj = tags.find((v) => v.name === tag);
734
+ if (!tagObj) {
735
+ tagObj = {
736
+ show: true,
737
+ elementId: slugger.slugTag(tag),
738
+ name: tag,
739
+ displayName: specTagsItem?.["x-displayName"] || specTagsItem?.name || tag,
740
+ description: specTagsItem?.description || "",
741
+ headers: specTagsItem?.description
742
+ ? getHeadersFromMarkdown(specTagsItem.description)
743
+ : [],
744
+ paths: [],
745
+ expanded: isTagExpanded(specTagsItem),
746
+ xInternal: specTagsItem?.["x-internal"] ?? undefined,
747
+ xHidden: specTagsItem?.["x-hidden"] ?? undefined,
748
+ };
749
+ tags.push(tagObj);
750
+ }
751
+
752
+ // Generate a short summary which is broken
753
+ let shortSummary = (
754
+ pathOrHookObj.summary ||
755
+ pathOrHookObj.description ||
756
+ `${methodName.toUpperCase()} ${pathOrHookName}`
757
+ ).trim();
758
+ if (shortSummary.length > 100) {
759
+ [shortSummary] = shortSummary.split(/[.|!|?]\s|[\r?\n]/); // take the first line (period or carriage return)
760
+ }
761
+ // Merge Common Parameters with This methods parameters
762
+ let finalParameters = [];
763
+ if (commonParams) {
764
+ if (pathOrHookObj.parameters) {
765
+ finalParameters = commonParams
766
+ .filter((commonParam) => {
767
+ if (
768
+ !pathOrHookObj.parameters.some(
769
+ (param) => commonParam.name === param.name && commonParam.in === param.in,
770
+ )
771
+ ) {
772
+ return commonParam;
773
+ }
774
+ })
775
+ .concat(pathOrHookObj.parameters);
776
+ } else {
777
+ finalParameters = commonParams.slice(0);
778
+ }
779
+ } else {
780
+ finalParameters = pathOrHookObj.parameters ? pathOrHookObj.parameters.slice(0) : [];
781
+ }
782
+
783
+ // Filter callbacks to contain only objects.
784
+ if (pathOrHookObj.callbacks) {
785
+ for (const [callbackName, callbackConfig] of Object.entries(pathOrHookObj.callbacks)) {
786
+ const filteredCallbacks =
787
+ Object.entries(callbackConfig).filter((entry) => typeof entry[1] === "object") || [];
788
+ pathOrHookObj.callbacks[callbackName] = Object.fromEntries(filteredCallbacks);
789
+ }
790
+ }
791
+
792
+ const cleanPath = (pathOrHookName || "")
793
+ .replace(/[{}]/g, "")
794
+ .replace(/[^a-zA-Z0-9]+/g, "-")
795
+ .replace(/^-+|-+$/g, "");
796
+ const fallbackOpId = `${methodName}${toPascalCase(cleanPath)}`;
797
+ const operationId = pathOrHookObj.operationId || fallbackOpId;
798
+ const tagSlug = hasTagGroups ? toKebabCase(tag) : "";
799
+ const elementId = slugger.slug(
800
+ {
801
+ method: methodName,
802
+ path: pathOrHookName,
803
+ operationId: pathOrHookObj.operationId,
804
+ summary: pathOrHookObj.summary || pathOrHookObj.description,
805
+ },
806
+ tagSlug,
807
+ );
808
+
809
+ // Update Responses
810
+ tagObj.paths.push({
811
+ show: true,
812
+ expanded: false,
813
+ isWebhook,
814
+ expandedAtLeastOnce: false,
815
+ summary: pathOrHookObj.summary || "",
816
+ description: pathOrHookObj.description || "",
817
+ externalDocs: pathOrHookObj.externalDocs,
818
+ shortSummary,
819
+ method: methodName,
820
+ path: pathOrHookName,
821
+ operationId,
822
+ elementId,
823
+ servers: pathOrHookObj.servers
824
+ ? commonPathProp.servers.concat(pathOrHookObj.servers)
825
+ : commonPathProp.servers,
826
+ parameters: finalParameters,
827
+ requestBody: pathOrHookObj.requestBody,
828
+ responses: pathOrHookObj.responses,
829
+ callbacks: pathOrHookObj.callbacks,
830
+ deprecated: pathOrHookObj.deprecated,
831
+ security: pathOrHookObj.security ?? openApiSpec.security,
832
+ // commonSummary: commonPathProp.summary,
833
+ // commonDescription: commonPathProp.description,
834
+ xBadges: pathOrHookObj["x-badges"] || undefined,
835
+ xCodeSamples: pathOrHookObj["x-codeSamples"] || pathOrHookObj["x-code-samples"] || "",
836
+ rawOperation: pathOrHookObj,
837
+ xInternal: pathOrHookObj["x-internal"] ?? pathItem?.["x-internal"] ?? undefined,
838
+ xHidden: pathOrHookObj["x-hidden"] ?? pathItem?.["x-hidden"] ?? undefined,
839
+ });
840
+ }); // End of tag path create
841
+ }); // End of operationsToProcess
842
+ }
843
+
844
+ // A tag no operation uses is dropped, unless it has a description to show (a guide or concepts section) and
845
+ // either show-unused-tags is on or the spec lists the tag in x-tagGroups. Such a tag is marked descriptionOnly.
846
+ const tagNamesInGroups = new Set(
847
+ (openApiSpec["x-tagGroups"] || openApiSpec["x-tag-groups"] || [])
848
+ .flatMap((g) => (Array.isArray(g?.tags) ? g.tags : []))
849
+ .filter((name) => typeof name === "string")
850
+ .map((name) => name.toLowerCase()),
851
+ );
852
+ const tagsWithSortedPaths = tags.filter((tag) => {
853
+ if (tag.paths && tag.paths.length > 0) {
854
+ return true;
855
+ }
856
+ if (tag.description && (showUnusedTags || tagNamesInGroups.has(tag.name?.toLowerCase()))) {
857
+ tag.descriptionOnly = true;
858
+ return true;
859
+ }
860
+ return false;
861
+ });
862
+ tagsWithSortedPaths.forEach((tag) => {
863
+ if (sortEndpointsBy !== "none") {
864
+ if (sortEndpointsBy === "method") {
865
+ tag.paths.sort((a, b) => {
866
+ const idxA = supportedMethods.indexOf(a.method);
867
+ const idxB = supportedMethods.indexOf(b.method);
868
+ if (idxA !== -1 && idxB !== -1) {
869
+ return idxA - idxB;
870
+ }
871
+ if (idxA !== -1) {
872
+ return -1;
873
+ }
874
+ if (idxB !== -1) {
875
+ return 1;
876
+ }
877
+ return a.method.localeCompare(b.method);
878
+ });
879
+ } else if (sortEndpointsBy === "summary") {
880
+ tag.paths.sort((a, b) => a.shortSummary.localeCompare(b.shortSummary));
881
+ } else if (sortEndpointsBy === "path") {
882
+ tag.paths.sort((a, b) => a.path.localeCompare(b.path));
883
+ }
884
+ }
885
+ tag.firstPathId = tag.paths[0]?.elementId || "";
886
+ });
887
+ return sortTags
888
+ ? tagsWithSortedPaths.sort((a, b) => a.name.localeCompare(b.name))
889
+ : tagsWithSortedPaths;
890
+ }
891
+
892
+ /**
893
+ * Organizes parsed tags into 3-tier tag groups based on the `x-tagGroups` or `x-tag-groups` vendor extension.
894
+ * If the extension is absent, returns an empty array to maintain backward-compatible 2-tier navigation.
895
+ *
896
+ * @param {object} openApiSpec - The root OpenAPI specification object.
897
+ * @param {Array<object>} tags - The processed tags array returned by groupByTags.
898
+ * @returns {Array<object>} Array of tag group objects, or empty array if no tag groups are defined.
899
+ */
900
+ export function groupByTagGroups(openApiSpec, tags = []) {
901
+ const rawGroups =
902
+ openApiSpec?.["x-tagGroups"] || openApiSpec?.["x-tag-groups"] || openApiSpec?.["x-taggroups"];
903
+ if (!rawGroups || !Array.isArray(rawGroups) || rawGroups.length === 0) {
904
+ return [];
905
+ }
906
+
907
+ const slugger = new OperationSlugger();
908
+ const assignedTagNames = new Set();
909
+ const groups = [];
910
+
911
+ rawGroups.forEach((g) => {
912
+ if (!g || !g.name) return;
913
+ const targetTagNames = Array.isArray(g.tags) ? g.tags : [];
914
+ const groupTags = [];
915
+ targetTagNames.forEach((tName) => {
916
+ if (typeof tName !== "string") return;
917
+ const foundTag = tags.find(
918
+ (t) => t.name === tName || t.name.toLowerCase() === tName.toLowerCase(),
919
+ );
920
+ if (foundTag && !groupTags.includes(foundTag)) {
921
+ groupTags.push(foundTag);
922
+ assignedTagNames.add(foundTag.name);
923
+ }
924
+ });
925
+
926
+ if (groupTags.length > 0) {
927
+ groups.push({
928
+ name: g.name,
929
+ displayName: g["x-displayName"] || g.displayName || g.name,
930
+ description: g.description || "",
931
+ elementId: slugger.slugTagGroup(g.name),
932
+ tags: groupTags,
933
+ expanded: isTagExpanded(g),
934
+ xInternal: g["x-internal"] ?? undefined,
935
+ xHidden: g["x-hidden"] ?? undefined,
936
+ });
937
+ }
938
+ });
939
+
940
+ return groups;
941
+ }