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