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