rapidoc 11.0.1 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +171 -54
  2. package/dist/rapidoc-flow-min.js +580 -0
  3. package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
  4. package/dist/rapidoc-min.js +645 -202
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +57 -42
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +912 -0
  10. package/src/components/api-response.ts +514 -0
  11. package/src/components/json-tree.ts +171 -0
  12. package/src/components/schema-base-element.ts +329 -0
  13. package/src/components/schema-table.ts +475 -0
  14. package/src/components/schema-tree.ts +393 -0
  15. package/src/components/tag-input.ts +180 -0
  16. package/src/flow.js +3 -0
  17. package/src/index.js +6 -0
  18. package/src/oauth-receiver.ts +29 -0
  19. package/src/rapi-flow.ts +1178 -0
  20. package/src/rapidoc-mini.ts +603 -0
  21. package/src/rapidoc.ts +1854 -0
  22. package/src/styles/README.md +96 -0
  23. package/src/styles/auth-styles.js +318 -0
  24. package/src/styles/border-styles.js +24 -0
  25. package/src/styles/css-bundle.js +129 -0
  26. package/src/styles/custom-styles.js +8 -0
  27. package/src/styles/dialog-box-styles.js +102 -0
  28. package/src/styles/endpoint-styles.js +564 -0
  29. package/src/styles/flex-styles.js +16 -0
  30. package/src/styles/flow-styles.js +248 -0
  31. package/src/styles/font-styles.js +432 -0
  32. package/src/styles/info-styles.js +36 -0
  33. package/src/styles/input-styles.js +555 -0
  34. package/src/styles/layout-styles.js +396 -0
  35. package/src/styles/microlighter-styles.js +99 -0
  36. package/src/styles/nav-styles.js +633 -0
  37. package/src/styles/param-list-styles.js +261 -0
  38. package/src/styles/schema-styles.js +296 -0
  39. package/src/styles/tab-styles.js +106 -0
  40. package/src/styles/theme-tokens.js +582 -0
  41. package/src/templates/README.md +89 -0
  42. package/src/templates/advanced-search-dialog-template.js +94 -0
  43. package/src/templates/api-response-template.js +223 -0
  44. package/src/templates/callback-template.js +86 -0
  45. package/src/templates/code-samples-template.js +52 -0
  46. package/src/templates/components-template.js +86 -0
  47. package/src/templates/downloads-template.js +70 -0
  48. package/src/templates/endpoint-template.js +377 -0
  49. package/src/templates/expanded-endpoint-template.js +270 -0
  50. package/src/templates/focused-endpoint-template.js +146 -0
  51. package/src/templates/header-template.js +92 -0
  52. package/src/templates/logo-template.js +21 -0
  53. package/src/templates/main-body-template.js +102 -0
  54. package/src/templates/navbar-template.js +498 -0
  55. package/src/templates/overview-template.js +104 -0
  56. package/src/templates/request-body-template.js +673 -0
  57. package/src/templates/request-params-template.js +485 -0
  58. package/src/templates/security-scheme-template.js +1272 -0
  59. package/src/templates/server-template.js +153 -0
  60. package/src/utils/README.md +146 -0
  61. package/src/utils/arazzo/README.md +119 -0
  62. package/src/utils/arazzo/components.js +15 -0
  63. package/src/utils/arazzo/graph.js +269 -0
  64. package/src/utils/arazzo/index.js +52 -0
  65. package/src/utils/arazzo/operations.js +106 -0
  66. package/src/utils/arazzo/parse.js +196 -0
  67. package/src/utils/arazzo/refs.js +40 -0
  68. package/src/utils/arazzo/svg.js +707 -0
  69. package/src/utils/auto-color-scheme.js +99 -0
  70. package/src/utils/bind-navbar.js +175 -0
  71. package/src/utils/color-utils.js +59 -0
  72. package/src/utils/common-utils.js +191 -0
  73. package/src/utils/curl-utils.js +126 -0
  74. package/src/utils/highlighter.js +419 -0
  75. package/src/utils/markdown-utils.js +110 -0
  76. package/src/utils/mcp/README.md +155 -0
  77. package/src/utils/mcp/common.js +166 -0
  78. package/src/utils/mcp/manifest.js +382 -0
  79. package/src/utils/mcp/project/server.ts +53 -0
  80. package/src/utils/mcp/project.js +88 -0
  81. package/src/utils/mcp/runtime.js +289 -0
  82. package/src/utils/mcp/schema.js +98 -0
  83. package/src/utils/mcp/workflow-runtime.js +374 -0
  84. package/src/utils/mcp/workflows.js +240 -0
  85. package/src/utils/mcp/zip.js +102 -0
  86. package/src/utils/mcp-generator.js +47 -0
  87. package/src/utils/mock-interceptor.js +557 -0
  88. package/src/utils/oauth-utils.js +87 -0
  89. package/src/utils/request-utils.js +264 -0
  90. package/src/utils/response-utils.js +203 -0
  91. package/src/utils/sanitize.js +57 -0
  92. package/src/utils/schema-utils.js +1954 -0
  93. package/src/utils/slugger.js +187 -0
  94. package/src/utils/spec-parser.js +941 -0
  95. package/src/utils/url-utils.js +246 -0
  96. package/src/utils/xml-utils.js +61 -0
@@ -0,0 +1,912 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import { guard } from "lit/directives/guard.js";
4
+ import { scheduleHighlight } from "~/utils/highlighter";
5
+ import ParamListStyles from "~/styles/param-list-styles";
6
+ import FlexStyles from "~/styles/flex-styles";
7
+ import InputStyles from "~/styles/input-styles";
8
+ import FontStyles from "~/styles/font-styles";
9
+ import BorderStyles from "~/styles/border-styles";
10
+ import TabStyles from "~/styles/tab-styles";
11
+ import MicrolighterStyles from "~/styles/microlighter-styles";
12
+ import CustomStyles from "~/styles/custom-styles";
13
+ import { constructFetchURL } from "~/utils/url-utils";
14
+ import { generateCurlCommand } from "~/utils/curl-utils";
15
+ import {
16
+ constructFetchHeaders,
17
+ buildUrlEncodedBody,
18
+ buildMultipartFormData,
19
+ serializeMultipartBody,
20
+ } from "~/utils/request-utils";
21
+ import { processFetchResponse } from "../utils/response-utils.js";
22
+ import { inputParametersTemplate } from "~/templates/request-params-template";
23
+ import requestBodyTemplate from "~/templates/request-body-template";
24
+ import apiCallTemplate from "~/templates/api-response-template";
25
+
26
+ @customElement("api-request")
27
+ export default class ApiRequest extends LitElement {
28
+ @property({ type: Object }) config: any;
29
+ @property({ type: String, attribute: "server-url" }) serverUrl = "";
30
+ @property({ type: Array }) servers: any = [];
31
+ @property({ type: String }) method = "";
32
+ @property({ type: String }) path = "";
33
+ @property({ type: Array }) security: any = [];
34
+ @property({ type: Array }) parameters: any = [];
35
+ @property({ type: Object }) request_body: any;
36
+ @property({ type: Array }) api_keys: any = [];
37
+ @property({ type: Object }) parser: any;
38
+ @property({ type: String }) accept = "";
39
+ @property({ type: String }) callback = "";
40
+ @property({ type: String }) webhook = "";
41
+ @property({ type: String, attribute: "fill-request-fields-with-example" })
42
+ fillRequestFieldsWithExample = "";
43
+ @property({ type: String, attribute: "allow-try" }) allowTry = "true";
44
+ @property({ type: String, attribute: "show-curl-before-try" }) showCurlBeforeTry = "false";
45
+ @property({ type: String, attribute: "render-style" }) renderStyle = "view";
46
+ @property({ type: String, attribute: "schema-style" }) schemaStyle = "tree";
47
+ @property({ type: String, attribute: "active-schema-tab" }) activeSchemaTab = "schema";
48
+ @property({
49
+ type: Object,
50
+ converter: {
51
+ fromAttribute: (attr: string) => JSON.parse(attr),
52
+ toAttribute: (prop: any) => JSON.stringify(prop),
53
+ },
54
+ attribute: "active-parameter-schema-tabs",
55
+ })
56
+ activeParameterSchemaTabs: any = {};
57
+ @property({ type: Number, attribute: "schema-expand-level" }) schemaExpandLevel = 99999;
58
+ @property({ type: String, attribute: "schema-description-expanded" }) schemaDescriptionExpanded =
59
+ "false";
60
+ @property({ type: String, attribute: "allow-schema-description-expand-toggle" })
61
+ allowSchemaDescriptionExpandToggle = "false";
62
+ @property({ type: String, attribute: "schema-hide-read-only" }) schemaHideReadOnly = "true";
63
+ @property({ type: String, attribute: "schema-hide-write-only" }) schemaHideWriteOnly = "false";
64
+ @property({ type: String, attribute: "fetch-credentials" }) fetchCredentials = "";
65
+
66
+ // Internal reactive state
67
+ @state() responseMessage: any = "";
68
+ @state() responseText = "";
69
+ @state() responseHeaders: any = "";
70
+ @state() responseStatus = "success";
71
+ @state() responseUrl = "";
72
+ @state() curlSyntax = "";
73
+ @state() responseIsBlob = false;
74
+ @state() responseBlobType = "";
75
+ @state() responseBlobUrl = "";
76
+ @state() respContentDisposition = "";
77
+ @state() activeResponseTab = "response"; // allowed values: response, headers, curl
78
+ @state() selectedRequestBodyType = "";
79
+ @state() selectedRequestBodyExample = "";
80
+ @state() fileInputKeys: any = {};
81
+ @state() loading = false;
82
+ // Try-bar buttons follow the fields: Fill Example shows only when a field has an example,
83
+ // Clear is enabled only while a field holds a value
84
+ @state() hasExampleToFill = false;
85
+ @state() hasValueToClear = false;
86
+
87
+ activeAbortController: any = null;
88
+
89
+ constructor() {
90
+ super();
91
+ this.responseMessage = "";
92
+ this.responseStatus = "success";
93
+ this.responseHeaders = "";
94
+ this.responseText = "";
95
+ this.responseUrl = "";
96
+ this.curlSyntax = "";
97
+ this.responseIsBlob = false;
98
+ this.responseBlobType = "";
99
+ this.responseBlobUrl = "";
100
+ this.respContentDisposition = "";
101
+ this.activeResponseTab = "response";
102
+ this.selectedRequestBodyType = "";
103
+ this.selectedRequestBodyExample = "";
104
+ this.activeParameterSchemaTabs = {};
105
+ this.fileInputKeys = {};
106
+ this.loading = false;
107
+ }
108
+
109
+ static get styles() {
110
+ return [
111
+ ParamListStyles,
112
+ InputStyles,
113
+ FontStyles,
114
+ FlexStyles,
115
+ BorderStyles,
116
+ TabStyles,
117
+ MicrolighterStyles,
118
+ css`
119
+ *,
120
+ *:before,
121
+ *:after {
122
+ box-sizing: border-box;
123
+ }
124
+ :where(button, input[type='checkbox'], [tabindex='0']):focus-visible {
125
+ box-shadow: var(--focus-shadow);
126
+ }
127
+ :where(input[type='text'], input[type='password'], select, textarea):focus-visible {
128
+ border-color: var(--primary);
129
+ }
130
+ tag-input:focus-within {
131
+ outline: 1px solid;
132
+ }
133
+ .top-gap {
134
+ margin-top: 24px;
135
+ }
136
+ /* The first label under the Request heading belongs to it; no section gap */
137
+ .request-panel > div > .top-gap:first-child,
138
+ .request-panel > div > .request-body-container:first-child > .top-gap {
139
+ margin-top: 4px;
140
+ }
141
+
142
+ .textarea {
143
+ min-height: 220px;
144
+ padding: 5px;
145
+ resize: vertical;
146
+ direction: ltr;
147
+ }
148
+
149
+ .response-message {
150
+ font-weight: bold;
151
+ text-overflow: ellipsis;
152
+
153
+ &.error {
154
+ color: var(--destructive);
155
+ }
156
+ &.success {
157
+ color: var(--blue);
158
+ }
159
+ }
160
+
161
+ .file-input-container {
162
+ align-items: flex-start;
163
+ width: 100%;
164
+
165
+ /* Each file input spans the full width; its remove button sits inside the box, at the right edge */
166
+ & .input-set {
167
+ position: relative;
168
+ width: 100%;
169
+ display: flex;
170
+ align-items: center;
171
+
172
+ &:first-child .file-input-remove-btn {
173
+ display: none;
174
+ }
175
+
176
+ &:not(:first-child) input[type='file'] {
177
+ padding-right: 2.25em;
178
+ }
179
+ }
180
+
181
+ /* Inset by the file input's border + padding, so it lines up with Choose File */
182
+ & .file-input-add-btn {
183
+ align-self: flex-start;
184
+ margin: 4px 0 0 0.45em;
185
+ font-weight: 400;
186
+ }
187
+ }
188
+
189
+ .file-input-remove-btn {
190
+ position: absolute;
191
+ top: 50%;
192
+ right: 0.45em;
193
+ transform: translateY(-50%);
194
+ padding: 0 0.25em;
195
+ font-size: 1.25em;
196
+ line-height: 1;
197
+ color: var(--destructive);
198
+ border: none;
199
+ border-radius: var(--radius);
200
+ background: none;
201
+ cursor: pointer;
202
+
203
+ &:hover {
204
+ background: color-mix(in srgb, var(--destructive) 15%, transparent);
205
+ }
206
+
207
+ &:focus-visible {
208
+ outline: none;
209
+ box-shadow: var(--focus-shadow);
210
+ }
211
+ }
212
+
213
+ .v-tab-btn {
214
+ font-size: var(--font-size-small);
215
+ height: 24px;
216
+ border: none;
217
+ background: none;
218
+ opacity: 0.3;
219
+ cursor: pointer;
220
+ padding: 4px 8px;
221
+
222
+ &.active {
223
+ font-weight: bold;
224
+ background: var(--background);
225
+ opacity: 1;
226
+ }
227
+ }
228
+
229
+ @container (min-width: 768px) {
230
+ .textarea {
231
+ padding: 8px;
232
+ }
233
+ }
234
+
235
+ /*
236
+ * Try bar: server/auth summary on the left, actions on the right. The summary is a two-row
237
+ * label/value list sized like a parameter label (name over type). The bar is its own container,
238
+ * so it reflows by the width it has, not the viewport.
239
+ */
240
+ .try-bar {
241
+ container-type: inline-size;
242
+ margin: 16px 0;
243
+ font-size: var(--font-size-small);
244
+ }
245
+ /* Narrow: summary on top, actions below it on the right */
246
+ .try-layout {
247
+ display: flex;
248
+ flex-direction: column;
249
+ gap: 0.75em;
250
+ }
251
+ .try-info {
252
+ display: grid;
253
+ grid-template-columns: max-content minmax(0, 1fr);
254
+ gap: 0.25em 0.75em;
255
+ margin: 0;
256
+ line-height: 1.35;
257
+
258
+ & > dt {
259
+ font-weight: 600;
260
+ color: var(--foreground);
261
+ }
262
+ & > dd {
263
+ margin: 0;
264
+ min-width: 0;
265
+ color: var(--muted-foreground);
266
+ overflow-wrap: anywhere;
267
+ }
268
+ }
269
+ .try-actions {
270
+ display: flex;
271
+ flex-wrap: wrap;
272
+ justify-content: flex-end;
273
+ gap: 0.5em;
274
+ }
275
+ /* Wide: one row; the summary takes the free space and the actions keep their size */
276
+ @container (min-width: 34rem) {
277
+ .try-layout {
278
+ flex-direction: row;
279
+ align-items: center;
280
+ gap: 1.5em;
281
+ }
282
+ .try-info {
283
+ flex: 1;
284
+ min-width: 0;
285
+ }
286
+ .try-actions {
287
+ flex: none;
288
+ }
289
+ }
290
+ /* Very narrow: each label above its value */
291
+ @container (max-width: 20rem) {
292
+ .try-info {
293
+ grid-template-columns: minmax(0, 1fr);
294
+ gap: 0;
295
+
296
+ & > dd + dt {
297
+ margin-top: 0.5em;
298
+ }
299
+ }
300
+ }
301
+ `,
302
+ CustomStyles,
303
+ ];
304
+ }
305
+
306
+ /** Shared schema & display configurations that affect both parameters and request body */
307
+ get commonConfigDeps() {
308
+ return [
309
+ this.schemaStyle,
310
+ this.schemaExpandLevel,
311
+ this.schemaDescriptionExpanded,
312
+ this.allowSchemaDescriptionExpandToggle,
313
+ this.schemaHideReadOnly,
314
+ this.schemaHideWriteOnly,
315
+ this.fillRequestFieldsWithExample,
316
+ this.renderStyle,
317
+ this.allowTry,
318
+ this.callback,
319
+ this.webhook,
320
+ ];
321
+ }
322
+
323
+ /** Dependencies for query/path/header/cookie parameters */
324
+ get parameterGuardDeps() {
325
+ return [
326
+ this.method,
327
+ this.path,
328
+ this.parameters,
329
+ this.activeParameterSchemaTabs,
330
+ ...this.commonConfigDeps,
331
+ ];
332
+ }
333
+
334
+ /** Dependencies for the request body template */
335
+ get requestBodyGuardDeps() {
336
+ return [
337
+ this.method,
338
+ this.path,
339
+ this.request_body,
340
+ this.selectedRequestBodyType,
341
+ this.selectedRequestBodyExample,
342
+ this.activeSchemaTab,
343
+ this.fileInputKeys,
344
+ ...this.commonConfigDeps,
345
+ ];
346
+ }
347
+
348
+ render() {
349
+ return html`<div
350
+ @input="${this.syncTryBarState}"
351
+ @change="${this.syncTryBarState}"
352
+ class="col regular-font request-panel ${
353
+ "read focused".includes(this.renderStyle) || this.callback === "true"
354
+ ? "read-mode"
355
+ : "view-mode"
356
+ }"
357
+ >
358
+ ${
359
+ this.callback === "true"
360
+ ? html`<h6 class="rapidoc-heading label-heading tiny-title" part="section-request-title heading heading-6">Callback request</h6>`
361
+ : html`<h4 class="rapidoc-heading label-heading req-res-title" part="section-request-title heading heading-4">Request</h4>`
362
+ }
363
+ <div>
364
+ ${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "path"))}
365
+ ${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "query"))}
366
+ ${guard(this.requestBodyGuardDeps, () => requestBodyTemplate.call(this))}
367
+ ${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "header"))}
368
+ ${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "cookie"))}
369
+ ${this.allowTry === "false" ? "" : html`${apiCallTemplate.call(this)}`}
370
+ </div>
371
+ </div>`;
372
+ }
373
+
374
+ async updated(changedProperties) {
375
+ if (this.showCurlBeforeTry === "true") {
376
+ if (
377
+ !changedProperties ||
378
+ !changedProperties.has("curlSyntax") ||
379
+ changedProperties.size > 1
380
+ ) {
381
+ this.applyCURLSyntax(this.shadowRoot);
382
+ }
383
+ }
384
+ scheduleHighlight(this.getRootNode()?.host?.shadowRoot || this.shadowRoot);
385
+
386
+ if (this.webhook === "true") {
387
+ this.allowTry = "false";
388
+ }
389
+ // After the fields are in the DOM; deferred so the update is not requested mid-update
390
+ queueMicrotask(() => this.syncTryBarState());
391
+ }
392
+
393
+ // This method is called before navigation change in focused mode
394
+ async beforeNavigationFocusedMode() {}
395
+
396
+ // Called by rapi-doc after navigating to a new endpoint in focused renderStyle.
397
+ // In focused mode, the single <api-request> element instance is reused across operations.
398
+ // We reset selectedRequestBodyType and selectedRequestBodyExample so stale MIME type/example selections
399
+ // from a previous endpoint don't carry over to an endpoint that may not support them.
400
+ // The first valid MIME type will be automatically selected by request-body-template during render.
401
+ async afterNavigationFocusedMode() {
402
+ this.selectedRequestBodyType = "";
403
+ this.selectedRequestBodyExample = "";
404
+ this.clearResponseData();
405
+ }
406
+
407
+ // Request-Body Event Handlers
408
+ onSelectExample(e) {
409
+ this.selectedRequestBodyExample = e.target.value;
410
+ const exampleDropdownEl = e.target;
411
+ window.setTimeout(
412
+ (selectEl) => {
413
+ const readOnlyExampleEl = selectEl
414
+ .closest(".example-panel")
415
+ .querySelector(".request-body-param");
416
+ const userInputExampleTextareaEl = selectEl
417
+ .closest(".example-panel")
418
+ .querySelector(".request-body-param-user-input");
419
+ userInputExampleTextareaEl.value = readOnlyExampleEl.innerText;
420
+
421
+ const requestPanelEl = this.getRequestPanel({ target: selectEl });
422
+ this.liveCURLSyntaxUpdate(requestPanelEl);
423
+ },
424
+ 0,
425
+ exampleDropdownEl,
426
+ );
427
+ }
428
+
429
+ onMimeTypeChange(e) {
430
+ this.selectedRequestBodyType = e.target.value;
431
+ const mimeDropdownEl = e.target;
432
+ this.selectedRequestBodyExample = "";
433
+ window.setTimeout(
434
+ (selectEl) => {
435
+ const readOnlyExampleEl = selectEl
436
+ .closest(".request-body-container")
437
+ .querySelector(".request-body-param");
438
+ if (readOnlyExampleEl) {
439
+ const userInputExampleTextareaEl = selectEl
440
+ .closest(".request-body-container")
441
+ .querySelector(".request-body-param-user-input");
442
+ userInputExampleTextareaEl.value = readOnlyExampleEl.innerText;
443
+ }
444
+ },
445
+ 0,
446
+ mimeDropdownEl,
447
+ );
448
+ }
449
+
450
+ async onFillRequestData(e) {
451
+ const requestPanelEl = e.target.closest(".request-panel");
452
+ const requestPanelInputEls = [
453
+ ...requestPanelEl.querySelectorAll("input, tag-input, textarea:not(.is-hidden)"),
454
+ ];
455
+ requestPanelInputEls.forEach((el) => {
456
+ if (el.dataset.example) {
457
+ if (el.tagName.toUpperCase() === "TAG-INPUT") {
458
+ el.value = el.dataset.example.split("~|~");
459
+ } else {
460
+ el.value = el.dataset.example;
461
+ }
462
+ }
463
+ });
464
+ this.syncTryBarState();
465
+ }
466
+
467
+ async onClearRequestData(e) {
468
+ const requestPanelEl = e.target.closest(".request-panel");
469
+ const requestPanelInputEls = [
470
+ ...requestPanelEl.querySelectorAll("input, tag-input, textarea:not(.is-hidden)"),
471
+ ];
472
+ requestPanelInputEls.forEach((el) => {
473
+ el.value = "";
474
+ });
475
+ this.syncTryBarState();
476
+ }
477
+
478
+ // Reads the request fields (the same ones Fill Example and Clear act on) and updates the
479
+ // try-bar flags; setting a flag to its current value schedules no render
480
+ syncTryBarState() {
481
+ const requestPanelEl = this.shadowRoot?.querySelector(".request-panel");
482
+ const inputEls: any[] = requestPanelEl
483
+ ? [...requestPanelEl.querySelectorAll("input, tag-input, textarea:not(.is-hidden)")]
484
+ : [];
485
+ this.hasExampleToFill = inputEls.some((el) => !!el.dataset.example);
486
+ this.hasValueToClear = inputEls.some((el) =>
487
+ Array.isArray(el.value) ? el.value.length > 0 : el.value !== "",
488
+ );
489
+ }
490
+
491
+ buildFetchURL(requestPanelEl) {
492
+ const pathParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='path']")];
493
+ const queryParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='query']")];
494
+ const queryParamObjTypeEls = [
495
+ ...requestPanelEl.querySelectorAll("[data-ptype='query-object']"),
496
+ ];
497
+
498
+ const pathParams = {};
499
+ pathParamEls.forEach((el) => {
500
+ pathParams[el.dataset.pname] = el.value;
501
+ });
502
+
503
+ const queryParams = [];
504
+ queryParamEls.forEach((el) => {
505
+ const isArray = el.dataset.array !== "false";
506
+ const style = el.dataset.paramSerializeStyle || "form";
507
+ const explode =
508
+ el.dataset.paramSerializeExplode !== undefined
509
+ ? el.dataset.paramSerializeExplode === "true"
510
+ : style === "form";
511
+ const allowReserved = el.dataset.paramAllowReserved === "true";
512
+
513
+ queryParams.push({
514
+ name: el.dataset.pname,
515
+ value: el.value,
516
+ style,
517
+ explode,
518
+ allowReserved,
519
+ isArray,
520
+ itemType: el.dataset.itemType || "",
521
+ });
522
+ });
523
+
524
+ queryParamObjTypeEls.forEach((el) => {
525
+ const style = el.dataset.paramSerializeStyle || "form";
526
+ const explode =
527
+ el.dataset.paramSerializeExplode !== undefined
528
+ ? el.dataset.paramSerializeExplode === "true"
529
+ : style === "form";
530
+ const allowReserved = el.dataset.paramAllowReserved === "true";
531
+
532
+ queryParams.push({
533
+ name: el.dataset.pname,
534
+ value: el.value,
535
+ style,
536
+ explode,
537
+ allowReserved,
538
+ isObject: true,
539
+ });
540
+ });
541
+
542
+ return constructFetchURL({
543
+ serverUrl: this.serverUrl,
544
+ path: this.path,
545
+ pathParams,
546
+ queryParams,
547
+ apiKeys: this.api_keys,
548
+ });
549
+ }
550
+
551
+ buildFetchHeaders(requestPanelEl) {
552
+ const respEl = this.closest(
553
+ ".expanded-req-resp-container, .req-resp-container",
554
+ )?.getElementsByTagName("api-response")[0];
555
+ const headerParamEls = [
556
+ ...requestPanelEl.querySelectorAll("[data-ptype='header'], [data-ptype='header-object']"),
557
+ ];
558
+ const requestBodyContainerEl = requestPanelEl.querySelector(".request-body-container");
559
+ const accept = respEl?.selectedMimeType || this.accept || "";
560
+ const contentType = requestBodyContainerEl?.dataset?.selectedRequestBodyType || "";
561
+
562
+ const headerParams = headerParamEls.map((el) => ({
563
+ name: el.dataset.pname,
564
+ value: el.value,
565
+ explode: el.dataset.paramSerializeExplode === "true",
566
+ isObject: el.dataset.ptype === "header-object",
567
+ }));
568
+
569
+ return constructFetchHeaders({
570
+ accept,
571
+ apiKeys: this.api_keys,
572
+ headerParams,
573
+ contentType,
574
+ });
575
+ }
576
+
577
+ buildFetchBodyOptions(requestPanelEl) {
578
+ const requestBodyContainerEl = requestPanelEl.querySelector(".request-body-container");
579
+ const fetchOptions: any = {
580
+ method: this.method.toUpperCase(),
581
+ };
582
+ if (requestBodyContainerEl) {
583
+ const requestBodyType = requestBodyContainerEl.dataset.selectedRequestBodyType || "";
584
+ if (requestBodyType.includes("form-urlencoded")) {
585
+ const formUrlDynamicTextAreaEl = requestPanelEl.querySelector(
586
+ "[data-ptype='dynamic-form']",
587
+ );
588
+ if (formUrlDynamicTextAreaEl) {
589
+ fetchOptions.body = buildUrlEncodedBody([], formUrlDynamicTextAreaEl.value);
590
+ } else {
591
+ const formUrlEls = [...requestPanelEl.querySelectorAll("[data-ptype='form-urlencode']")];
592
+ const fields = formUrlEls
593
+ .filter((v: any) => v.type !== "file")
594
+ .map((el: any) => ({
595
+ name: el.dataset.pname,
596
+ value: el.value,
597
+ isArray: el.dataset.array !== "false",
598
+ }));
599
+ fetchOptions.body = buildUrlEncodedBody(fields);
600
+ }
601
+ } else if (requestBodyType.includes("form-data")) {
602
+ const formDataEls = [...requestPanelEl.querySelectorAll("[data-ptype='form-data']")];
603
+ const fields = formDataEls.map((el: any) => ({
604
+ name: el.dataset.pname,
605
+ value: el.value,
606
+ file: el.type === "file" ? el.files?.[0] : undefined,
607
+ isArray: el.dataset.array !== "false",
608
+ }));
609
+ fetchOptions.body = buildMultipartFormData(fields);
610
+ // Content-Type of the parts that need one (application/json for objects, or the spec's encoding)
611
+ const partContentTypes = Object.fromEntries(
612
+ formDataEls
613
+ .filter((el: any) => el.dataset.contentType)
614
+ .map((el: any) => [el.dataset.pname, el.dataset.contentType]),
615
+ );
616
+ if (Object.keys(partContentTypes).length > 0) {
617
+ fetchOptions.partContentTypes = partContentTypes;
618
+ }
619
+ } else if (
620
+ /^audio\/|^image\/|^video\/|^font\/|tar$|zip$|7z$|rtf$|msword$|excel$|\/pdf$|\/octet-stream$/.test(
621
+ requestBodyType,
622
+ )
623
+ ) {
624
+ const bodyParamFileEl: any = requestPanelEl.querySelector(".request-body-param-file");
625
+ if (bodyParamFileEl?.files[0]) {
626
+ fetchOptions.body = bodyParamFileEl.files[0];
627
+ }
628
+ } else if (
629
+ requestBodyType.includes("json") ||
630
+ requestBodyType.includes("xml") ||
631
+ requestBodyType.includes("text")
632
+ ) {
633
+ const exampleTextAreaEl: any = requestPanelEl.querySelector(
634
+ ".request-body-param-user-input",
635
+ );
636
+ if (exampleTextAreaEl?.value) {
637
+ fetchOptions.body = exampleTextAreaEl.value;
638
+ }
639
+ }
640
+ }
641
+
642
+ return fetchOptions;
643
+ }
644
+
645
+ async onTryClick(e) {
646
+ const tryBtnEl = e?.target;
647
+ const requestPanelEl = tryBtnEl
648
+ ? this.getRequestPanel(e)
649
+ : this.shadowRoot.querySelector(".request-panel");
650
+ const fetchUrl = this.buildFetchURL(requestPanelEl);
651
+ const fetchOptions = this.buildFetchBodyOptions(requestPanelEl);
652
+ const reqHeaders = this.buildFetchHeaders(requestPanelEl);
653
+ this.responseUrl = "";
654
+ this.responseHeaders = [];
655
+ this.curlSyntax = this.generateCURLSyntax(fetchUrl, reqHeaders, fetchOptions, requestPanelEl);
656
+ this.responseStatus = "success";
657
+ this.responseIsBlob = false;
658
+
659
+ this.respContentDisposition = "";
660
+ if (this.responseBlobUrl) {
661
+ URL.revokeObjectURL(this.responseBlobUrl);
662
+ this.responseBlobUrl = "";
663
+ }
664
+ if (this.fetchCredentials) {
665
+ fetchOptions.credentials = this.fetchCredentials;
666
+ }
667
+ const controller = new AbortController();
668
+ this.activeAbortController = controller;
669
+ const { signal } = controller;
670
+ fetchOptions.headers = reqHeaders;
671
+ const tempRequest = { url: fetchUrl, ...fetchOptions };
672
+ this.dispatchEvent(
673
+ new CustomEvent("before-try", {
674
+ bubbles: true,
675
+ composed: true,
676
+ detail: {
677
+ request: tempRequest,
678
+ controller,
679
+ },
680
+ }),
681
+ );
682
+ const updatedFetchOptions = {
683
+ method: tempRequest.method,
684
+ headers: tempRequest.headers,
685
+ credentials: tempRequest.credentials,
686
+ body: tempRequest.body,
687
+ };
688
+ // FormData can't give a text part a Content-Type, so such bodies are serialized here
689
+ if (tempRequest.body instanceof FormData && tempRequest.partContentTypes) {
690
+ const multipart = serializeMultipartBody(tempRequest.body, tempRequest.partContentTypes);
691
+ const headers = new Headers(tempRequest.headers);
692
+ headers.set("Content-Type", multipart.contentType);
693
+ updatedFetchOptions.headers = headers;
694
+ updatedFetchOptions.body = multipart.body;
695
+ }
696
+ let fetchRequest;
697
+ let fetchResponse;
698
+ let responseClone;
699
+ try {
700
+ this.loading = true;
701
+ this.responseText = "⌛";
702
+ this.responseMessage = "";
703
+ fetchRequest = new Request(tempRequest.url, updatedFetchOptions);
704
+ const startTime = performance.now();
705
+ fetchResponse = await fetch(fetchRequest, { signal });
706
+ const endTime = performance.now();
707
+ responseClone = fetchResponse.clone(); // create a response clone to allow reading response body again (response.json, response.text etc)
708
+ this.responseMessage = html`${fetchResponse.statusText ? `${fetchResponse.statusText}:${fetchResponse.status}` : fetchResponse.status}
709
+ <div style="color:var(--muted-foreground)">Took ${Math.round(endTime - startTime)} milliseconds</div>`;
710
+ this.responseUrl = fetchResponse.url;
711
+ const respHeadersObj = {};
712
+ fetchResponse.headers.forEach((hdrVal, hdr) => {
713
+ respHeadersObj[hdr] = hdrVal;
714
+ this.responseHeaders = `${this.responseHeaders}${hdr}: ${hdrVal}\n`;
715
+ });
716
+
717
+ const processed = await processFetchResponse(fetchResponse);
718
+ this.responseText = processed.responseText;
719
+ this.responseIsBlob = processed.responseIsBlob;
720
+ this.responseBlobType = processed.responseBlobType;
721
+ this.responseBlobUrl = processed.responseBlobUrl;
722
+ this.respContentDisposition = processed.respContentDisposition;
723
+
724
+ this.dispatchEvent(
725
+ new CustomEvent("after-try", {
726
+ bubbles: true,
727
+ composed: true,
728
+ detail: {
729
+ request: fetchRequest,
730
+ response: responseClone,
731
+ responseHeaders: respHeadersObj,
732
+ responseBody: processed.respJson || processed.respText || processed.respBlob,
733
+ responseStatus: responseClone.ok,
734
+ },
735
+ }),
736
+ );
737
+ } catch (err) {
738
+ if (err.name === "AbortError") {
739
+ this.dispatchEvent(
740
+ new CustomEvent("request-aborted", {
741
+ bubbles: true,
742
+ composed: true,
743
+ detail: {
744
+ err,
745
+ request: fetchRequest,
746
+ },
747
+ }),
748
+ );
749
+ this.responseMessage = "Request Aborted";
750
+ this.responseText = "Request Aborted";
751
+ } else {
752
+ this.dispatchEvent(
753
+ new CustomEvent("after-try", {
754
+ bubbles: true,
755
+ composed: true,
756
+ detail: {
757
+ err,
758
+ request: fetchRequest,
759
+ },
760
+ }),
761
+ );
762
+ if (tempRequest.method?.toUpperCase() === "TRACE") {
763
+ this.responseMessage = `${err.message} (Browser security restricts TRACE requests in fetch. Use the cURL snippet below.)`;
764
+ } else {
765
+ this.responseMessage = `${err.message} (CORS or Network Issue)`;
766
+ }
767
+ }
768
+ } finally {
769
+ this.loading = false;
770
+ this.activeAbortController = null;
771
+ }
772
+ }
773
+
774
+ liveCURLSyntaxUpdate(requestPanelEl) {
775
+ this.applyCURLSyntax(requestPanelEl);
776
+ }
777
+
778
+ onGenerateCURLClick(e) {
779
+ const requestPanelEl = this.getRequestPanel(e);
780
+ this.applyCURLSyntax(requestPanelEl);
781
+ }
782
+
783
+ getRequestPanel(e) {
784
+ return e.target.closest(".request-panel");
785
+ }
786
+
787
+ applyCURLSyntax(requestPanelEl) {
788
+ const fetchUrl = this.buildFetchURL(requestPanelEl);
789
+ const fetchOptions = this.buildFetchBodyOptions(requestPanelEl);
790
+ const fetchHeaders = this.buildFetchHeaders(requestPanelEl);
791
+
792
+ const newCurlSyntax = this.generateCURLSyntax(
793
+ fetchUrl,
794
+ fetchHeaders,
795
+ fetchOptions,
796
+ requestPanelEl,
797
+ );
798
+ if (this.curlSyntax !== newCurlSyntax) {
799
+ this.curlSyntax = newCurlSyntax;
800
+ }
801
+ }
802
+
803
+ generateCURLSyntax(fetchUrl, fetchHeaders, fetchOptions, requestPanelEl) {
804
+ const requestBodyContainerEl = requestPanelEl?.querySelector(".request-body-container");
805
+ const requestBodyType = requestBodyContainerEl?.dataset?.selectedRequestBodyType || "";
806
+ let body = fetchOptions?.body;
807
+
808
+ if (!body && requestBodyContainerEl) {
809
+ const exampleTextAreaEl: any = requestPanelEl.querySelector(".request-body-param-user-input");
810
+ if (exampleTextAreaEl?.value) {
811
+ body = exampleTextAreaEl.value;
812
+ }
813
+ }
814
+
815
+ return generateCurlCommand({
816
+ method: this.method,
817
+ url: fetchUrl,
818
+ headers: fetchHeaders,
819
+ body,
820
+ contentType: requestBodyType,
821
+ partContentTypes: fetchOptions?.partContentTypes,
822
+ });
823
+ }
824
+
825
+ onAddFileInput(fieldName) {
826
+ const currentKeys = this.fileInputKeys[fieldName] ?? [0];
827
+ const nextKey = currentKeys.length > 0 ? Math.max(...currentKeys) + 1 : 0;
828
+ this.fileInputKeys = {
829
+ ...this.fileInputKeys,
830
+ [fieldName]: [...currentKeys, nextKey],
831
+ };
832
+ }
833
+
834
+ onRemoveFileInput(fieldName, keyToRemove) {
835
+ const currentKeys = this.fileInputKeys[fieldName] ?? [0];
836
+ this.fileInputKeys = {
837
+ ...this.fileInputKeys,
838
+ [fieldName]: currentKeys.filter((k) => k !== keyToRemove),
839
+ };
840
+ }
841
+
842
+ clearResponseData() {
843
+ this.responseUrl = "";
844
+ this.responseHeaders = "";
845
+ this.responseText = "";
846
+ this.responseStatus = "success";
847
+ this.responseMessage = "";
848
+ this.responseIsBlob = false;
849
+ this.responseBlobType = "";
850
+ this.respContentDisposition = "";
851
+ this.fileInputKeys = {};
852
+ if (this.responseBlobUrl) {
853
+ URL.revokeObjectURL(this.responseBlobUrl);
854
+ this.responseBlobUrl = "";
855
+ }
856
+ }
857
+
858
+ willUpdate(changedProperties) {
859
+ super.willUpdate?.(changedProperties);
860
+ // Sync configuration options passed down from parent <rapi-doc>
861
+ if (this.config && changedProperties.has("config")) {
862
+ if (this.config.renderStyle) this.renderStyle = this.config.renderStyle;
863
+ if (this.config.schemaStyle) this.schemaStyle = this.config.schemaStyle;
864
+ if (this.config.schemaExpandLevel !== undefined)
865
+ this.schemaExpandLevel = this.config.schemaExpandLevel;
866
+ if (this.config.schemaDescriptionExpanded !== undefined)
867
+ this.schemaDescriptionExpanded = this.config.schemaDescriptionExpanded;
868
+ if (this.config.allowSchemaDescriptionExpandToggle !== undefined)
869
+ this.allowSchemaDescriptionExpandToggle = this.config.allowSchemaDescriptionExpandToggle;
870
+ // In incoming requests, readOnly fields are hidden by default ('true') unless configured to 'never'.
871
+ // For incoming webhooks, the direction is inverted (the external caller sends the payload), so readOnly fields are shown ('false').
872
+ if (this.config.schemaHideReadOnly !== undefined)
873
+ this.schemaHideReadOnly =
874
+ this.config.schemaHideReadOnly === "never"
875
+ ? "false"
876
+ : this.webhook === "true"
877
+ ? "false"
878
+ : "true";
879
+ // In incoming requests, writeOnly fields are shown by default ('false').
880
+ // For webhooks, writeOnly fields are hidden ('true') unless configured to 'never'.
881
+ if (this.config.schemaHideWriteOnly !== undefined)
882
+ this.schemaHideWriteOnly =
883
+ this.config.schemaHideWriteOnly === "never"
884
+ ? "false"
885
+ : this.webhook === "true"
886
+ ? "true"
887
+ : "false";
888
+ if (this.config.fillRequestFieldsWithExample !== undefined)
889
+ this.fillRequestFieldsWithExample = this.config.fillRequestFieldsWithExample;
890
+ if (this.config.allowTry !== undefined) this.allowTry = this.config.allowTry;
891
+ if (this.config.showCurlBeforeTry !== undefined)
892
+ this.showCurlBeforeTry = this.config.showCurlBeforeTry;
893
+ if (this.config.fetchCredentials !== undefined)
894
+ this.fetchCredentials = this.config.fetchCredentials;
895
+ if (this.config.defaultSchemaTab) this.activeSchemaTab = this.config.defaultSchemaTab;
896
+ }
897
+ }
898
+
899
+ disconnectedCallback() {
900
+ this.curlSyntax = "";
901
+ if (this.activeAbortController) {
902
+ this.activeAbortController.abort();
903
+ this.activeAbortController = null;
904
+ }
905
+ // Cleanup ObjectURL for the blob data if this component created one
906
+ if (this.responseBlobUrl) {
907
+ URL.revokeObjectURL(this.responseBlobUrl);
908
+ this.responseBlobUrl = "";
909
+ }
910
+ super.disconnectedCallback();
911
+ }
912
+ }