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
package/src/rapidoc.ts ADDED
@@ -0,0 +1,1854 @@
1
+ import { LitElement, noChange } from "lit";
2
+ import { customElement, property } from "lit/decorators.js";
3
+ import { fixRenderedAnchorLinks } from "~/utils/markdown-utils";
4
+ import { marked } from "marked";
5
+ import Slugger from "github-slugger";
6
+ import { scheduleHighlight } from "~/utils/highlighter";
7
+
8
+ // Styles
9
+ import FontStyles from "~/styles/font-styles";
10
+ import InputStyles from "~/styles/input-styles";
11
+ import FlexStyles from "~/styles/flex-styles";
12
+ import AuthStyles from "~/styles/auth-styles";
13
+ import ParamListStyles from "~/styles/param-list-styles";
14
+ import EndpointStyles from "~/styles/endpoint-styles";
15
+ import MicrolighterStyles from "~/styles/microlighter-styles";
16
+ import TabStyles from "~/styles/tab-styles";
17
+ import NavStyles from "~/styles/nav-styles";
18
+ import InfoStyles from "~/styles/info-styles";
19
+ import CustomStyles from "~/styles/custom-styles";
20
+ import DialogBoxStyles from "~/styles/dialog-box-styles";
21
+ import LayoutStyles from "~/styles/layout-styles";
22
+ import ThemeTokens, {
23
+ DEFAULT_THEME,
24
+ applyCustomTheme,
25
+ isValidHexColor,
26
+ normalizeTheme,
27
+ } from "~/styles/theme-tokens";
28
+
29
+ import {
30
+ advancedSearch,
31
+ getMatchedPaths,
32
+ getMatchedComponents,
33
+ rapidocApiKey,
34
+ sleep,
35
+ } from "~/utils/common-utils";
36
+ import ProcessSpec from "~/utils/spec-parser";
37
+ import { enableMockServer, disableMockServer, updateMockConfig } from "~/utils/mock-interceptor";
38
+ import mainBodyTemplate from "~/templates/main-body-template";
39
+ import { expandParentNavContainers } from "~/templates/navbar-template";
40
+ import { applyApiKey, onClearAllApiKeys } from "~/templates/security-scheme-template";
41
+ import { setApiServer } from "~/templates/server-template";
42
+ import {
43
+ generateMcpServer as mcpServerGenerator,
44
+ buildMcpManifest,
45
+ estimateTokenCost,
46
+ parseMcpEnabled,
47
+ createZip,
48
+ } from "~/utils/mcp-generator";
49
+ import { bindNavBar } from "~/utils/bind-navbar";
50
+ import { methodPathSlug } from "~/utils/slugger";
51
+ import { AutoColorSchemeController } from "~/utils/auto-color-scheme";
52
+
53
+ const paramsConverter = {
54
+ fromAttribute: (attr) => {
55
+ if (!attr) {
56
+ return null;
57
+ }
58
+ const trimmed = attr.trim();
59
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
60
+ try {
61
+ return JSON.parse(trimmed);
62
+ } catch {
63
+ return trimmed;
64
+ }
65
+ }
66
+ return trimmed;
67
+ },
68
+ toAttribute: (prop) => {
69
+ if (typeof prop === "object" && prop !== null) {
70
+ return JSON.stringify(prop);
71
+ }
72
+ return prop;
73
+ },
74
+ };
75
+
76
+ // Input that marks the reader's first interaction with the page (see getContentScrollBehavior)
77
+ const INTERACTION_EVENTS = ["pointerdown", "keydown", "wheel", "touchstart"];
78
+
79
+ @customElement("rapi-doc")
80
+ export default class RapiDoc extends LitElement {
81
+ constructor() {
82
+ super();
83
+ this.showSummaryWhenCollapsed = true;
84
+ this._intersectingElements = new Map();
85
+ // Will activate intersection observer only after spec load and hash analyze
86
+ // to scroll to the proper element without being reverted by observer behavior
87
+ this.isIntersectionObserverActive = false;
88
+ this._userSetLayout = false;
89
+ this._isDefaultingLayout = false;
90
+ this._observerLockTimeout = null;
91
+ this._observerLockCleanup = null;
92
+ // Set on the reader's first pointer, key, wheel or touch input (see getContentScrollBehavior)
93
+ this._readerHasInteracted = false;
94
+ this._onFirstInteraction = null;
95
+ this._onHashChange = null;
96
+ // Spec loads carry a token; only the latest load may apply its result (see loadSpec)
97
+ this._specLoadSeq = 0;
98
+ // The spec-url attribute value whose load has been handled, or that an inlined spec superseded
99
+ this._lastSpecUrlRequested = null;
100
+ this.initIntersectionObserver();
101
+ }
102
+
103
+ // Heading
104
+ @property({ type: String, attribute: "heading-text" }) headingText = "";
105
+ @property({ type: String, attribute: "goto-path" }) gotoPath = "";
106
+
107
+ // Spec
108
+ @property({ type: String, attribute: "update-route" }) updateRoute = "true";
109
+ @property({ type: String, attribute: "spec-url" }) specUrl = "";
110
+ @property({ type: String, attribute: "sort-tags" }) sortTags = "false";
111
+ @property({ type: String, attribute: "sort-schemas" }) sortSchemas = "false";
112
+ @property({ type: String, attribute: "generate-missing-tags" }) generateMissingTags = "false";
113
+ @property({ type: String, attribute: "show-unused-tags" }) showUnusedTags = "false";
114
+ @property({ type: String, attribute: "sort-endpoints-by" }) sortEndpointsBy = "path";
115
+ @property({ type: String, attribute: false }) specFile: any;
116
+
117
+ // UI Layouts
118
+ @property({ type: String, reflect: true }) layout = "row";
119
+ @property({ type: String, attribute: "render-style" }) renderStyle = "read";
120
+ @property({ type: String, attribute: "default-schema-tab" }) defaultSchemaTab = "example";
121
+ @property({ type: String, attribute: "response-area-height" }) responseAreaHeight = "400px";
122
+ @property({ type: String, attribute: "fill-request-fields-with-example" })
123
+ fillRequestFieldsWithExample = "true";
124
+ @property({ type: String, attribute: "persist-auth" }) persistAuth = "false";
125
+
126
+ // Schema Styles
127
+ @property({ type: String, attribute: "schema-style" }) schemaStyle = "tree";
128
+ @property({ type: Number, attribute: "schema-expand-level" }) schemaExpandLevel = 99999;
129
+ @property({ type: String, attribute: "schema-description-expanded" }) schemaDescriptionExpanded =
130
+ "false";
131
+ @property({ type: String, attribute: "schema-hide-read-only" }) schemaHideReadOnly = "true";
132
+ @property({ type: String, attribute: "schema-hide-write-only" }) schemaHideWriteOnly = "true";
133
+
134
+ // API Server
135
+ @property({ type: String, attribute: "api-key-name" }) apiKeyName = "";
136
+ @property({ type: String, attribute: "api-key-location" }) apiKeyLocation = "header";
137
+ @property({ type: String, attribute: "api-key-value" }) apiKeyValue = "-";
138
+ @property({ type: String, attribute: "default-api-server" }) defaultApiServerUrl = "";
139
+ @property({ type: String, attribute: "server-url" }) serverUrl = "";
140
+ @property({ type: String, attribute: "oauth-receiver" }) oauthReceiver = "oauth-receiver.html";
141
+ @property({ type: Object, attribute: "additional-authorize-params", converter: paramsConverter })
142
+ additionalAuthorizeParams: any;
143
+ @property({ type: Object, attribute: "additional-token-params", converter: paramsConverter })
144
+ additionalTokenParams: any;
145
+
146
+ // Mock Server
147
+ @property({ type: String, attribute: "mock-server" }) mockServer = "false";
148
+ // A string of codes for every operation, or a map by operation (`{ "POST /orders": "503, 200", "*": "200" }`)
149
+ @property({ type: String, attribute: "mock-server-status-code" }) mockServerStatusCode:
150
+ | string
151
+ | Record<string, string> = "200";
152
+ @property({ type: String, attribute: "mock-server-status-strategy" }) mockServerStatusStrategy =
153
+ "example";
154
+ @property({ type: Number, attribute: "mock-server-delay" }) mockServerDelay = 0;
155
+ @property({ type: String, attribute: "mock-server-log" }) mockServerLog = "false";
156
+
157
+ // Hide/Show Sections & Enable Disable actions
158
+ @property({ type: String, attribute: "show-header" }) showHeader = "true";
159
+ @property({ type: String, attribute: "show-side-nav" }) showSideNav = "true";
160
+ @property({ type: String, attribute: "show-info" }) showInfo = "true";
161
+ @property({ type: String, attribute: "allow-authentication" }) allowAuthentication = "true";
162
+ @property({ type: String, attribute: "allow-try" }) allowTry = "true";
163
+ @property({ type: String, attribute: "show-curl-before-try" }) showCurlBeforeTry = "false";
164
+ @property({ type: String, attribute: "allow-spec-url-load" }) allowSpecUrlLoad = "true";
165
+ @property({ type: String, attribute: "allow-spec-file-load" }) allowSpecFileLoad = "true";
166
+ @property({ type: String, attribute: "allow-spec-file-download" }) allowSpecFileDownload =
167
+ "false";
168
+ @property({ type: String, attribute: "allow-search" }) allowSearch = "true";
169
+ @property({ type: String, attribute: "allow-advanced-search" }) allowAdvancedSearch = "true";
170
+ @property({ type: String, attribute: "allow-server-selection" }) allowServerSelection = "true";
171
+ @property({ type: String, attribute: "allow-schema-description-expand-toggle" })
172
+ allowSchemaDescriptionExpandToggle = "true";
173
+ // "true" shows the Export MCP Server button; "none" or "false" hides it
174
+ @property({ type: String, attribute: "mcp" }) mcp = "false";
175
+ @property({ type: String, attribute: "mcp-server-name" }) mcpServerName = "";
176
+ @property({ type: String, attribute: "show-components" }) showComponents = "false";
177
+ @property({ type: String, attribute: "page-direction" }) pageDirection = "ltr";
178
+ @property({ type: String, attribute: "scroll-behavior" }) scrollBehavior = "smooth";
179
+ @property({ type: String, attribute: "auto-height" }) autoHeight = "false";
180
+ @property({ type: String, attribute: "scroll-container" }) scrollContainer = "";
181
+ @property({ type: String, attribute: "scroll-offset" }) scrollOffset = "";
182
+
183
+ // Main Colors and Font
184
+ @property({ type: String, reflect: true }) theme = DEFAULT_THEME;
185
+ @property({ type: String, attribute: "primary-color" }) primaryColor = "";
186
+ @property({ type: String, attribute: "color-scheme", reflect: true }) colorScheme = "dark";
187
+ // color-scheme="auto": follows the page's theme (see utils/auto-color-scheme.js)
188
+ _autoColorScheme = new AutoColorSchemeController(this);
189
+ @property({ type: String, reflect: true }) radius = "sm";
190
+ @property({ type: String, reflect: true }) scale = "default";
191
+ @property({ type: String, attribute: "css-file" }) cssFile = "";
192
+ @property({ type: String, attribute: "css-classes" }) cssClasses = "";
193
+
194
+ // Nav Bar
195
+ @property({ type: String, attribute: "nav-item-spacing", reflect: true }) navItemSpacing =
196
+ "default";
197
+ @property({ type: String, attribute: "nav-item-overflow", reflect: true }) navItemOverflow =
198
+ "ellipsis";
199
+ @property({ type: String, attribute: "show-method-in-nav-bar" }) showMethodInNavBar = "false";
200
+ @property({ type: String, attribute: "use-path-in-nav-bar" }) usePathInNavBar = "false";
201
+ @property({ type: String, attribute: "info-description-headings-in-navbar" })
202
+ infoDescriptionHeadingsInNavBar = "false";
203
+
204
+ // Fetch Options
205
+ @property({ type: String, attribute: "fetch-credentials" }) fetchCredentials = "";
206
+
207
+ // Filters
208
+ @property({ type: String, attribute: "match-paths" }) matchPaths = "";
209
+ @property({ type: String, attribute: "match-type" }) matchType = "includes";
210
+ @property({ type: String, attribute: "remove-endpoints-with-badge-label-as" })
211
+ removeEndpointsWithBadgeLabelAs = "";
212
+
213
+ // Internal Properties
214
+ @property({ type: Boolean }) loading = false;
215
+ @property({ type: String }) focusedElementId = "";
216
+ @property({ type: Object }) advancedSearchMatches: any;
217
+ @property({ type: String }) searchVal = "";
218
+
219
+ static get styles() {
220
+ return [
221
+ ThemeTokens,
222
+ FontStyles,
223
+ InputStyles,
224
+ FlexStyles,
225
+ AuthStyles,
226
+ ParamListStyles,
227
+ EndpointStyles,
228
+ MicrolighterStyles,
229
+ TabStyles,
230
+ NavStyles,
231
+ InfoStyles,
232
+ DialogBoxStyles,
233
+ LayoutStyles,
234
+ CustomStyles,
235
+ ];
236
+ }
237
+
238
+ // Startup
239
+ connectedCallback() {
240
+ super.connectedCallback();
241
+ this.initIntersectionObserver();
242
+ if (this.shadowRoot && this.shadowRoot.querySelector(".main-content")) {
243
+ this._hasDsd = true;
244
+ }
245
+ const parent = this.parentElement;
246
+ if (parent && parent.tagName === "BODY") {
247
+ if (parent.offsetWidth === 0 && parent.style.width === "") {
248
+ parent.style.width = "100vw";
249
+ }
250
+ if (parent.offsetHeight === 0 && parent.style.height === "") {
251
+ parent.style.height = "100vh";
252
+ }
253
+ if (!parent.style.marginTop) {
254
+ parent.style.marginTop = "0";
255
+ }
256
+ if (!parent.style.marginRight) {
257
+ parent.style.marginRight = "0";
258
+ }
259
+ if (!parent.style.marginBottom) {
260
+ parent.style.marginBottom = "0";
261
+ }
262
+ if (!parent.style.marginLeft) {
263
+ parent.style.marginLeft = "0";
264
+ }
265
+ }
266
+
267
+ if (!this.renderStyle || !"read, view, focused,".includes(`${this.renderStyle},`)) {
268
+ this.renderStyle = "focused";
269
+ }
270
+
271
+ if (this.hasAttribute("layout")) {
272
+ const attrLayout = this.getAttribute("layout");
273
+ if ("row, column,".includes(`${attrLayout},`)) {
274
+ this.layout = attrLayout;
275
+ this._userSetLayout = true;
276
+ }
277
+ }
278
+
279
+ if (!this._userSetLayout) {
280
+ const defaultLayout =
281
+ this.renderStyle === "read" || this.renderStyle === "focused" ? "column" : "row";
282
+ this._isDefaultingLayout = true;
283
+ this.layout = defaultLayout;
284
+ this.setAttribute("layout", defaultLayout);
285
+ this._isDefaultingLayout = false;
286
+ } else if (!this.layout || !"row, column,".includes(`${this.layout},`)) {
287
+ this.layout =
288
+ this.renderStyle === "read" || this.renderStyle === "focused" ? "column" : "row";
289
+ this.setAttribute("layout", this.layout);
290
+ }
291
+ if (!this.schemaStyle || !"tree, table,".includes(`${this.schemaStyle},`)) {
292
+ this.schemaStyle = "tree";
293
+ }
294
+ if (this.theme === "light" || this.theme === "dark") {
295
+ this.colorScheme = this.theme;
296
+ this.theme = DEFAULT_THEME;
297
+ }
298
+ if (this.primaryColor && isValidHexColor(this.primaryColor)) {
299
+ this.theme = this.primaryColor;
300
+ }
301
+ const normalizedTheme = normalizeTheme(this.theme);
302
+ this.theme = normalizedTheme.theme;
303
+ this.setAttribute("theme", this.theme);
304
+ if (!this.colorScheme || !"dark, light, system,".includes(`${this.colorScheme},`)) {
305
+ this.colorScheme = "dark";
306
+ }
307
+ if (!this.radius || !"none, sm, md, lg, full,".includes(`${this.radius},`)) {
308
+ this.radius = "sm";
309
+ }
310
+ if (!this.scale || !"sm, default, lg,".includes(`${this.scale},`)) {
311
+ this.scale = "default";
312
+ }
313
+ if (!this.navItemSpacing || !"compact, default, relaxed,".includes(`${this.navItemSpacing},`)) {
314
+ this.navItemSpacing = "default";
315
+ }
316
+ if (!this.getAttribute("theme")) {
317
+ this.setAttribute("theme", this.theme);
318
+ }
319
+ if (!this.getAttribute("color-scheme")) {
320
+ this.setAttribute("color-scheme", this.colorScheme);
321
+ }
322
+ if (!this.getAttribute("radius")) {
323
+ this.setAttribute("radius", this.radius);
324
+ }
325
+ if (!this.getAttribute("scale")) {
326
+ this.setAttribute("scale", this.scale);
327
+ }
328
+ applyCustomTheme(this, this.theme);
329
+ if (
330
+ !this.defaultSchemaTab ||
331
+ !"example, schema, model,".includes(`${this.defaultSchemaTab},`)
332
+ ) {
333
+ this.defaultSchemaTab = "example";
334
+ } else if (this.defaultSchemaTab === "model") {
335
+ this.defaultSchemaTab = "schema";
336
+ }
337
+ if (!this.schemaExpandLevel || this.schemaExpandLevel < 1) {
338
+ this.schemaExpandLevel = 99999;
339
+ }
340
+ if (
341
+ !this.schemaDescriptionExpanded ||
342
+ !"true, false,".includes(`${this.schemaDescriptionExpanded},`)
343
+ ) {
344
+ this.schemaDescriptionExpanded = "false";
345
+ }
346
+
347
+ if (!this.schemaHideReadOnly || !"default, never,".includes(`${this.schemaHideReadOnly},`)) {
348
+ this.schemaHideReadOnly = "default";
349
+ }
350
+ if (!this.schemaHideWriteOnly || !"default, never,".includes(`${this.schemaHideWriteOnly},`)) {
351
+ this.schemaHideWriteOnly = "default";
352
+ }
353
+
354
+ if (
355
+ !this.fillRequestFieldsWithExample ||
356
+ !"true, false,".includes(`${this.fillRequestFieldsWithExample},`)
357
+ ) {
358
+ this.fillRequestFieldsWithExample = "true";
359
+ }
360
+ if (!this.persistAuth || !"true, false,".includes(`${this.persistAuth},`)) {
361
+ this.persistAuth = "false";
362
+ }
363
+ if (!this.responseAreaHeight) {
364
+ this.responseAreaHeight = "400px";
365
+ }
366
+
367
+ if (!this.allowSearch || !"true, false,".includes(`${this.allowSearch},`)) {
368
+ this.allowSearch = "true";
369
+ }
370
+ if (!this.allowAdvancedSearch || !"true, false,".includes(`${this.allowAdvancedSearch},`)) {
371
+ this.allowAdvancedSearch = "true";
372
+ }
373
+
374
+ if (!this.allowTry || !"true, false,".includes(`${this.allowTry},`)) {
375
+ this.allowTry = "true";
376
+ }
377
+ if (!this.apiKeyValue) {
378
+ this.apiKeyValue = "-";
379
+ }
380
+ if (!this.apiKeyLocation) {
381
+ this.apiKeyLocation = "header";
382
+ }
383
+ if (!this.apiKeyName) {
384
+ this.apiKeyName = "";
385
+ }
386
+
387
+ if (!this.oauthReceiver) {
388
+ this.oauthReceiver = "oauth-receiver.html";
389
+ }
390
+ if (!this.updateRoute || !"true, false,".includes(`${this.updateRoute},`)) {
391
+ this.updateRoute = "true";
392
+ }
393
+ if (!this.sortTags || !"true, false,".includes(`${this.sortTags},`)) {
394
+ this.sortTags = "false";
395
+ }
396
+ if (!this.sortSchemas || !"true, false,".includes(`${this.sortSchemas},`)) {
397
+ this.sortSchemas = "false";
398
+ }
399
+ if (!this.generateMissingTags || !"true, false,".includes(`${this.generateMissingTags},`)) {
400
+ this.generateMissingTags = "false";
401
+ }
402
+ if (!this.showUnusedTags || !"true, false,".includes(`${this.showUnusedTags},`)) {
403
+ this.showUnusedTags = "false";
404
+ }
405
+ if (
406
+ !this.sortEndpointsBy ||
407
+ !"method, path, summary, none,".includes(`${this.sortEndpointsBy},`)
408
+ ) {
409
+ this.sortEndpointsBy = "path";
410
+ }
411
+
412
+ if (!this.navItemSpacing || !"compact, relaxed, default,".includes(`${this.navItemSpacing},`)) {
413
+ this.navItemSpacing = "default";
414
+ }
415
+ if (
416
+ !this.showMethodInNavBar ||
417
+ !"false, as-plain-text, as-colored-text, as-colored-block,".includes(
418
+ `${this.showMethodInNavBar},`,
419
+ )
420
+ ) {
421
+ this.showMethodInNavBar = "false";
422
+ }
423
+ if (!this.usePathInNavBar || !"true, false,".includes(`${this.usePathInNavBar},`)) {
424
+ this.usePathInNavBar = "false";
425
+ }
426
+ if (!this.navItemOverflow || !"ellipsis, wrap,".includes(`${this.navItemOverflow},`)) {
427
+ this.navItemOverflow = "ellipsis";
428
+ }
429
+ if (!this.getAttribute("nav-item-overflow")) {
430
+ this.setAttribute("nav-item-overflow", this.navItemOverflow);
431
+ }
432
+ if (!this.showInfo || !"true, false,".includes(`${this.showInfo},`)) {
433
+ this.showInfo = "true";
434
+ }
435
+ if (!this.allowServerSelection || !"true, false,".includes(`${this.allowServerSelection},`)) {
436
+ this.allowServerSelection = "true";
437
+ }
438
+ if (!this.allowAuthentication || !"true, false,".includes(`${this.allowAuthentication},`)) {
439
+ this.allowAuthentication = "true";
440
+ }
441
+ if (
442
+ !this.allowSchemaDescriptionExpandToggle ||
443
+ !"true, false,".includes(`${this.allowSchemaDescriptionExpandToggle},`)
444
+ ) {
445
+ this.allowSchemaDescriptionExpandToggle = "true";
446
+ }
447
+
448
+ if (!this.showSideNav || !"true false".includes(this.showSideNav)) {
449
+ this.showSideNav = "true";
450
+ }
451
+ if (!this.showComponents || !"true false".includes(this.showComponents)) {
452
+ this.showComponents = "false";
453
+ }
454
+ if (
455
+ !this.infoDescriptionHeadingsInNavBar ||
456
+ !"true, false,".includes(`${this.infoDescriptionHeadingsInNavBar},`)
457
+ ) {
458
+ this.infoDescriptionHeadingsInNavBar = "false";
459
+ }
460
+ if (
461
+ !this.fetchCredentials ||
462
+ !"omit, same-origin, include,".includes(`${this.fetchCredentials},`)
463
+ ) {
464
+ this.fetchCredentials = "";
465
+ }
466
+ if (!this.scrollBehavior || !"smooth, auto,".includes(`${this.scrollBehavior},`)) {
467
+ this.scrollBehavior = "auto";
468
+ }
469
+
470
+ if (!this.matchType || !"includes regex".includes(this.matchType)) {
471
+ this.matchType = "includes";
472
+ }
473
+ if (!this.matchPaths) {
474
+ this.matchPaths = "";
475
+ }
476
+ if (!this.removeEndpointsWithBadgeLabelAs) {
477
+ this.removeEndpointsWithBadgeLabelAs = "";
478
+ }
479
+ if (!this.cssFile) {
480
+ this.cssFile = null;
481
+ }
482
+ if (!this.cssClasses) {
483
+ this.cssClasses = "";
484
+ }
485
+ if (!this.mcp) {
486
+ this.mcp = "none";
487
+ }
488
+
489
+ if (!this._onHashChange) {
490
+ this._onHashChange = () => {
491
+ this.scrollToPath(this.getElementIDFromURL());
492
+ };
493
+ window.addEventListener("hashchange", this._onHashChange, true);
494
+ }
495
+
496
+ if (!this._readerHasInteracted && !this._onFirstInteraction) {
497
+ this._onFirstInteraction = () => {
498
+ this._readerHasInteracted = true;
499
+ this.removeFirstInteractionListeners();
500
+ };
501
+ INTERACTION_EVENTS.forEach((type) =>
502
+ window.addEventListener(type, this._onFirstInteraction, { capture: true, passive: true }),
503
+ );
504
+ }
505
+
506
+ // Auto-hydrate from inlined spec or spec-url attribute if not already loaded. This block owns
507
+ // the initial load; the deferred spec-url path in attributeChangedCallback skips the value
508
+ // recorded in _lastSpecUrlRequested so one <rapi-doc spec-url> loads once.
509
+ if (!this.resolvedSpec) {
510
+ const inlinedSpecEl = this.querySelector(
511
+ 'script[type="application/json"][slot="rapidoc-spec"]',
512
+ );
513
+ if (inlinedSpecEl?.textContent) {
514
+ try {
515
+ const parsed = JSON.parse(inlinedSpecEl.textContent);
516
+ // The inlined spec wins over a spec-url attribute present at connect
517
+ this._lastSpecUrlRequested = this.getAttribute("spec-url");
518
+ this.loadSpec(parsed);
519
+ } catch {
520
+ // ignore parse error
521
+ }
522
+ } else if (this.getAttribute("spec-url")) {
523
+ this.loadSpec(this.getAttribute("spec-url"));
524
+ }
525
+ }
526
+ }
527
+
528
+ // Cleanup
529
+ disconnectedCallback() {
530
+ if (this._intersectionRaf) {
531
+ cancelAnimationFrame(this._intersectionRaf);
532
+ }
533
+ if (typeof this._observerLockCleanup === "function") {
534
+ this._observerLockCleanup();
535
+ this._observerLockCleanup = null;
536
+ }
537
+ if (this._observerLockTimeout) {
538
+ clearTimeout(this._observerLockTimeout);
539
+ this._observerLockTimeout = null;
540
+ }
541
+ if (this.intersectionObserver) {
542
+ this.intersectionObserver.disconnect();
543
+ }
544
+ if (this._onHashChange) {
545
+ window.removeEventListener("hashchange", this._onHashChange, true);
546
+ this._onHashChange = null;
547
+ }
548
+ this.removeFirstInteractionListeners();
549
+ if (this.mockServer === "true") {
550
+ disableMockServer(this);
551
+ }
552
+ super.disconnectedCallback();
553
+ }
554
+
555
+ removeFirstInteractionListeners() {
556
+ if (!this._onFirstInteraction) return;
557
+ INTERACTION_EVENTS.forEach((type) =>
558
+ window.removeEventListener(type, this._onFirstInteraction, { capture: true }),
559
+ );
560
+ this._onFirstInteraction = null;
561
+ }
562
+
563
+ /**
564
+ * Behavior of scrolls that move the content to a section. Scrolls before the reader's first input are
565
+ * load-time navigation (location hash, goto-path, back/forward) and jump, like the browser's own
566
+ * fragment navigation. A smooth scroll advances only while the page renders frames, and a background
567
+ * tab renders none: the scroll would not happen, and the active-section tracking would then report the
568
+ * section at the top of the page (and rewrite the URL to it).
569
+ */
570
+ getContentScrollBehavior() {
571
+ return this._readerHasInteracted ? this.scrollBehavior : "instant";
572
+ }
573
+
574
+ infoDescriptionHeadingRenderer() {
575
+ const renderer = new marked.Renderer();
576
+ const slugger = new Slugger();
577
+ renderer.heading = ({ text, depth }) =>
578
+ `<h${depth} class="observe-me" id="${slugger.slug(text)}">${text}</h${depth}>`;
579
+ fixRenderedAnchorLinks(renderer);
580
+ return renderer;
581
+ }
582
+
583
+ willUpdate(changedProperties) {
584
+ super.willUpdate?.(changedProperties);
585
+ if (this.defaultSchemaTab === "model") {
586
+ this.defaultSchemaTab = "schema";
587
+ }
588
+ if (changedProperties.has("layout") && !this._isDefaultingLayout) {
589
+ this._userSetLayout = Boolean(this.layout && "row, column,".includes(`${this.layout},`));
590
+ }
591
+ if (changedProperties.has("renderStyle") && !this._userSetLayout) {
592
+ const targetLayout =
593
+ this.renderStyle === "read" || this.renderStyle === "focused" ? "column" : "row";
594
+ if (this.layout !== targetLayout) {
595
+ this._isDefaultingLayout = true;
596
+ this.layout = targetLayout;
597
+ this.setAttribute("layout", targetLayout);
598
+ this._isDefaultingLayout = false;
599
+ }
600
+ }
601
+ }
602
+
603
+ _clearDsdIfNeeded() {
604
+ if (this._hasDsd && this.shadowRoot) {
605
+ this.shadowRoot.replaceChildren();
606
+ if (this.renderOptions) {
607
+ delete this.renderOptions.renderBefore;
608
+ }
609
+ this._hasDsd = false;
610
+ }
611
+ }
612
+
613
+ render() {
614
+ if (this._hasDsd && !this.resolvedSpec) {
615
+ return noChange;
616
+ }
617
+ this._clearDsdIfNeeded();
618
+
619
+ // adding custom style for RapiDoc: the page's <link> whose href contains the css-file value
620
+ if (this.cssFile) {
621
+ const cssLinkEl = document.querySelector(`link[href*="${CSS.escape(this.cssFile)}"]`);
622
+ if (cssLinkEl) {
623
+ this.shadowRoot.appendChild(cssLinkEl.cloneNode());
624
+ }
625
+ }
626
+ return mainBodyTemplate.call(this);
627
+ }
628
+
629
+ get config() {
630
+ return {
631
+ renderStyle: this.renderStyle,
632
+ schemaStyle: this.schemaStyle,
633
+ defaultSchemaTab: this.defaultSchemaTab,
634
+ schemaExpandLevel: this.schemaExpandLevel,
635
+ schemaDescriptionExpanded: this.schemaDescriptionExpanded,
636
+ allowSchemaDescriptionExpandToggle: this.allowSchemaDescriptionExpandToggle,
637
+ schemaHideReadOnly: this.schemaHideReadOnly,
638
+ schemaHideWriteOnly: this.schemaHideWriteOnly,
639
+ fillRequestFieldsWithExample: this.fillRequestFieldsWithExample,
640
+ allowTry: this.allowTry,
641
+ showCurlBeforeTry: this.showCurlBeforeTry,
642
+ fetchCredentials: this.fetchCredentials,
643
+ };
644
+ }
645
+
646
+ updated(changedProperties) {
647
+ super.updated?.(changedProperties);
648
+ scheduleHighlight(this.shadowRoot);
649
+
650
+ if (changedProperties.has("theme")) {
651
+ if (this.theme === "light" || this.theme === "dark") {
652
+ this.colorScheme = this.theme;
653
+ this.setAttribute("color-scheme", this.colorScheme);
654
+ this.theme = DEFAULT_THEME;
655
+ this.setAttribute("theme", DEFAULT_THEME);
656
+ }
657
+ const normalizedTheme = normalizeTheme(this.theme);
658
+ if (this.theme !== normalizedTheme.theme) {
659
+ this.theme = normalizedTheme.theme;
660
+ this.setAttribute("theme", this.theme);
661
+ }
662
+ applyCustomTheme(this, this.theme);
663
+ }
664
+
665
+ if (
666
+ changedProperties.has("primaryColor") &&
667
+ this.primaryColor &&
668
+ isValidHexColor(this.primaryColor)
669
+ ) {
670
+ this.theme = normalizeTheme(this.primaryColor).theme;
671
+ this.setAttribute("theme", this.theme);
672
+ applyCustomTheme(this, this.theme);
673
+ }
674
+
675
+ if (changedProperties.has("colorScheme")) {
676
+ if (!this.colorScheme || !"dark, light, system,".includes(`${this.colorScheme},`)) {
677
+ this.colorScheme = "dark";
678
+ }
679
+ if (this.getAttribute("color-scheme") !== this.colorScheme) {
680
+ this.setAttribute("color-scheme", this.colorScheme);
681
+ }
682
+ }
683
+
684
+ if (changedProperties.has("mockServer")) {
685
+ if (this.mockServer === "true" && this.resolvedSpec) {
686
+ enableMockServer(this.resolvedSpec, {
687
+ owner: this,
688
+ statusCode: this.mockServerStatusCode,
689
+ statusStrategy: this.mockServerStatusStrategy,
690
+ delay: this.mockServerDelay,
691
+ log: this.mockServerLog,
692
+ });
693
+ } else if (this.mockServer !== "true") {
694
+ disableMockServer(this);
695
+ }
696
+ }
697
+ if (
698
+ changedProperties.has("mockServerStatusCode") ||
699
+ changedProperties.has("mockServerStatusStrategy") ||
700
+ changedProperties.has("mockServerDelay") ||
701
+ changedProperties.has("mockServerLog")
702
+ ) {
703
+ updateMockConfig({
704
+ owner: this,
705
+ statusCode: this.mockServerStatusCode,
706
+ statusStrategy: this.mockServerStatusStrategy,
707
+ delay: this.mockServerDelay,
708
+ log: this.mockServerLog,
709
+ });
710
+ }
711
+ if (changedProperties.has("scrollContainer")) {
712
+ this.initIntersectionObserver();
713
+ }
714
+ }
715
+
716
+ observeExpandedContent() {
717
+ // Main Container
718
+ const observeOverviewEls = this.shadowRoot.querySelectorAll(".observe-me");
719
+ observeOverviewEls.forEach((targetEl) => {
720
+ this.intersectionObserver.observe(targetEl);
721
+ });
722
+ }
723
+
724
+ attributeChangedCallback(name, oldVal, newVal) {
725
+ if (name === "spec-url") {
726
+ if (oldVal !== newVal) {
727
+ // put it at the end of event-loop to load all the attributes
728
+ window.setTimeout(async () => {
729
+ // Already loaded by connectedCallback (or superseded by an inlined spec there)
730
+ if (newVal === this._lastSpecUrlRequested) {
731
+ return;
732
+ }
733
+ await this.loadSpec(newVal);
734
+ // If goto-path is provided and no location-hash is present then try to scroll there
735
+ if (this.gotoPath && !window.location.hash) {
736
+ this.scrollToPath(this.gotoPath);
737
+ }
738
+ }, 0);
739
+ }
740
+ }
741
+ if (
742
+ name === "match-paths" ||
743
+ name === "match-type" ||
744
+ name === "remove-endpoints-with-badge-label-as"
745
+ ) {
746
+ if (oldVal !== newVal) {
747
+ window.setTimeout(async () => {
748
+ await this.loadSpec(this.specUrl);
749
+ }, 0);
750
+ }
751
+ }
752
+ if (name === "scroll-offset" && oldVal !== newVal) {
753
+ window.setTimeout(() => this.initIntersectionObserver(), 0);
754
+ }
755
+ if (name === "layout") {
756
+ if (!this._isDefaultingLayout) {
757
+ if (newVal !== null && "row, column,".includes(`${newVal},`)) {
758
+ this._userSetLayout = true;
759
+ this.layout = newVal;
760
+ } else if (newVal === null) {
761
+ this._userSetLayout = false;
762
+ const defaultLayout =
763
+ this.renderStyle === "read" || this.renderStyle === "focused" ? "column" : "row";
764
+ this._isDefaultingLayout = true;
765
+ this.layout = defaultLayout;
766
+ this.setAttribute("layout", defaultLayout);
767
+ this._isDefaultingLayout = false;
768
+ }
769
+ }
770
+ }
771
+ if (name === "render-style") {
772
+ if (newVal === "read") {
773
+ window.setTimeout(() => {
774
+ this.observeExpandedContent();
775
+ }, 100);
776
+ } else {
777
+ this.intersectionObserver.disconnect();
778
+ }
779
+ if (!this._userSetLayout) {
780
+ const defaultLayout = newVal === "read" || newVal === "focused" ? "column" : "row";
781
+ this._isDefaultingLayout = true;
782
+ this.layout = defaultLayout;
783
+ this.setAttribute("layout", defaultLayout);
784
+ this._isDefaultingLayout = false;
785
+ }
786
+ }
787
+ if (name === "api-key-name" || name === "api-key-location" || name === "api-key-value") {
788
+ let updateSelectedApiKey = false;
789
+ let apiKeyName = "";
790
+ let apiKeyLocation = "";
791
+ let apiKeyValue = "";
792
+
793
+ if (name === "api-key-name") {
794
+ if (this.getAttribute("api-key-location") && this.getAttribute("api-key-value")) {
795
+ apiKeyName = newVal;
796
+ apiKeyLocation = this.getAttribute("api-key-location");
797
+ apiKeyValue = this.getAttribute("api-key-value");
798
+ updateSelectedApiKey = true;
799
+ }
800
+ } else if (name === "api-key-location") {
801
+ if (this.getAttribute("api-key-name") && this.getAttribute("api-key-value")) {
802
+ apiKeyLocation = newVal;
803
+ apiKeyName = this.getAttribute("api-key-name");
804
+ apiKeyValue = this.getAttribute("api-key-value");
805
+ updateSelectedApiKey = true;
806
+ }
807
+ } else if (name === "api-key-value") {
808
+ if (this.getAttribute("api-key-name") && this.getAttribute("api-key-location")) {
809
+ apiKeyValue = newVal;
810
+ apiKeyLocation = this.getAttribute("api-key-location");
811
+ apiKeyName = this.getAttribute("api-key-name");
812
+ updateSelectedApiKey = true;
813
+ }
814
+ }
815
+
816
+ if (updateSelectedApiKey) {
817
+ // While the spec is loading, resolvedSpec is a placeholder without securitySchemes
818
+ if (this.resolvedSpec?.securitySchemes) {
819
+ const rapiDocApiKey = this.resolvedSpec.securitySchemes.find(
820
+ (v) => v.securitySchemeId === rapidocApiKey,
821
+ );
822
+ if (!rapiDocApiKey) {
823
+ this.resolvedSpec.securitySchemes.push({
824
+ securitySchemeId: rapidocApiKey,
825
+ description: "api-key provided in rapidoc element attributes",
826
+ type: "apiKey",
827
+ name: apiKeyName,
828
+ in: apiKeyLocation,
829
+ value: apiKeyValue,
830
+ finalKeyValue: apiKeyValue,
831
+ });
832
+ } else {
833
+ rapiDocApiKey.name = apiKeyName;
834
+ rapiDocApiKey.in = apiKeyLocation;
835
+ rapiDocApiKey.value = apiKeyValue;
836
+ rapiDocApiKey.finalKeyValue = apiKeyValue;
837
+ }
838
+ this.requestUpdate();
839
+ }
840
+ }
841
+ }
842
+ super.attributeChangedCallback(name, oldVal, newVal);
843
+ }
844
+
845
+ onSpecUrlChange() {
846
+ this.setAttribute("spec-url", this.shadowRoot.getElementById("spec-url").value);
847
+ }
848
+
849
+ onSpecFileChange(e) {
850
+ this.setAttribute("spec-file", this.shadowRoot.getElementById("spec-file").value);
851
+ const specFile = e.target.files[0];
852
+ const reader = new FileReader();
853
+ reader.onload = () => {
854
+ try {
855
+ const specObj = JSON.parse(reader.result);
856
+ this.loadSpec(specObj);
857
+ this.shadowRoot.getElementById("spec-url").value = "";
858
+ } catch {
859
+ console.error("RapiDoc: Unable to read or parse json");
860
+ }
861
+ };
862
+ // Read the Text file
863
+ reader.readAsText(specFile);
864
+ }
865
+
866
+ onFileLoadClick() {
867
+ this.shadowRoot.getElementById("spec-file").click();
868
+ }
869
+
870
+ onSearchChange(e) {
871
+ // The event may come from the ↩ icon (a div without `value`): read the nav-bar input instead
872
+ const input =
873
+ e.target instanceof HTMLInputElement
874
+ ? e.target
875
+ : this.shadowRoot.getElementById("nav-bar-search");
876
+ this.searchVal = input?.value ?? "";
877
+ this.resolvedSpec.tags.forEach((tag) =>
878
+ tag.paths.filter((path) => {
879
+ if (this.searchVal) {
880
+ if (getMatchedPaths(this.searchVal, path, tag.name)) {
881
+ tag.expanded = true;
882
+ }
883
+ }
884
+ }),
885
+ );
886
+ this.resolvedSpec.components.forEach((component) =>
887
+ component.subComponents.filter((v) => {
888
+ v.expanded = false;
889
+ if (getMatchedComponents(this.searchVal, v)) {
890
+ v.expanded = true;
891
+ }
892
+ }),
893
+ );
894
+ this.requestUpdate();
895
+ }
896
+
897
+ onClearSearch() {
898
+ const searchEl = this.shadowRoot.getElementById("nav-bar-search");
899
+ searchEl.value = "";
900
+ this.searchVal = "";
901
+ this.resolvedSpec.components.forEach((component) =>
902
+ component.subComponents.filter((v) => {
903
+ v.expanded = true;
904
+ }),
905
+ );
906
+ }
907
+
908
+ onOpenNavBarToggle() {
909
+ const navBarEL = this.shadowRoot.getElementById("nav-bar");
910
+ navBarEL.classList.toggle("floating-nav");
911
+ }
912
+
913
+ onShowAdvancedSearchClicked() {
914
+ this.shadowRoot.getElementById("advanced-search-dialog").showModal();
915
+ }
916
+ onAdvancedSearchClose() {
917
+ this.shadowRoot.getElementById("advanced-search-dialog").close();
918
+ }
919
+
920
+ // Public Method
921
+ async loadSpec(specUrl) {
922
+ if (!specUrl) {
923
+ return;
924
+ }
925
+ if (typeof specUrl === "string") {
926
+ this._lastSpecUrlRequested = specUrl;
927
+ }
928
+ // Each load gets a token; a load whose token is no longer current never applies its result
929
+ const token = ++this._specLoadSeq;
930
+ this.searchVal = "";
931
+ try {
932
+ if (!this._hasDsd) {
933
+ this.resolvedSpec = {
934
+ specLoadError: false,
935
+ isSpecLoading: true,
936
+ tags: [],
937
+ };
938
+ this.loading = true;
939
+ }
940
+ this.loadFailed = false;
941
+ const spec = await ProcessSpec.call(
942
+ this,
943
+ specUrl,
944
+ this.generateMissingTags === "true",
945
+ this.sortTags === "true",
946
+ this.sortSchemas === "true",
947
+ this.getAttribute("sort-endpoints-by"),
948
+ this.getAttribute("api-key-name"),
949
+ this.getAttribute("api-key-location"),
950
+ this.getAttribute("api-key-value"),
951
+ this.getAttribute("server-url"),
952
+ this.matchPaths,
953
+ this.matchType,
954
+ this.removeEndpointsWithBadgeLabelAs,
955
+ this.showUnusedTags === "true",
956
+ );
957
+ if (token !== this._specLoadSeq) {
958
+ return; // a newer load started while this one was parsing
959
+ }
960
+ this.loading = false;
961
+ this.afterSpecParsedAndValidated(spec, token);
962
+ } catch (err) {
963
+ if (token !== this._specLoadSeq) {
964
+ return;
965
+ }
966
+ this.loading = false;
967
+ this._clearDsdIfNeeded();
968
+ this.loadFailed = true;
969
+ this.resolvedSpec = null;
970
+ console.error(`RapiDoc: Unable to resolve the API spec.. ${err.message}`);
971
+ }
972
+ }
973
+
974
+ async afterSpecParsedAndValidated(spec, token = this._specLoadSeq) {
975
+ this._clearDsdIfNeeded();
976
+ this.resolvedSpec = spec;
977
+ this.selectedServer = undefined;
978
+ if (this.defaultApiServerUrl) {
979
+ if (this.defaultApiServerUrl === this.serverUrl) {
980
+ this.selectedServer = {
981
+ url: this.serverUrl,
982
+ computedUrl: this.serverUrl,
983
+ };
984
+ } else if (this.resolvedSpec.servers) {
985
+ this.selectedServer = this.resolvedSpec.servers.find(
986
+ (v) => v.url === this.defaultApiServerUrl,
987
+ );
988
+ }
989
+ }
990
+ if (!this.selectedServer) {
991
+ if (this.resolvedSpec.servers) {
992
+ this.selectedServer = this.resolvedSpec.servers[0];
993
+ }
994
+ }
995
+ this.requestUpdate();
996
+ while (!(await this.updateComplete));
997
+ if (token !== this._specLoadSeq) {
998
+ return; // a newer load took over while this one was rendering; it fires its own spec-loaded
999
+ }
1000
+ const specLoadedEvent = new CustomEvent("spec-loaded", { detail: spec });
1001
+ this.dispatchEvent(specLoadedEvent);
1002
+
1003
+ if (this.mockServer === "true") {
1004
+ enableMockServer(this.resolvedSpec, {
1005
+ owner: this,
1006
+ statusCode: this.mockServerStatusCode,
1007
+ statusStrategy: this.mockServerStatusStrategy,
1008
+ delay: this.mockServerDelay,
1009
+ log: this.mockServerLog,
1010
+ });
1011
+ }
1012
+
1013
+ // Initiate IntersectionObserver and put it at the end of event loop, to allow loading all the child elements (must for larger specs)
1014
+ this.intersectionObserver.disconnect();
1015
+ if (this._intersectingElements) {
1016
+ this._intersectingElements.clear();
1017
+ }
1018
+ if (this.renderStyle === "read") {
1019
+ await sleep(100);
1020
+ if (token !== this._specLoadSeq) {
1021
+ return;
1022
+ }
1023
+ this.observeExpandedContent(); // This will auto-highlight the selected nav-item in read-mode
1024
+ }
1025
+
1026
+ this.isIntersectionObserverActive = true;
1027
+
1028
+ // On first time Spec load, try to navigate to location hash if provided
1029
+ const elementId = this.getElementIDFromURL();
1030
+
1031
+ if (elementId) {
1032
+ if (this.renderStyle === "view") {
1033
+ this.expandAndGotoOperation(elementId, true, true);
1034
+ } else {
1035
+ this.scrollToPath(elementId);
1036
+ }
1037
+ } else if (this.gotoPath) {
1038
+ this.scrollToPath(this.gotoPath);
1039
+ } else if (this.renderStyle === "focused") {
1040
+ // If goto-path is provided and no location-hash is present then try to scroll to default element
1041
+ if (!this.gotoPath) {
1042
+ const defaultElementId =
1043
+ this.showInfo === "true"
1044
+ ? "overview"
1045
+ : this.resolvedSpec.tags.find((t) => t.paths.length > 0)?.paths[0]?.elementId;
1046
+ if (defaultElementId) {
1047
+ this.scrollToPath(defaultElementId);
1048
+ }
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ /**
1054
+ * Return the URL from where is served the RapiDoc component, removing any hash
1055
+ */
1056
+ getComponentBaseURL() {
1057
+ return window.location.href.split("#")[0];
1058
+ }
1059
+
1060
+ /**
1061
+ * From the URL return the ID of the element from the location hash
1062
+ */
1063
+ getElementIDFromURL() {
1064
+ return window.location.hash ? window.location.hash.substring(1) : "";
1065
+ }
1066
+
1067
+ replaceHistoryState(hashId) {
1068
+ const baseURL = this.getComponentBaseURL();
1069
+ window.history.replaceState(null, null, hashId ? `${baseURL}#${hashId}` : baseURL);
1070
+ }
1071
+
1072
+ expandAndGotoOperation(elementId, scrollToElement = true) {
1073
+ if (!this.resolvedSpec) {
1074
+ return;
1075
+ }
1076
+ // Expand full operation and tag
1077
+ let isExpandingNeeded = true;
1078
+ const tmpElementId = elementId.indexOf("#") === -1 ? elementId : elementId.substring(1);
1079
+ if (
1080
+ tmpElementId.startsWith("overview") ||
1081
+ tmpElementId === "servers" ||
1082
+ tmpElementId === "auth"
1083
+ ) {
1084
+ isExpandingNeeded = false;
1085
+ } else {
1086
+ for (let i = 0; i < this.resolvedSpec.tags?.length; i++) {
1087
+ const tag = this.resolvedSpec.tags[i];
1088
+ const path = tag.paths?.find((p) => p.elementId === elementId);
1089
+ if (path) {
1090
+ if (path.expanded && tag.expanded) {
1091
+ isExpandingNeeded = false;
1092
+ } else {
1093
+ path.expanded = true;
1094
+ tag.expanded = true;
1095
+ }
1096
+ }
1097
+ }
1098
+ if (this.resolvedSpec.tagGroups) {
1099
+ for (let g = 0; g < this.resolvedSpec.tagGroups.length; g++) {
1100
+ const group = this.resolvedSpec.tagGroups[g];
1101
+ const hasPath = group.tags?.some((t) => t.paths?.some((p) => p.elementId === elementId));
1102
+ if (hasPath && !group.expanded) {
1103
+ group.expanded = true;
1104
+ isExpandingNeeded = true;
1105
+ }
1106
+ }
1107
+ }
1108
+ }
1109
+ if (scrollToElement) {
1110
+ // requestUpdate() and delay required, else we cant find element
1111
+ if (isExpandingNeeded) {
1112
+ this.requestUpdate();
1113
+ }
1114
+ window.setTimeout(
1115
+ () => {
1116
+ const firstNavEl = this.shadowRoot.querySelector(".first-nav-item");
1117
+ const isFirst = firstNavEl && firstNavEl.dataset.contentId === tmpElementId;
1118
+ const gotoEl = this.shadowRoot.getElementById(tmpElementId);
1119
+ if (gotoEl) {
1120
+ const mainContentEl = this.shadowRoot.querySelector(".main-content");
1121
+ let targetTop: number;
1122
+ if (this.autoHeight === "true") {
1123
+ this.scrollAutoHeightTarget(gotoEl, tmpElementId);
1124
+ } else if (isFirst) {
1125
+ targetTop = 0;
1126
+ this.lockObserverDuringScroll(gotoEl, tmpElementId, targetTop);
1127
+ if (mainContentEl) {
1128
+ mainContentEl.scrollTo({ top: 0, behavior: this.getContentScrollBehavior() });
1129
+ } else {
1130
+ gotoEl.scrollIntoView({
1131
+ behavior: this.getContentScrollBehavior(),
1132
+ block: "start",
1133
+ });
1134
+ }
1135
+ } else if (mainContentEl) {
1136
+ const containerRect = mainContentEl.getBoundingClientRect();
1137
+ const targetRect = gotoEl.getBoundingClientRect();
1138
+ targetTop = targetRect.top - containerRect.top + mainContentEl.scrollTop;
1139
+ this.lockObserverDuringScroll(gotoEl, tmpElementId, targetTop);
1140
+ mainContentEl.scrollTo({ top: targetTop, behavior: this.getContentScrollBehavior() });
1141
+ } else {
1142
+ this.lockObserverDuringScroll(gotoEl, tmpElementId);
1143
+ gotoEl.scrollIntoView({ behavior: this.getContentScrollBehavior(), block: "start" });
1144
+ }
1145
+ this._activeElementId = tmpElementId;
1146
+ this.dispatchEvent(
1147
+ new CustomEvent("nav-active-change", {
1148
+ detail: {
1149
+ id: tmpElementId,
1150
+ elementId: tmpElementId,
1151
+ },
1152
+ bubbles: true,
1153
+ composed: true,
1154
+ }),
1155
+ );
1156
+ if (this.updateRoute === "true") {
1157
+ this.replaceHistoryState(tmpElementId);
1158
+ }
1159
+ }
1160
+ },
1161
+ isExpandingNeeded ? 150 : 0,
1162
+ );
1163
+ }
1164
+ }
1165
+
1166
+ isValidTopId(id) {
1167
+ return id.startsWith("overview") || id === "servers" || id === "auth";
1168
+ }
1169
+
1170
+ isValidPathId(id) {
1171
+ if (id === "overview" && this.showInfo) {
1172
+ return true;
1173
+ }
1174
+ if (id === "servers" && this.allowServerSelection) {
1175
+ return true;
1176
+ }
1177
+ if (id === "auth" && this.allowAuthentication) {
1178
+ return true;
1179
+ }
1180
+ if (id.startsWith("tag--")) {
1181
+ return this.resolvedSpec?.tags?.find((tag) => tag.elementId === id);
1182
+ }
1183
+ return this.resolvedSpec?.tags?.find((tag) => tag.paths.find((path) => path.elementId === id));
1184
+ }
1185
+
1186
+ initIntersectionObserver() {
1187
+ if (this.intersectionObserver) {
1188
+ this.intersectionObserver.disconnect();
1189
+ }
1190
+ let container = null;
1191
+ if (this.scrollContainer && typeof document !== "undefined") {
1192
+ try {
1193
+ container = document.querySelector(this.scrollContainer);
1194
+ const isAncestor = Boolean(
1195
+ container &&
1196
+ (container.contains(this) ||
1197
+ (this.assignedSlot && container.contains(this.assignedSlot)) ||
1198
+ (container.shadowRoot && container.shadowRoot.contains(this.assignedSlot))),
1199
+ );
1200
+ if (container && !isAncestor) {
1201
+ // If the specified element is not an ancestor of <rapi-doc>, IntersectionObserver cannot observe inside it
1202
+ console.warn(
1203
+ `RapiDoc: The element specified by scroll-container ("${this.scrollContainer}") is not an ancestor of <rapi-doc>. Falling back to auto-detection.`,
1204
+ );
1205
+ container = null;
1206
+ }
1207
+ } catch {
1208
+ // Ignore invalid query selector
1209
+ }
1210
+ }
1211
+ if (!container && typeof window !== "undefined") {
1212
+ let parent: any = this.assignedSlot ? this.assignedSlot.parentElement : this.parentElement;
1213
+ while (parent && parent !== document.body && parent !== document.documentElement) {
1214
+ const overflowY = window.getComputedStyle(parent).overflowY;
1215
+ if (overflowY === "auto" || overflowY === "scroll") {
1216
+ container = parent;
1217
+ break;
1218
+ }
1219
+ parent =
1220
+ parent.parentElement ||
1221
+ (parent.getRootNode && parent.getRootNode() instanceof ShadowRoot
1222
+ ? (parent.getRootNode() as ShadowRoot).host
1223
+ : null);
1224
+ }
1225
+ }
1226
+ this._scrollRootContainer = container;
1227
+ const intersectionObserverOptions = {
1228
+ root: container,
1229
+ // Headings that slide under a sticky page header (getScrollOffset) count as scrolled past.
1230
+ rootMargin: `-${this.getScrollOffset()}px 0px -50% 0px`,
1231
+ threshold: 0,
1232
+ };
1233
+ this.intersectionObserver = new IntersectionObserver((entries) => {
1234
+ this.onIntersect(entries);
1235
+ }, intersectionObserverOptions);
1236
+ if (this.isIntersectionObserverActive) {
1237
+ this.observeExpandedContent();
1238
+ }
1239
+ }
1240
+
1241
+ onIntersect(entries) {
1242
+ if (this.isIntersectionObserverActive === false) {
1243
+ return;
1244
+ }
1245
+
1246
+ if (!this._intersectingElements) {
1247
+ this._intersectingElements = new Map();
1248
+ }
1249
+
1250
+ for (const entry of entries) {
1251
+ if (entry.isIntersecting && entry.intersectionRatio > 0) {
1252
+ this._intersectingElements.set(entry.target.id, entry.target);
1253
+ } else {
1254
+ this._intersectingElements.delete(entry.target.id);
1255
+ }
1256
+ }
1257
+
1258
+ if (this._intersectingElements.size === 0) {
1259
+ return;
1260
+ }
1261
+
1262
+ const activeCandidates = Array.from(this._intersectingElements.values()).filter(
1263
+ (el) => el.isConnected,
1264
+ );
1265
+ if (activeCandidates.length === 0) {
1266
+ return;
1267
+ }
1268
+
1269
+ const containerTop =
1270
+ (this._scrollRootContainer ? this._scrollRootContainer.getBoundingClientRect().top : 0) +
1271
+ this.getScrollOffset();
1272
+ const visibleHeadings = [];
1273
+ const scrolledPast = [];
1274
+
1275
+ for (const el of activeCandidates) {
1276
+ const top = el.getBoundingClientRect().top - containerTop;
1277
+ // A section scrolled to (scrollToPath) can end up a fraction of a pixel above the top: the
1278
+ // browser rounds the scroll position to device pixels. It still counts as visible.
1279
+ if (top > -1) {
1280
+ visibleHeadings.push({ el, top });
1281
+ } else {
1282
+ scrolledPast.push({ el, top });
1283
+ }
1284
+ }
1285
+
1286
+ let bestEl;
1287
+ if (visibleHeadings.length > 0) {
1288
+ // Preference to the topmost heading visible in the viewport/reading zone
1289
+ visibleHeadings.sort((a, b) => a.top - b.top);
1290
+ bestEl = visibleHeadings[0].el;
1291
+ } else {
1292
+ // If all visible candidates have their headings scrolled past top (< 0),
1293
+ // pick the one whose top is closest to 0 (the section currently filling the reading area)
1294
+ scrolledPast.sort((a, b) => b.top - a.top);
1295
+ bestEl = scrolledPast[0].el;
1296
+ }
1297
+
1298
+ const targetId = bestEl.id;
1299
+ if (this._activeElementId === targetId) {
1300
+ return;
1301
+ }
1302
+ this._activeElementId = targetId;
1303
+
1304
+ if (this._intersectionRaf) {
1305
+ cancelAnimationFrame(this._intersectionRaf);
1306
+ }
1307
+
1308
+ this._intersectionRaf = requestAnimationFrame(() => {
1309
+ // 1. Dispatch custom event so outer portals/host apps can sync active state
1310
+ let resolvedOperationId = targetId;
1311
+ if (targetId.startsWith("tag--") && this.resolvedSpec?.tags) {
1312
+ const matchingTag = this.resolvedSpec.tags.find((t) => t.elementId === targetId);
1313
+ if (matchingTag?.paths?.[0]?.elementId) {
1314
+ resolvedOperationId = matchingTag.paths[0].elementId;
1315
+ }
1316
+ }
1317
+
1318
+ this.dispatchEvent(
1319
+ new CustomEvent("nav-active-change", {
1320
+ detail: {
1321
+ id: targetId,
1322
+ elementId: targetId,
1323
+ operationId: resolvedOperationId,
1324
+ },
1325
+ bubbles: true,
1326
+ composed: true,
1327
+ }),
1328
+ );
1329
+
1330
+ if (this.updateRoute === "true") {
1331
+ this.replaceHistoryState(targetId);
1332
+ }
1333
+
1334
+ // 2. If internal navbar is active (show-side-nav="true"), update Shadow DOM elements
1335
+ const newNavEl = this.shadowRoot.getElementById(`link-${targetId}`);
1336
+ if (newNavEl) {
1337
+ const oldNavEl = this.shadowRoot.querySelector(
1338
+ ".nav-bar-tag.active, .nav-bar-path.active, .nav-bar-info.active, .nav-bar-h1.active, .nav-bar-h2.active, .operations.active",
1339
+ );
1340
+
1341
+ if (oldNavEl && oldNavEl !== newNavEl) {
1342
+ oldNavEl.classList.remove("active");
1343
+ oldNavEl.part.remove("section-navbar-active-item");
1344
+ }
1345
+
1346
+ expandParentNavContainers(newNavEl);
1347
+ newNavEl.classList.add("active");
1348
+ newNavEl.part.add("section-navbar-active-item");
1349
+
1350
+ // Scroll sidebar only if the active item is out of view
1351
+ const navScrollEl = newNavEl.closest(".nav-scroll");
1352
+ if (navScrollEl) {
1353
+ const headerEl =
1354
+ navScrollEl.querySelector(".nav-bar-header") ||
1355
+ navScrollEl.querySelector(".nav-bar-search-container");
1356
+ const topOffset = headerEl ? headerEl.offsetHeight : 0;
1357
+
1358
+ if (newNavEl.offsetTop <= topOffset + 30) {
1359
+ if (navScrollEl.scrollTop > 0) {
1360
+ const behavior = this.scrollBehavior === "auto" ? "auto" : "smooth";
1361
+ navScrollEl.scrollTo({ top: 0, behavior });
1362
+ }
1363
+ } else {
1364
+ const scrollRect = navScrollEl.getBoundingClientRect();
1365
+ const itemRect = newNavEl.getBoundingClientRect();
1366
+ const isOutOfView =
1367
+ itemRect.top < scrollRect.top + topOffset + 15 ||
1368
+ itemRect.bottom > scrollRect.bottom - 40;
1369
+ if (isOutOfView) {
1370
+ const behavior = this.scrollBehavior === "auto" ? "auto" : "smooth";
1371
+ // Land the item below the sticky search header (which sits inside the scroll area), not under it
1372
+ const targetTop =
1373
+ itemRect.top - scrollRect.top + navScrollEl.scrollTop - topOffset - 20;
1374
+ navScrollEl.scrollTo({ top: targetTop, behavior });
1375
+ }
1376
+ }
1377
+ }
1378
+ }
1379
+ });
1380
+ }
1381
+
1382
+ // Called by anchor tags created using markdown
1383
+ handleHref(e) {
1384
+ if (e.target.tagName.toLowerCase() === "a") {
1385
+ if (e.target.getAttribute("href").startsWith("#")) {
1386
+ const gotoEl = this.shadowRoot.getElementById(
1387
+ e.target.getAttribute("href").replace("#", ""),
1388
+ );
1389
+ if (gotoEl) {
1390
+ const mainContentEl = this.shadowRoot.querySelector(".main-content");
1391
+ if (this.autoHeight === "true" || this.hasAttribute("auto-height")) {
1392
+ this.scrollAutoHeightTarget(gotoEl, gotoEl.id);
1393
+ } else if (mainContentEl) {
1394
+ const containerRect = mainContentEl.getBoundingClientRect();
1395
+ const targetRect = gotoEl.getBoundingClientRect();
1396
+ const targetTop = targetRect.top - containerRect.top + mainContentEl.scrollTop;
1397
+ mainContentEl.scrollTo({ top: targetTop, behavior: this.scrollBehavior });
1398
+ } else {
1399
+ gotoEl.scrollIntoView({ behavior: this.scrollBehavior, block: "start" });
1400
+ }
1401
+ }
1402
+ }
1403
+ }
1404
+ }
1405
+
1406
+ /**
1407
+ * Height of the area at the top of the scroll root that sticky or fixed page headers cover, in px.
1408
+ * Only applies with auto-height, where the page (or `scroll-container`) scrolls instead of RapiDoc.
1409
+ * Uses the `scroll-offset` attribute when set, otherwise the scroll root's `scroll-padding-top`,
1410
+ * so pages that already reserve space for their header need no extra configuration.
1411
+ */
1412
+ getScrollOffset(): number {
1413
+ if (
1414
+ !(this.autoHeight === "true" || this.hasAttribute("auto-height")) ||
1415
+ typeof window === "undefined"
1416
+ ) {
1417
+ return 0;
1418
+ }
1419
+ const explicit = parseFloat(this.scrollOffset);
1420
+ if (Number.isFinite(explicit)) {
1421
+ return explicit;
1422
+ }
1423
+ const root: HTMLElement =
1424
+ this._scrollRootContainer ||
1425
+ (document.scrollingElement as HTMLElement) ||
1426
+ document.documentElement;
1427
+ const padding = getComputedStyle(root).scrollPaddingTop;
1428
+ const value = parseFloat(padding);
1429
+ if (!Number.isFinite(value)) {
1430
+ return 0;
1431
+ }
1432
+ return padding.endsWith("%") ? (value / 100) * root.clientHeight : value;
1433
+ }
1434
+
1435
+ /**
1436
+ * auto-height: scrolls the page, or the `scroll-container`, so that `targetEl` starts just below
1437
+ * the area covered by sticky headers (see getScrollOffset).
1438
+ */
1439
+ scrollAutoHeightTarget(targetEl: HTMLElement, targetId: string) {
1440
+ const top = targetEl.getBoundingClientRect().top - this.getScrollOffset();
1441
+ const container = this._scrollRootContainer;
1442
+ if (container) {
1443
+ const targetTop = top - container.getBoundingClientRect().top + container.scrollTop;
1444
+ this.lockObserverDuringScroll(targetEl, targetId, targetTop);
1445
+ container.scrollTo({ top: targetTop, behavior: this.getContentScrollBehavior() });
1446
+ } else {
1447
+ const targetTop = top + window.scrollY;
1448
+ this.lockObserverDuringScroll(targetEl, targetId, targetTop);
1449
+ window.scrollTo({ top: targetTop, behavior: this.getContentScrollBehavior() });
1450
+ }
1451
+ }
1452
+
1453
+ /**
1454
+ * Temporarily deactivates the IntersectionObserver during navigation/programmatic scrolling,
1455
+ * preventing intermediate elements from hijacking the active navbar item while smooth scrolling.
1456
+ * Re-activates the observer once scrolling finishes (via 'scrollend', debounced scroll fallback, or safety timeout).
1457
+ *
1458
+ * @param {HTMLElement | null} targetEl - The element being scrolled to
1459
+ * @param {string} targetId - Element / operation identifier
1460
+ * @param {number} [targetTop] - Target scroll offset if known
1461
+ */
1462
+ lockObserverDuringScroll(
1463
+ targetEl: HTMLElement | null,
1464
+ targetId: string = "",
1465
+ targetTop?: number,
1466
+ ) {
1467
+ if (this._intersectionRaf) {
1468
+ cancelAnimationFrame(this._intersectionRaf);
1469
+ this._intersectionRaf = null;
1470
+ }
1471
+
1472
+ if (typeof this._observerLockCleanup === "function") {
1473
+ this._observerLockCleanup();
1474
+ this._observerLockCleanup = null;
1475
+ }
1476
+ if (this._observerLockTimeout) {
1477
+ clearTimeout(this._observerLockTimeout);
1478
+ this._observerLockTimeout = null;
1479
+ }
1480
+
1481
+ // onIntersect tracks elements by their DOM id; targetId can be a shorter slug of the same element
1482
+ // (e.g. "get-route-estimate" for "route-planning--get-route-estimate"). Keying by the slug would leave
1483
+ // an entry the observer never removes, pinning the active item to this element.
1484
+ const observedId = targetEl?.id || targetId;
1485
+ this.isIntersectionObserverActive = false;
1486
+ if (this._intersectingElements) {
1487
+ this._intersectingElements.clear();
1488
+ if (targetEl && observedId) {
1489
+ this._intersectingElements.set(observedId, targetEl);
1490
+ }
1491
+ }
1492
+ if (observedId) {
1493
+ this._activeElementId = observedId;
1494
+ }
1495
+
1496
+ const isAutoHeight = this.autoHeight === "true" || this.hasAttribute("auto-height");
1497
+ const scrollContainer: any = isAutoHeight
1498
+ ? this._scrollRootContainer || (typeof window !== "undefined" ? window : null)
1499
+ : this.shadowRoot?.querySelector(".main-content") ||
1500
+ (typeof window !== "undefined" ? window : null);
1501
+
1502
+ if (!scrollContainer || typeof window === "undefined") {
1503
+ this.isIntersectionObserverActive = true;
1504
+ return;
1505
+ }
1506
+
1507
+ let isUnlocked = false;
1508
+ const unlock = () => {
1509
+ if (isUnlocked) return;
1510
+ isUnlocked = true;
1511
+ if (cleanup) {
1512
+ cleanup();
1513
+ cleanup = null;
1514
+ this._observerLockCleanup = null;
1515
+ }
1516
+ if (this._observerLockTimeout) {
1517
+ clearTimeout(this._observerLockTimeout);
1518
+ this._observerLockTimeout = null;
1519
+ }
1520
+
1521
+ if (targetEl && observedId && this._intersectingElements) {
1522
+ this._intersectingElements.set(observedId, targetEl);
1523
+ }
1524
+ this.isIntersectionObserverActive = true;
1525
+ };
1526
+
1527
+ let debounceTimer: any = null;
1528
+ const onScroll = () => {
1529
+ clearTimeout(debounceTimer);
1530
+ debounceTimer = setTimeout(unlock, 120);
1531
+ };
1532
+
1533
+ const onScrollEnd = () => {
1534
+ unlock();
1535
+ };
1536
+
1537
+ let cleanup: any = () => {
1538
+ scrollContainer.removeEventListener?.("scrollend", onScrollEnd);
1539
+ window.removeEventListener?.("scrollend", onScrollEnd);
1540
+ scrollContainer.removeEventListener?.("scroll", onScroll);
1541
+ window.removeEventListener?.("scroll", onScroll);
1542
+ clearTimeout(debounceTimer);
1543
+ };
1544
+ this._observerLockCleanup = cleanup;
1545
+
1546
+ if ("onscrollend" in window) {
1547
+ scrollContainer.addEventListener?.("scrollend", onScrollEnd, { once: true, passive: true });
1548
+ window.addEventListener?.("scrollend", onScrollEnd, { once: true, passive: true });
1549
+ }
1550
+ scrollContainer.addEventListener?.("scroll", onScroll, { passive: true });
1551
+ window.addEventListener?.("scroll", onScroll, { passive: true });
1552
+
1553
+ const currentScrollTop =
1554
+ scrollContainer === window ? window.scrollY : scrollContainer.scrollTop || 0;
1555
+ const isZeroDistance =
1556
+ typeof targetTop === "number" && Math.abs(targetTop - currentScrollTop) < 3;
1557
+ const maxSafetyDelay = isZeroDistance
1558
+ ? 60
1559
+ : this.getContentScrollBehavior() === "smooth"
1560
+ ? 1200
1561
+ : 100;
1562
+ this._observerLockTimeout = setTimeout(unlock, maxSafetyDelay);
1563
+ }
1564
+
1565
+ /**
1566
+ * Called by
1567
+ * - onClick of Navigation Bar
1568
+ * - onClick of Advanced Search items
1569
+ *
1570
+ * Functionality:
1571
+ * 1. First deactivate IntersectionObserver
1572
+ * 2. Scroll to the element
1573
+ * 3. Activate IntersectionObserver (after scroll completes)
1574
+ */
1575
+ async scrollToEventTarget(event, scrollNavItemToView = true) {
1576
+ if (!(event.type === "click" || (event.type === "keyup" && event.keyCode === 13))) {
1577
+ return;
1578
+ }
1579
+ const navEl = ((event.target as HTMLElement)?.closest?.("[data-content-id]") ||
1580
+ event.target) as HTMLElement;
1581
+ if (!navEl?.dataset?.contentId) {
1582
+ return;
1583
+ }
1584
+ if (this.renderStyle === "focused") {
1585
+ const requestEl = this.shadowRoot.querySelector("api-request");
1586
+ if (typeof requestEl?.beforeNavigationFocusedMode === "function") {
1587
+ requestEl.beforeNavigationFocusedMode();
1588
+ }
1589
+ }
1590
+ const isFirstNavItem = navEl.dataset.firstNavItem === "true";
1591
+ this.scrollToPath(navEl.dataset.contentId, true, scrollNavItemToView, isFirstNavItem);
1592
+ }
1593
+
1594
+ // Public Method - Returns the DOM element in ShadowRoot matching elementId, operationId, or slug
1595
+ getTargetElement(elementId: string): HTMLElement | null {
1596
+ if (!elementId || !this.shadowRoot) return null;
1597
+ return (this.shadowRoot.getElementById(elementId) ||
1598
+ this.shadowRoot.querySelector(`[data-op-slug="${CSS.escape(elementId)}"]`) ||
1599
+ this.shadowRoot.querySelector(`[id$="--${CSS.escape(elementId)}"]`) ||
1600
+ this.shadowRoot.getElementById(
1601
+ elementId.replace(/^[a-z0-9_-]+--/, ""),
1602
+ )) as HTMLElement | null;
1603
+ }
1604
+
1605
+ /**
1606
+ * Every operation can also be reached by its method + path anchor (methodPathSlug, e.g.
1607
+ * "get-routes-id-estimate"), which stays the same when its operationId or summary changes. The
1608
+ * comparison ignores case and punctuation, so older RapiDoc anchors ("get-/routes/-id-/estimate") work too.
1609
+ * Returns the operation's own element id for such an anchor, otherwise elementId unchanged.
1610
+ */
1611
+ resolveOperationAlias(elementId: string): string {
1612
+ if (!elementId || this.getTargetElement(elementId)) return elementId;
1613
+ const normalize = (id: string) =>
1614
+ id
1615
+ .toLowerCase()
1616
+ .replace(/[^a-z0-9]+/g, "-")
1617
+ .replace(/^-+|-+$/g, "");
1618
+ const wanted = normalize(elementId);
1619
+ for (const tag of this.resolvedSpec?.tags || []) {
1620
+ const op = tag.paths?.find((p) => normalize(methodPathSlug(p.method, p.path)) === wanted);
1621
+ if (op) return op.elementId;
1622
+ }
1623
+ return elementId;
1624
+ }
1625
+
1626
+ // Public Method - Binds an external navbar to this <rapi-doc> element
1627
+ bindNavBar(sidebar: HTMLElement | string, options?: any) {
1628
+ return bindNavBar(sidebar, this, options);
1629
+ }
1630
+
1631
+ // Public Method (scrolls to a given path and highlights the left-nav selection)
1632
+ async scrollTo(elementId) {
1633
+ return this.scrollToPath(elementId);
1634
+ }
1635
+
1636
+ // Public Method (scrolls to a given path and highlights the left-nav selection)
1637
+ async scrollToPath(
1638
+ elementId,
1639
+ expandPath = true,
1640
+ scrollNavItemToView = true,
1641
+ isFirstNavItem = false,
1642
+ ) {
1643
+ elementId = this.resolveOperationAlias(elementId);
1644
+ if (this.renderStyle === "focused") {
1645
+ // for focused mode update this.focusedElementId to update the rendering, else it wont find the needed html elements
1646
+ // focusedElementId will get validated in the template
1647
+ this.focusedElementId = elementId;
1648
+ await sleep(0);
1649
+ }
1650
+ if (this.renderStyle === "view") {
1651
+ this.expandAndGotoOperation(elementId, expandPath, true);
1652
+ } else {
1653
+ const contentEl = this.getTargetElement(elementId);
1654
+ const isValidElementId = Boolean(contentEl);
1655
+ if (contentEl) {
1656
+ const firstNavEl = this.shadowRoot.querySelector(".first-nav-item");
1657
+ const isFirst =
1658
+ isFirstNavItem || (firstNavEl && firstNavEl.dataset.contentId === elementId);
1659
+ const mainContentEl = this.shadowRoot.querySelector(".main-content");
1660
+ let targetTop: number;
1661
+
1662
+ if (this.autoHeight === "true" || this.hasAttribute("auto-height")) {
1663
+ this.scrollAutoHeightTarget(contentEl, elementId);
1664
+ } else if (isFirst) {
1665
+ targetTop = 0;
1666
+ this.lockObserverDuringScroll(contentEl, elementId, targetTop);
1667
+ if (mainContentEl) {
1668
+ mainContentEl.scrollTo({ top: 0, behavior: this.getContentScrollBehavior() });
1669
+ } else {
1670
+ contentEl.scrollIntoView({ behavior: this.getContentScrollBehavior(), block: "start" });
1671
+ }
1672
+ } else if (mainContentEl) {
1673
+ const containerRect = mainContentEl.getBoundingClientRect();
1674
+ const targetRect = contentEl.getBoundingClientRect();
1675
+ targetTop = targetRect.top - containerRect.top + mainContentEl.scrollTop;
1676
+ this.lockObserverDuringScroll(contentEl, elementId, targetTop);
1677
+ mainContentEl.scrollTo({ top: targetTop, behavior: this.getContentScrollBehavior() });
1678
+ } else {
1679
+ this.lockObserverDuringScroll(contentEl, elementId);
1680
+ contentEl.scrollIntoView({ behavior: this.getContentScrollBehavior(), block: "start" });
1681
+ }
1682
+ }
1683
+ if (isValidElementId) {
1684
+ // In focused mode, <api-request> and <api-response> component instances are reused across endpoint switches
1685
+ // to avoid expensive DOM re-creation. Reset their selection state so selections from the previous operation
1686
+ // (such as alternative request bodies or non-default status codes) do not persist into the new operation.
1687
+ if (this.renderStyle === "focused") {
1688
+ const requestEl = this.shadowRoot.querySelector("api-request");
1689
+ if (typeof requestEl?.afterNavigationFocusedMode === "function") {
1690
+ requestEl.afterNavigationFocusedMode();
1691
+ }
1692
+ const responseEl = this.shadowRoot.querySelector("api-response");
1693
+ if (typeof responseEl?.resetSelection === "function") {
1694
+ responseEl.resetSelection();
1695
+ }
1696
+ }
1697
+
1698
+ // Update Location Hash
1699
+ this._activeElementId = contentEl?.id || elementId;
1700
+ this.dispatchEvent(
1701
+ new CustomEvent("nav-active-change", {
1702
+ detail: {
1703
+ id: elementId,
1704
+ elementId,
1705
+ },
1706
+ bubbles: true,
1707
+ composed: true,
1708
+ }),
1709
+ );
1710
+ if (this.updateRoute === "true") {
1711
+ this.replaceHistoryState(elementId);
1712
+ }
1713
+
1714
+ // Update NavBar View and Styles
1715
+ const newNavEl = this.shadowRoot.getElementById(`link-${elementId}`);
1716
+
1717
+ if (newNavEl) {
1718
+ expandParentNavContainers(newNavEl);
1719
+ if (scrollNavItemToView) {
1720
+ const navScrollEl = this.shadowRoot.querySelector(".nav-scroll");
1721
+ if (navScrollEl) {
1722
+ const scrollRect = navScrollEl.getBoundingClientRect();
1723
+ const itemRect = newNavEl.getBoundingClientRect();
1724
+ const targetTop =
1725
+ itemRect.top -
1726
+ scrollRect.top +
1727
+ navScrollEl.scrollTop -
1728
+ navScrollEl.clientHeight / 2;
1729
+ navScrollEl.scrollTo({ top: targetTop, behavior: this.scrollBehavior });
1730
+ } else {
1731
+ newNavEl.scrollIntoView({ behavior: this.scrollBehavior, block: "center" });
1732
+ }
1733
+ }
1734
+ await sleep(0);
1735
+ const oldNavEl = this.shadowRoot.querySelector(
1736
+ ".nav-bar-tag.active, .nav-bar-path.active, .nav-bar-info.active, .nav-bar-h1.active, .nav-bar-h2.active, .operations.active",
1737
+ );
1738
+ if (oldNavEl) {
1739
+ oldNavEl.classList.remove("active");
1740
+ oldNavEl.part.remove("active");
1741
+ oldNavEl.part.remove("section-navbar-active-item");
1742
+ }
1743
+ newNavEl.classList.add("active"); // must add the class after scrolling
1744
+ newNavEl.part.add("section-navbar-active-item");
1745
+ // this.requestUpdate();
1746
+ }
1747
+ }
1748
+ }
1749
+ }
1750
+
1751
+ // Public Method - to update security-scheme of type http
1752
+ setHttpUserNameAndPassword(securitySchemeId, username, password) {
1753
+ return applyApiKey.call(this, securitySchemeId, username, password);
1754
+ }
1755
+
1756
+ // Public Method - to update security-scheme of type apiKey or OAuth.
1757
+ // options.note: where the key came from (e.g. "Using your sign-in"), shown in place of the scheme's form
1758
+ setApiKey(securitySchemeId, apiKeyValue, options: { note?: string } = {}) {
1759
+ return applyApiKey.call(this, securitySchemeId, "", "", apiKeyValue, options?.note || "");
1760
+ }
1761
+
1762
+ // Public Method
1763
+ removeAllSecurityKeys() {
1764
+ return onClearAllApiKeys.call(this);
1765
+ }
1766
+
1767
+ // Public Method
1768
+ setApiServer(apiServerUrl) {
1769
+ // return apiServerUrl;
1770
+ return setApiServer.call(this, apiServerUrl);
1771
+ }
1772
+
1773
+ // True when the `mcp` attribute asks for the Export MCP Server button
1774
+ get mcpEnabled() {
1775
+ return parseMcpEnabled(this.mcp);
1776
+ }
1777
+
1778
+ /**
1779
+ * The <rapi-flow> on the page whose Arazzo document the export takes workflow tools from: the
1780
+ * one whose `for` names this element, else one without `for`. Nothing when rapi-flow is not
1781
+ * loaded.
1782
+ */
1783
+ private findArazzoDocument() {
1784
+ if (typeof customElements === "undefined" || !customElements.get("rapi-flow")) return undefined;
1785
+ const flows = [...document.querySelectorAll("rapi-flow")] as any[];
1786
+ return (
1787
+ flows.find((f) => this.id && f.getAttribute("for") === this.id && f.arazzoDocument) ||
1788
+ flows.find((f) => !f.getAttribute("for") && f.arazzoDocument)
1789
+ );
1790
+ }
1791
+
1792
+ private mcpOptions(options) {
1793
+ const flow = this.findArazzoDocument();
1794
+ return {
1795
+ serverName: this.mcpServerName,
1796
+ arazzo: flow?.arazzoDocument,
1797
+ // <rapi-flow> carries the workflow runtime; this bundle does not
1798
+ workflowRuntime: flow?.workflowRuntimeSource,
1799
+ ...options,
1800
+ };
1801
+ }
1802
+
1803
+ // Public Method - Builds the MCP manifest and the files of the exported project in memory
1804
+ generateMcpServer(options = {}) {
1805
+ const spec = this.resolvedSpec || this.openApiSpec;
1806
+ if (!spec) return null;
1807
+ return mcpServerGenerator(spec, this.mcpOptions(options));
1808
+ }
1809
+
1810
+ // Public Method - Downloads the exported MCP project as a zip
1811
+ downloadMcpServer(options = {}) {
1812
+ const result = this.generateMcpServer(options);
1813
+ if (!result) return;
1814
+ const blob = new Blob([createZip(result.files)], { type: "application/zip" });
1815
+ const link = document.createElement("a");
1816
+ link.href = URL.createObjectURL(blob);
1817
+ link.download = `${result.manifest.server.name.replace(/_/g, "-")}-mcp.zip`;
1818
+ document.body.appendChild(link);
1819
+ link.click();
1820
+ document.body.removeChild(link);
1821
+ URL.revokeObjectURL(link.href);
1822
+ }
1823
+
1824
+ // Public Method - Returns token budget diagnostics for MCP tooling
1825
+ getMcpBudget(options = {}) {
1826
+ const spec = this.resolvedSpec || this.openApiSpec;
1827
+ if (!spec) return null;
1828
+ return estimateTokenCost(buildMcpManifest(spec, this.mcpOptions(options)));
1829
+ }
1830
+
1831
+ // Event handler for Advanced Search text-inputs and checkboxes
1832
+ onAdvancedSearch(ev, delay) {
1833
+ const eventTargetEl = ev.target;
1834
+ clearTimeout(this.timeoutId);
1835
+ this.timeoutId = setTimeout(() => {
1836
+ let searchInputEl;
1837
+ if (eventTargetEl.type === "text") {
1838
+ searchInputEl = eventTargetEl;
1839
+ } else {
1840
+ searchInputEl = eventTargetEl
1841
+ .closest(".advanced-search-options")
1842
+ .querySelector("input[type=text]");
1843
+ }
1844
+ const searchOptions = [
1845
+ ...eventTargetEl.closest(".advanced-search-options").querySelectorAll("input:checked"),
1846
+ ].map((v) => v.id);
1847
+ this.advancedSearchMatches = advancedSearch(
1848
+ searchInputEl.value,
1849
+ this.resolvedSpec.tags,
1850
+ searchOptions,
1851
+ );
1852
+ }, delay);
1853
+ }
1854
+ }