rapidoc 9.3.3 → 9.3.5

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 (48) hide show
  1. package/.github/workflow/words.yaml +22 -0
  2. package/README.md +2 -7
  3. package/dist/index.html +1 -1
  4. package/dist/index.html.gz +0 -0
  5. package/dist/rapidoc-min.js +858 -748
  6. package/dist/rapidoc-min.js.LICENSE.txt +14 -9
  7. package/dist/rapidoc-min.js.LICENSE.txt.gz +0 -0
  8. package/dist/rapidoc-min.js.gz +0 -0
  9. package/dist/rapidoc-min.js.map +1 -1
  10. package/dist/rapidoc-min.js.map.gz +0 -0
  11. package/dist/rapidoc.js +11948 -32008
  12. package/dist/report.html +6 -5
  13. package/index.html +2 -1
  14. package/package.json +25 -24
  15. package/src/components/api-request.js +266 -122
  16. package/src/components/api-response.js +11 -10
  17. package/src/components/json-tree.js +17 -12
  18. package/src/components/schema-table.js +58 -49
  19. package/src/components/schema-tree.js +60 -56
  20. package/src/components/tag-input.js +7 -4
  21. package/src/json-schema-viewer.js +2 -1
  22. package/src/rapidoc-mini.js +1 -1
  23. package/src/rapidoc.js +108 -35
  24. package/src/styles/endpoint-styles.js +1 -1
  25. package/src/styles/info-styles.js +2 -1
  26. package/src/styles/input-styles.js +2 -16
  27. package/src/styles/nav-styles.js +57 -10
  28. package/src/styles/schema-styles.js +27 -15
  29. package/src/styles/tab-styles.js +1 -0
  30. package/src/templates/advance-search-template.js +3 -3
  31. package/src/templates/callback-template.js +3 -3
  32. package/src/templates/code-samples-template.js +1 -0
  33. package/src/templates/components-template.js +3 -5
  34. package/src/templates/endpoint-template.js +39 -26
  35. package/src/templates/expanded-endpoint-template.js +58 -9
  36. package/src/templates/focused-endpoint-template.js +5 -2
  37. package/src/templates/header-template.js +2 -2
  38. package/src/templates/json-schema-viewer-template.js +2 -1
  39. package/src/templates/main-body-template.js +19 -18
  40. package/src/templates/navbar-template.js +99 -82
  41. package/src/templates/overview-template.js +1 -1
  42. package/src/templates/security-scheme-template.js +51 -27
  43. package/src/templates/server-template.js +1 -1
  44. package/src/utils/common-utils.js +2 -2
  45. package/src/utils/schema-utils.js +192 -67
  46. package/src/utils/spec-parser.js +9 -3
  47. package/src/utils/theme.js +9 -3
  48. package/webpack.config.js +2 -1
@@ -60,8 +60,10 @@ export default class ApiRequest extends LitElement {
60
60
  responseHeaders: { type: String, attribute: false },
61
61
  responseStatus: { type: String, attribute: false },
62
62
  responseUrl: { type: String, attribute: false },
63
+ curlSyntax: { type: String, attribute: false },
63
64
  fillRequestFieldsWithExample: { type: String, attribute: 'fill-request-fields-with-example' },
64
65
  allowTry: { type: String, attribute: 'allow-try' },
66
+ showCurlBeforeTry: { type: String, attribute: 'show-curl-before-try' },
65
67
  renderStyle: { type: String, attribute: 'render-style' },
66
68
  schemaStyle: { type: String, attribute: 'schema-style' },
67
69
  activeSchemaTab: { type: String, attribute: 'active-schema-tab' },
@@ -98,7 +100,9 @@ export default class ApiRequest extends LitElement {
98
100
  PrismStyles,
99
101
  css`
100
102
  *, *:before, *:after { box-sizing: border-box; }
101
-
103
+ :where(button, input[type="checkbox"], [tabindex="0"]):focus-visible { box-shadow: var(--focus-shadow); }
104
+ :where(input[type="text"], input[type="password"], select, textarea):focus-visible { border-color: var(--primary-color); }
105
+ tag-input:focus-within { outline: 1px solid;}
102
106
  .read-mode {
103
107
  margin-top: 24px;
104
108
  }
@@ -213,20 +217,27 @@ export default class ApiRequest extends LitElement {
213
217
  `;
214
218
  }
215
219
 
216
- /*
217
- async updated(changedProperties) {
220
+ async updated() {
221
+ if (this.showCurlBeforeTry === 'true') {
222
+ this.applyCURLSyntax(this.shadowRoot);
223
+ }
224
+
218
225
  // In focused mode after rendering the request component, update the text-areas(which contains examples) using
219
226
  // the original values from hidden textareas
220
227
  // This is done coz, user may update the dom by editing the textarea's and once the DOM is updated externally change detection wont happen, therefore update the values manually
221
- if (this.renderStyle === 'focused') {
222
- if (changedProperties.size === 1 && changedProperties.has('activeSchemaTab')) {
223
- // dont update example as only tabs is switched
224
- } else {
225
- this.requestUpdate();
226
- }
228
+
229
+ // if (this.renderStyle === 'focused') {
230
+ // if (changedProperties.size === 1 && changedProperties.has('activeSchemaTab')) {
231
+ // // dont update example as only tabs is switched
232
+ // } else {
233
+ // this.requestUpdate();
234
+ // }
235
+ // }
236
+
237
+ if (this.webhook === 'true') {
238
+ this.allowTry = 'false';
227
239
  }
228
240
  }
229
- */
230
241
 
231
242
  async saveExampleState() {
232
243
  if (this.renderStyle === 'focused') {
@@ -259,28 +270,24 @@ export default class ApiRequest extends LitElement {
259
270
  }
260
271
  }
261
272
 
273
+ /* eslint-disable indent */
262
274
  renderExample(example, paramType, paramName) {
263
275
  return html`
264
276
  ${paramType === 'array' ? '[' : ''}
265
277
  <a
266
278
  part="anchor anchor-param-example"
279
+ style="display:inline-block; min-width:24px; text-align:center"
267
280
  class="${this.allowTry === 'true' ? '' : 'inactive-link'}"
268
281
  data-example-type="${paramType === 'array' ? paramType : 'string'}"
269
- data-example="${example.value && Array.isArray(example.value) ? example.value?.join('~|~') : example.value || ''}"
282
+ data-example="${example.value && Array.isArray(example.value) ? example.value?.join('~|~') : (typeof example.value === 'object' ? JSON.stringify(example.value, null, 2) : example.value) || ''}"
283
+ title="${example.value && Array.isArray(example.value) ? example.value?.join('~|~') : (typeof example.value === 'object' ? JSON.stringify(example.value, null, 2) : example.value) || ''}"
270
284
  @click="${(e) => {
271
- const inputEl = e.target.closest('table').querySelector(`[data-pname="${paramName}"]`);
272
- if (inputEl) {
273
- if (e.target.dataset.exampleType === 'array') {
274
- inputEl.value = e.target.dataset.example.split('~|~');
275
- } else {
276
- inputEl.value = e.target.dataset.example;
277
- }
278
- }
279
- }
280
- }"
281
- >
282
- ${example.value && Array.isArray(example.value) ? example.value?.join(', ') : example.value || '∅'}
283
- </a>
285
+ const inputEl = e.target.closest('table').querySelector(`[data-pname="${paramName}"]`);
286
+ if (inputEl) {
287
+ inputEl.value = e.target.dataset.exampleType === 'array' ? e.target.dataset.example.split('~|~') : e.target.dataset.example;
288
+ }
289
+ }}"
290
+ > ${example.printableValue || example.value} </a>
284
291
  ${paramType === 'array' ? '] ' : ''}
285
292
  `;
286
293
  }
@@ -303,17 +310,15 @@ export default class ApiRequest extends LitElement {
303
310
  </ul>`;
304
311
  }
305
312
 
306
- /* eslint-disable indent */
307
-
308
313
  exampleListTemplate(paramName, paramType, exampleList = []) {
309
- return html` ${exampleList.length > 0
310
- ? html`<span style="font-weight:bold">Examples: </span>
311
- ${
312
- anyExampleWithSummaryOrDescription(exampleList)
314
+ return html` ${
315
+ exampleList.length > 0
316
+ ? html`<span style="font-weight:bold">Examples: </span>
317
+ ${anyExampleWithSummaryOrDescription(exampleList)
313
318
  ? this.renderLongFormatExamples(exampleList, paramType, paramName)
314
- : this.renderShortFormatExamples(exampleList, paramType, paramName)
315
- }`
316
- : ''}`;
319
+ : this.renderShortFormatExamples(exampleList, paramType, paramName)}`
320
+ : ''
321
+ }`;
317
322
  }
318
323
 
319
324
  inputParametersTemplate(paramType) {
@@ -336,7 +341,7 @@ export default class ApiRequest extends LitElement {
336
341
  for (const param of filteredParams) {
337
342
  const [declaredParamSchema, serializeStyle, mimeTypeElem] = getSchemaFromParam(param);
338
343
  if (!declaredParamSchema) {
339
- continue; // eslint-disable-line no-continue
344
+ continue;
340
345
  }
341
346
  const paramSchema = getTypeInfo(declaredParamSchema);
342
347
  if (!paramSchema) {
@@ -367,7 +372,7 @@ export default class ApiRequest extends LitElement {
367
372
  || nestExampleIfPresent(param.example)
368
373
  || nestExampleIfPresent(mimeTypeElem?.example)
369
374
  || mimeTypeElem?.examples
370
- || paramSchema.examples
375
+ || nestExampleIfPresent(paramSchema.examples)
371
376
  || nestExampleIfPresent(paramSchema.example)
372
377
  ),
373
378
  paramSchema.type,
@@ -449,8 +454,12 @@ export default class ApiRequest extends LitElement {
449
454
  data-param-allow-reserved = "${paramAllowReserved}"
450
455
  data-x-fill-example = "${param['x-fill-example'] || 'yes'}"
451
456
  spellcheck = "false"
452
- .textContent="${param['x-fill-example'] === 'no' ? '' : live(this.fillRequestFieldsWithExample === 'true' ? example.exampleVal : '')}"
457
+ .textContent="${param['x-fill-example'] === 'no' ? '' : live(this.fillRequestFieldsWithExample === 'true' ? (typeof example.exampleVal === 'object' ? JSON.stringify(example.exampleVal, null, 2) : example.exampleVal) : '')}"
453
458
  style = "resize:vertical; width:100%; height: ${'read focused'.includes(this.renderStyle) ? '180px' : '120px'};"
459
+ @input=${(e) => {
460
+ const requestPanelEl = this.getRequestPanel(e);
461
+ this.liveCURLSyntaxUpdate(requestPanelEl);
462
+ }}
454
463
  ></textarea>
455
464
  </div>`
456
465
  : html`
@@ -461,12 +470,12 @@ export default class ApiRequest extends LitElement {
461
470
  .data = '${schemaAsObj}'
462
471
  schema-expand-level = "${this.schemaExpandLevel}"
463
472
  schema-description-expanded = "${this.schemaDescriptionExpanded}"
464
- allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}",
473
+ allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}"
465
474
  schema-hide-read-only = "${this.schemaHideReadOnly.includes(this.method)}"
466
475
  schema-hide-write-only = "${this.schemaHideWriteOnly.includes(this.method)}"
467
- exportparts = "btn:btn, btn-fill:btn-fill, btn-outline:btn-outline, btn-try:btn-try, btn-clear:btn-clear, btn-clear-resp:btn-clear-resp,
468
- file-input:file-input, textbox:textbox, textbox-param:textbox-param, textarea:textarea, textarea-param:textarea-param,
469
- anchor:anchor, anchor-param-example:anchor-param-example"
476
+ exportparts = "wrap-request-btn:wrap-request-btn, btn:btn, btn-fill:btn-fill, btn-outline:btn-outline, btn-try:btn-try, btn-clear:btn-clear, btn-clear-resp:btn-clear-resp,
477
+ file-input:file-input, textbox:textbox, textbox-param:textbox-param, textarea:textarea, textarea-param:textarea-param,
478
+ anchor:anchor, anchor-param-example:anchor-param-example"
470
479
  > </schema-tree>
471
480
  </div>`
472
481
  }
@@ -482,6 +491,10 @@ export default class ApiRequest extends LitElement {
482
491
  data-x-fill-example = "${param['x-fill-example'] || 'yes'}"
483
492
  data-array="false"
484
493
  .value="${param['x-fill-example'] === 'no' ? '' : live(this.fillRequestFieldsWithExample === 'true' ? example.exampleVal : '')}"
494
+ @input=${(e) => {
495
+ const requestPanelEl = this.getRequestPanel(e);
496
+ this.liveCURLSyntaxUpdate(requestPanelEl);
497
+ }}
485
498
  />`
486
499
  }
487
500
  </td>`
@@ -530,18 +543,18 @@ export default class ApiRequest extends LitElement {
530
543
  return html`
531
544
  <div class="table-title top-gap">${title}</div>
532
545
  <div style="display:block; overflow-x:auto; max-width:100%;">
533
- <table class="m-table" style="width:100%; word-break:break-word;">
546
+ <table role="presentation" class="m-table" style="width:100%; word-break:break-word;">
534
547
  ${tableRows}
535
548
  </table>
536
549
  </div>`;
537
550
  }
538
551
 
539
- // This method is called before navigation change in focusd mode
540
- async beforerNavigationFocusedMode() {
552
+ // This method is called before navigation change in focused mode
553
+ async beforeNavigationFocusedMode() {
541
554
  // this.saveExampleState();
542
555
  }
543
556
 
544
- // This method is called after navigation change in focusd mode
557
+ // This method is called after navigation change in focused mode
545
558
  async afterNavigationFocusedMode() {
546
559
  this.selectedRequestBodyType = '';
547
560
  this.selectedRequestBodyExample = '';
@@ -557,6 +570,9 @@ export default class ApiRequest extends LitElement {
557
570
  const readOnlyExampleEl = selectEl.closest('.example-panel').querySelector('.request-body-param');
558
571
  const userInputExampleTextareaEl = selectEl.closest('.example-panel').querySelector('.request-body-param-user-input');
559
572
  userInputExampleTextareaEl.value = readOnlyExampleEl.innerText;
573
+
574
+ const requestPanelEl = this.getRequestPanel({ target: selectEl });
575
+ this.liveCURLSyntaxUpdate(requestPanelEl);
560
576
  }, 0, exampleDropdownEl);
561
577
  }
562
578
 
@@ -672,6 +688,16 @@ export default class ApiRequest extends LitElement {
672
688
  data-example-format = "${v.exampleFormat}"
673
689
  style="width:100%; resize:vertical;"
674
690
  .textContent = "${this.fillRequestFieldsWithExample === 'true' ? (v.exampleFormat === 'text' ? v.exampleValue : JSON.stringify(v.exampleValue, null, 2)) : ''}"
691
+ @input=${(e) => {
692
+ const requestPanelEl = this.getRequestPanel(e);
693
+ this.liveCURLSyntaxUpdate(requestPanelEl);
694
+ }}
695
+ @keydown=${(e) => {
696
+ if ((e.keyCode === 10 || e.keyCode === 13) && e.ctrlKey) {
697
+ return this.onTryClick(e);
698
+ }
699
+ }}
700
+
675
701
  ></textarea>
676
702
  </div>
677
703
  `)}
@@ -717,7 +743,7 @@ export default class ApiRequest extends LitElement {
717
743
  .data = '${schemaAsObj}'
718
744
  schema-expand-level = "${this.schemaExpandLevel}"
719
745
  schema-description-expanded = "${this.schemaDescriptionExpanded}"
720
- allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}",
746
+ allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}"
721
747
  schema-hide-read-only = "${this.schemaHideReadOnly}"
722
748
  schema-hide-write-only = "${this.schemaHideWriteOnly}"
723
749
  exportparts = "schema-description:schema-description, schema-multiline-toggle:schema-multiline-toggle"
@@ -732,7 +758,7 @@ export default class ApiRequest extends LitElement {
732
758
  .data = "${schemaAsObj}"
733
759
  schema-expand-level = "${this.schemaExpandLevel}"
734
760
  schema-description-expanded = "${this.schemaDescriptionExpanded}"
735
- allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}",
761
+ allow-schema-description-expand-toggle = "${this.allowSchemaDescriptionExpandToggle}"
736
762
  schema-hide-read-only = "${this.schemaHideReadOnly}"
737
763
  schema-hide-write-only = "${this.schemaHideWriteOnly}"
738
764
  exportparts = "schema-description:schema-description, schema-multiline-toggle:schema-multiline-toggle"
@@ -888,7 +914,7 @@ export default class ApiRequest extends LitElement {
888
914
  data-example = "${Array.isArray(fieldExamples) ? fieldExamples.join('~|~') : fieldExamples}"
889
915
  data-array = "true"
890
916
  placeholder = "add-multiple &#x21a9;"
891
- .value = "${Array.isArray(fieldExamples) ? Array.isArray(fieldExamples[0]) ? fieldExamples[0] : [fieldExamples[0]] : [fieldExamples]}"
917
+ .value = "${Array.isArray(fieldExamples) ? Array.isArray(fieldExamples[0]) ? fieldExamples[0] : fieldExamples : []}"
892
918
  >
893
919
  </tag-input>
894
920
  `
@@ -965,7 +991,7 @@ export default class ApiRequest extends LitElement {
965
991
  }`);
966
992
  }
967
993
  return html`
968
- <table style="width:100%;" class="m-table">
994
+ <table role="presentation" style="width:100%;" class="m-table">
969
995
  ${formDataTableRows}
970
996
  </table>
971
997
  `;
@@ -985,6 +1011,15 @@ export default class ApiRequest extends LitElement {
985
1011
  `;
986
1012
  }
987
1013
 
1014
+ curlSyntaxTemplate(display = 'flex') {
1015
+ return html`
1016
+ <div class="col m-markdown" style="flex:1; display:${display}; position:relative; max-width: 100%;">
1017
+ <button class="toolbar-btn" style = "position:absolute; top:12px; right:8px" @click='${(e) => { copyToClipboard(this.curlSyntax.replace(/\\$/, ''), e); }}' part="btn btn-fill"> Copy </button>
1018
+ <pre style="white-space:pre"><code>${unsafeHTML(Prism.highlight(this.curlSyntax.trim().replace(/\\$/, ''), Prism.languages.shell, 'shell'))}</code></pre>
1019
+ </div>
1020
+ `;
1021
+ }
1022
+
988
1023
  apiResponseTabTemplate() {
989
1024
  let responseFormat = '';
990
1025
  let responseContent = '';
@@ -1017,15 +1052,21 @@ export default class ApiRequest extends LitElement {
1017
1052
  }}">
1018
1053
  <button class="tab-btn ${this.activeResponseTab === 'response' ? 'active' : ''}" data-tab = 'response' > RESPONSE</button>
1019
1054
  <button class="tab-btn ${this.activeResponseTab === 'headers' ? 'active' : ''}" data-tab = 'headers' > RESPONSE HEADERS</button>
1020
- <button class="tab-btn ${this.activeResponseTab === 'curl' ? 'active' : ''}" data-tab = 'curl'>CURL</button>
1055
+ ${this.showCurlBeforeTry === 'true'
1056
+ ? ''
1057
+ : html`<button class="tab-btn ${this.activeResponseTab === 'curl' ? 'active' : ''}" data-tab = 'curl'>CURL</button>`}
1021
1058
  </div>
1022
1059
  ${this.responseIsBlob
1023
1060
  ? html`
1024
1061
  <div class="tab-content col" style="flex:1; display:${this.activeResponseTab === 'response' ? 'flex' : 'none'};">
1062
+ ${this.responseBlobType === 'image'
1063
+ ? html`<img style="max-height:var(--resp-area-height, 400px); object-fit:contain;" class="mar-top-8" src="${this.responseBlobUrl}"></img>`
1064
+ : ''
1065
+ }
1025
1066
  <button class="m-btn thin-border mar-top-8" style="width:135px" @click='${(e) => { downloadResource(this.responseBlobUrl, this.respContentDisposition, e); }}' part="btn btn-outline">
1026
1067
  DOWNLOAD
1027
1068
  </button>
1028
- ${this.responseBlobType === 'view'
1069
+ ${this.responseBlobType === 'view' || this.responseBlobType === 'image'
1029
1070
  ? html`<button class="m-btn thin-border mar-top-8" style="width:135px" @click='${(e) => { viewResource(this.responseBlobUrl, e); }}' part="btn btn-outline">VIEW (NEW TAB)</button>`
1030
1071
  : ''
1031
1072
  }
@@ -1040,10 +1081,7 @@ export default class ApiRequest extends LitElement {
1040
1081
  <button class="toolbar-btn" style = "position:absolute; top:12px; right:8px" @click='${(e) => { copyToClipboard(this.responseHeaders, e); }}' part="btn btn-fill"> Copy </button>
1041
1082
  <pre style="white-space:pre"><code>${unsafeHTML(Prism.highlight(this.responseHeaders, Prism.languages.css, 'css'))}</code></pre>
1042
1083
  </div>
1043
- <div class="tab-content col m-markdown" style="flex:1; display:${this.activeResponseTab === 'curl' ? 'flex' : 'none'};">
1044
- <button class="toolbar-btn" style = "position:absolute; top:12px; right:8px" @click='${(e) => { copyToClipboard(this.curlSyntax.replace(/\\$/, ''), e); }}' part="btn btn-fill"> Copy </button>
1045
- <pre style="white-space:pre"><code>${unsafeHTML(Prism.highlight(this.curlSyntax.trim().replace(/\\$/, ''), Prism.languages.shell, 'shell'))}</code></pre>
1046
- </div>
1084
+ ${this.showCurlBeforeTry === 'true' ? '' : this.curlSyntaxTemplate(this.activeResponseTab === 'curl' ? 'flex' : 'none')}
1047
1085
  </div>`;
1048
1086
  }
1049
1087
 
@@ -1073,7 +1111,7 @@ export default class ApiRequest extends LitElement {
1073
1111
  `;
1074
1112
 
1075
1113
  return html`
1076
- <div style="display:flex; align-items:flex-end; margin:16px 0; font-size:var(--font-size-small);">
1114
+ <div style="display:flex; align-items:flex-end; margin:16px 0; font-size:var(--font-size-small);" part="wrap-request-btn">
1077
1115
  <div class="hide-in-small-screen" style="flex-direction:column; margin:0; width:calc(100% - 60px);">
1078
1116
  <div style="display:flex; flex-direction:row; align-items:center; overflow:hidden;">
1079
1117
  ${selectedServerHtml}
@@ -1108,6 +1146,9 @@ export default class ApiRequest extends LitElement {
1108
1146
  }
1109
1147
  <button class="m-btn primary thin-border" part="btn btn-try" @click="${this.onTryClick}">TRY</button>
1110
1148
  </div>
1149
+ <div class="row" style="font-size:var(--font-size-small); margin:5px 0">
1150
+ ${this.showCurlBeforeTry === 'true' ? this.curlSyntaxTemplate() : ''}
1151
+ </div>
1111
1152
  ${this.responseMessage === '' ? '' : this.apiResponseTabTemplate()}
1112
1153
  `;
1113
1154
  }
@@ -1133,27 +1174,12 @@ export default class ApiRequest extends LitElement {
1133
1174
  requestPanelInputEls.forEach((el) => { el.value = ''; });
1134
1175
  }
1135
1176
 
1136
- async onTryClick(e) {
1137
- // const me = this;
1138
- const tryBtnEl = e.target;
1177
+ buildFetchURL(requestPanelEl) {
1139
1178
  let fetchUrl;
1140
- let curlUrl;
1141
- let curl = '';
1142
- let curlHeaders = '';
1143
- let curlData = '';
1144
- let curlForm = '';
1145
- const respEl = this.closest('.expanded-req-resp-container, .req-resp-container')?.getElementsByTagName('api-response')[0];
1146
- const acceptHeader = respEl?.selectedMimeType;
1147
- const requestPanelEl = e.target.closest('.request-panel');
1148
1179
  const pathParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='path']")];
1149
1180
  const queryParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='query']")];
1150
1181
  const queryParamObjTypeEls = [...requestPanelEl.querySelectorAll("[data-ptype='query-object']")];
1151
- const headerParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='header']")];
1152
- const requestBodyContainerEl = requestPanelEl.querySelector('.request-body-container');
1153
1182
  fetchUrl = this.path;
1154
- const fetchOptions = {
1155
- method: this.method.toUpperCase(),
1156
- };
1157
1183
  // Generate URL using Path Params
1158
1184
  pathParamEls.map((el) => {
1159
1185
  fetchUrl = fetchUrl.replace(`{${el.dataset.pname}}`, encodeURIComponent(el.value));
@@ -1202,7 +1228,7 @@ export default class ApiRequest extends LitElement {
1202
1228
  const queryParam = new URLSearchParams();
1203
1229
  try {
1204
1230
  let queryParamObj = {};
1205
- const { paramSerializeStyle, paramSerializeExplode } = el.dataset;
1231
+ const { paramSerializeStyle, paramSerializeExplode, pname } = el.dataset;
1206
1232
  queryParamObj = Object.assign(queryParamObj, JSON.parse(el.value.replace(/\s+/g, ' ')));
1207
1233
  if (el.dataset.paramAllowReserved === 'true') {
1208
1234
  queryParamsWithReservedCharsAllowed.push(el.dataset.pname);
@@ -1215,29 +1241,30 @@ export default class ApiRequest extends LitElement {
1215
1241
  }
1216
1242
  } else {
1217
1243
  for (const key in queryParamObj) {
1244
+ const pKey = `${pname}[${key}]`;
1218
1245
  if (typeof queryParamObj[key] === 'object') {
1219
1246
  if (Array.isArray(queryParamObj[key])) {
1220
1247
  if (paramSerializeStyle === 'spaceDelimited') {
1221
- queryParam.append(key, queryParamObj[key].join(' '));
1248
+ queryParam.append(pKey, queryParamObj[key].join(' '));
1222
1249
  } else if (paramSerializeStyle === 'pipeDelimited') {
1223
- queryParam.append(key, queryParamObj[key].join('|'));
1250
+ queryParam.append(pKey, queryParamObj[key].join('|'));
1224
1251
  } else {
1225
1252
  if (paramSerializeExplode === 'true') { // eslint-disable-line no-lonely-if
1226
1253
  queryParamObj[key].forEach((v) => {
1227
- queryParam.append(key, v);
1254
+ queryParam.append(pKey, v);
1228
1255
  });
1229
1256
  } else {
1230
- queryParam.append(key, queryParamObj[key]);
1257
+ queryParam.append(pKey, queryParamObj[key]);
1231
1258
  }
1232
1259
  }
1233
1260
  }
1234
1261
  } else {
1235
- queryParam.append(key, queryParamObj[key]);
1262
+ queryParam.append(pKey, queryParamObj[key]);
1236
1263
  }
1237
1264
  }
1238
1265
  }
1239
1266
  } catch (err) {
1240
- console.log('RapiDoc: unable to parse %s into object', el.value); // eslint-disable-line no-console
1267
+ console.error('RapiDoc: unable to parse %s into object', el.value); // eslint-disable-line no-console
1241
1268
  }
1242
1269
  if (queryParam.toString()) {
1243
1270
  urlQueryParamsMap.set(el.dataset.pname, queryParam);
@@ -1246,7 +1273,6 @@ export default class ApiRequest extends LitElement {
1246
1273
  }
1247
1274
  let urlQueryParamString = '';
1248
1275
  if (urlQueryParamsMap.size) {
1249
- urlQueryParamString = '?';
1250
1276
  urlQueryParamsMap.forEach((val, pname) => {
1251
1277
  if (queryParamsWithReservedCharsAllowed.includes(pname)) {
1252
1278
  urlQueryParamString += `${pname}=`;
@@ -1258,7 +1284,9 @@ export default class ApiRequest extends LitElement {
1258
1284
  });
1259
1285
  urlQueryParamString = urlQueryParamString.slice(0, -1);
1260
1286
  }
1261
- fetchUrl = `${fetchUrl}${urlQueryParamString}`;
1287
+ if (urlQueryParamString.length !== 0) {
1288
+ fetchUrl = `${fetchUrl}${fetchUrl.includes('?') ? '&' : '?'}${urlQueryParamString}`;
1289
+ }
1262
1290
 
1263
1291
  // Add authentication Query-Param if provided
1264
1292
  this.api_keys
@@ -1267,23 +1295,21 @@ export default class ApiRequest extends LitElement {
1267
1295
  fetchUrl = `${fetchUrl}${fetchUrl.includes('?') ? '&' : '?'}${v.name}=${encodeURIComponent(v.finalKeyValue)}`;
1268
1296
  });
1269
1297
 
1270
- // Final URL for API call
1271
1298
  fetchUrl = `${this.serverUrl.replace(/\/$/, '')}${fetchUrl}`;
1272
- if (fetchUrl.startsWith('http') === false) {
1273
- const url = new URL(fetchUrl, window.location.href);
1274
- curlUrl = url.href;
1275
- } else {
1276
- curlUrl = fetchUrl;
1277
- }
1278
- curl = `curl -X ${this.method.toUpperCase()} "${curlUrl}" \\\n`;
1299
+ return fetchUrl;
1300
+ }
1301
+
1302
+ buildFetchHeaders(requestPanelEl) {
1303
+ const respEl = this.closest('.expanded-req-resp-container, .req-resp-container')?.getElementsByTagName('api-response')[0];
1304
+ const headerParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='header']")];
1305
+ const requestBodyContainerEl = requestPanelEl.querySelector('.request-body-container');
1306
+ const acceptHeader = respEl?.selectedMimeType;
1279
1307
  const reqHeaders = new Headers();
1280
1308
  if (acceptHeader) {
1281
1309
  // Uses the acceptHeader from Response panel
1282
1310
  reqHeaders.append('Accept', acceptHeader);
1283
- curlHeaders += ` -H "Accept: ${acceptHeader}" \\\n`;
1284
1311
  } else if (this.accept) {
1285
1312
  reqHeaders.append('Accept', this.accept);
1286
- curlHeaders += ` -H "Accept: ${this.accept}" \\\n`;
1287
1313
  }
1288
1314
 
1289
1315
  // Add Authentication Header if provided
@@ -1291,18 +1317,32 @@ export default class ApiRequest extends LitElement {
1291
1317
  .filter((v) => (v.in === 'header'))
1292
1318
  .forEach((v) => {
1293
1319
  reqHeaders.append(v.name, v.finalKeyValue);
1294
- curlHeaders += ` -H "${v.name}: ${v.finalKeyValue}" \\\n`;
1295
1320
  });
1296
1321
 
1297
1322
  // Add Header Params
1298
1323
  headerParamEls.map((el) => {
1299
1324
  if (el.value) {
1300
1325
  reqHeaders.append(el.dataset.pname, el.value);
1301
- curlHeaders += ` -H "${el.dataset.pname}: ${el.value}" \\\n`;
1302
1326
  }
1303
1327
  });
1304
1328
 
1305
- // Request Body Params
1329
+ if (requestBodyContainerEl) {
1330
+ const requestBodyType = requestBodyContainerEl.dataset.selectedRequestBodyType;
1331
+ // Common for all request-body
1332
+ if (!requestBodyType.includes('form-data')) {
1333
+ // For multipart/form-data dont set the content-type to allow creation of browser generated part boundaries
1334
+ reqHeaders.append('Content-Type', requestBodyType);
1335
+ }
1336
+ }
1337
+
1338
+ return reqHeaders;
1339
+ }
1340
+
1341
+ buildFetchBodyOptions(requestPanelEl) {
1342
+ const requestBodyContainerEl = requestPanelEl.querySelector('.request-body-container');
1343
+ const fetchOptions = {
1344
+ method: this.method.toUpperCase(),
1345
+ };
1306
1346
  if (requestBodyContainerEl) {
1307
1347
  const requestBodyType = requestBodyContainerEl.dataset.selectedRequestBodyType;
1308
1348
  if (requestBodyType.includes('form-urlencoded')) {
@@ -1328,7 +1368,6 @@ export default class ApiRequest extends LitElement {
1328
1368
  formUrlDynParams.append(prop, JSON.stringify(tmpObj[prop]));
1329
1369
  }
1330
1370
  fetchOptions.body = formUrlDynParams;
1331
- curlData = ` -d ${formUrlDynParams.toString()} \\\n`;
1332
1371
  }
1333
1372
  } else {
1334
1373
  // url-encoded Form Params (regular)
@@ -1347,7 +1386,6 @@ export default class ApiRequest extends LitElement {
1347
1386
  }
1348
1387
  });
1349
1388
  fetchOptions.body = formUrlParams;
1350
- curlData = ` -d ${formUrlParams.toString()} \\\n`;
1351
1389
  }
1352
1390
  } else if (requestBodyType.includes('form-data')) {
1353
1391
  const formDataParams = new FormData();
@@ -1356,15 +1394,10 @@ export default class ApiRequest extends LitElement {
1356
1394
  if (el.dataset.array === 'false') {
1357
1395
  if (el.type === 'file' && el.files[0]) {
1358
1396
  formDataParams.append(el.dataset.pname, el.files[0], el.files[0].name);
1359
- curlForm += ` -F "${el.dataset.pname}=@${el.files[0].name}" \\\n`;
1360
1397
  } else if (el.value) {
1361
1398
  formDataParams.append(el.dataset.pname, el.value);
1362
- curlForm += ` -F "${el.dataset.pname}=${el.value}" \\\n`;
1363
1399
  }
1364
1400
  } else if (el.value && Array.isArray(el.value)) {
1365
- el.value.forEach((v) => {
1366
- curlForm = `${curlForm} -F "${el.dataset.pname}[]=${v}" \\\n`;
1367
- });
1368
1401
  formDataParams.append(el.dataset.pname, el.value.join(','));
1369
1402
  }
1370
1403
  });
@@ -1373,34 +1406,27 @@ export default class ApiRequest extends LitElement {
1373
1406
  const bodyParamFileEl = requestPanelEl.querySelector('.request-body-param-file');
1374
1407
  if (bodyParamFileEl?.files[0]) {
1375
1408
  fetchOptions.body = bodyParamFileEl.files[0]; // eslint-disable-line prefer-destructuring
1376
- curlData = ` --data-binary @${bodyParamFileEl.files[0].name} \\\n`;
1377
1409
  }
1378
1410
  } else if (requestBodyType.includes('json') || requestBodyType.includes('xml') || requestBodyType.includes('text')) {
1379
1411
  const exampleTextAreaEl = requestPanelEl.querySelector('.request-body-param-user-input');
1380
1412
  if (exampleTextAreaEl?.value) {
1381
1413
  fetchOptions.body = exampleTextAreaEl.value;
1382
- if (requestBodyType.includes('json')) {
1383
- try {
1384
- curlData = ` -d '${JSON.stringify(JSON.parse(exampleTextAreaEl.value))}' \\\n`;
1385
- } catch (err) {
1386
- // Ignore.
1387
- }
1388
- }
1389
- if (!curlData) {
1390
- curlData = ` -d '${exampleTextAreaEl.value.replace(/'/g, '\'"\'"\'')}' \\\n`;
1391
- }
1392
1414
  }
1393
1415
  }
1394
- // Common for all request-body
1395
- if (!requestBodyType.includes('form-data')) {
1396
- // For multipart/form-data dont set the content-type to allow creation of browser generated part boundaries
1397
- reqHeaders.append('Content-Type', requestBodyType);
1398
- }
1399
- curlHeaders += ` -H "Content-Type: ${requestBodyType}" \\\n`;
1400
1416
  }
1417
+
1418
+ return fetchOptions;
1419
+ }
1420
+
1421
+ async onTryClick(e) {
1422
+ const tryBtnEl = e.target;
1423
+ const requestPanelEl = tryBtnEl.closest('.request-panel');
1424
+ const fetchUrl = this.buildFetchURL(requestPanelEl);
1425
+ const fetchOptions = this.buildFetchBodyOptions(requestPanelEl);
1426
+ const reqHeaders = this.buildFetchHeaders(requestPanelEl);
1401
1427
  this.responseUrl = '';
1402
1428
  this.responseHeaders = [];
1403
- this.curlSyntax = '';
1429
+ this.curlSyntax = this.generateCURLSyntax(fetchUrl, reqHeaders, fetchOptions, requestPanelEl);
1404
1430
  this.responseStatus = 'success';
1405
1431
  this.responseIsBlob = false;
1406
1432
 
@@ -1409,22 +1435,28 @@ export default class ApiRequest extends LitElement {
1409
1435
  URL.revokeObjectURL(this.responseBlobUrl);
1410
1436
  this.responseBlobUrl = '';
1411
1437
  }
1412
- this.curlSyntax = `${curl}${curlHeaders}${curlData}${curlForm}`;
1413
1438
  if (this.fetchCredentials) {
1414
1439
  fetchOptions.credentials = this.fetchCredentials;
1415
1440
  }
1416
1441
  const controller = new AbortController();
1417
1442
  const { signal } = controller;
1418
1443
  fetchOptions.headers = reqHeaders;
1419
- const fetchRequest = new Request(fetchUrl, fetchOptions);
1444
+ const tempRequest = { url: fetchUrl, ...fetchOptions };
1420
1445
  this.dispatchEvent(new CustomEvent('before-try', {
1421
1446
  bubbles: true,
1422
1447
  composed: true,
1423
1448
  detail: {
1424
- request: fetchRequest,
1449
+ request: tempRequest,
1425
1450
  controller,
1426
1451
  },
1427
1452
  }));
1453
+ const updatedFetchOptions = {
1454
+ method: tempRequest.method,
1455
+ headers: tempRequest.headers,
1456
+ credentials: tempRequest.credentials,
1457
+ body: tempRequest.body,
1458
+ };
1459
+ const fetchRequest = new Request(tempRequest.url, updatedFetchOptions);
1428
1460
 
1429
1461
  let fetchResponse;
1430
1462
  let responseClone;
@@ -1439,6 +1471,23 @@ export default class ApiRequest extends LitElement {
1439
1471
  const startTime = performance.now();
1440
1472
  fetchResponse = await fetch(fetchRequest, { signal });
1441
1473
  const endTime = performance.now();
1474
+ // Allow to modify response
1475
+ /*
1476
+ let resolveModifiedResponse; // Create a promise that will be resolved from the event listener
1477
+ const modifiedResponsePromise = new Promise((resolve) => {
1478
+ resolveModifiedResponse = resolve;
1479
+ });
1480
+ this.dispatchEvent(new CustomEvent('fetched-try', {
1481
+ bubbles: true,
1482
+ composed: true,
1483
+ detail: {
1484
+ request: fetchRequest,
1485
+ response: fetchResponse,
1486
+ resolveModifiedResponse, // pass the resolver function
1487
+ },
1488
+ }));
1489
+ fetchResponse = await modifiedResponsePromise; // Wait for the modified response
1490
+ */
1442
1491
  responseClone = fetchResponse.clone(); // create a response clone to allow reading response body again (response.json, response.text etc)
1443
1492
  tryBtnEl.disabled = false;
1444
1493
  this.responseMessage = html`${fetchResponse.statusText ? `${fetchResponse.statusText}:${fetchResponse.status}` : fetchResponse.status} <div style="color:var(--light-fg)"> Took ${Math.round(endTime - startTime)} milliseconds </div>`;
@@ -1478,6 +1527,9 @@ export default class ApiRequest extends LitElement {
1478
1527
  } else if (/^font\/|tar$|zip$|7z$|rtf$|msword$|excel$|\/pdf$|\/octet-stream$|^application\/vnd\./.test(contentType)) {
1479
1528
  this.responseIsBlob = true;
1480
1529
  this.responseBlobType = 'download';
1530
+ } else if (/^image/.test(contentType)) {
1531
+ this.responseIsBlob = true;
1532
+ this.responseBlobType = 'image';
1481
1533
  } else if (/^audio|^image|^video/.test(contentType)) {
1482
1534
  this.responseIsBlob = true;
1483
1535
  this.responseBlobType = 'view';
@@ -1522,6 +1574,7 @@ export default class ApiRequest extends LitElement {
1522
1574
  },
1523
1575
  }));
1524
1576
  this.responseMessage = 'Request Aborted';
1577
+ this.responseText = 'Request Aborted';
1525
1578
  } else {
1526
1579
  this.dispatchEvent(new CustomEvent('after-try', {
1527
1580
  bubbles: true,
@@ -1537,6 +1590,96 @@ export default class ApiRequest extends LitElement {
1537
1590
  this.requestUpdate();
1538
1591
  }
1539
1592
 
1593
+ liveCURLSyntaxUpdate(requestPanelEl) {
1594
+ this.applyCURLSyntax(requestPanelEl);
1595
+ this.requestUpdate();
1596
+ }
1597
+
1598
+ onGenerateCURLClick(e) {
1599
+ const requestPanelEl = this.getRequestPanel(e);
1600
+ this.applyCURLSyntax(requestPanelEl);
1601
+ }
1602
+
1603
+ getRequestPanel(e) {
1604
+ return e.target.closest('.request-panel');
1605
+ }
1606
+
1607
+ applyCURLSyntax(requestPanelEl) {
1608
+ const fetchUrl = this.buildFetchURL(requestPanelEl);
1609
+ const fetchOptions = this.buildFetchBodyOptions(requestPanelEl);
1610
+ const fetchHeaders = this.buildFetchHeaders(requestPanelEl);
1611
+
1612
+ this.curlSyntax = this.generateCURLSyntax(fetchUrl, fetchHeaders, fetchOptions, requestPanelEl);
1613
+ }
1614
+
1615
+ generateCURLSyntax(fetchUrl, fetchHeaders, fetchOptions, requestPanelEl) {
1616
+ let curlUrl;
1617
+ let curl = '';
1618
+ let curlHeaders = '';
1619
+ let curlData = '';
1620
+ let curlForm = '';
1621
+ const requestBodyContainerEl = requestPanelEl.querySelector('.request-body-container');
1622
+
1623
+ if (fetchUrl.startsWith('http') === false) {
1624
+ const url = new URL(fetchUrl, window.location.href);
1625
+ curlUrl = url.href;
1626
+ } else {
1627
+ curlUrl = fetchUrl;
1628
+ }
1629
+
1630
+ curl = `curl -X ${this.method.toUpperCase()} "${curlUrl}" \\\n`;
1631
+
1632
+ fetchHeaders.forEach((value, key) => {
1633
+ let tempHeaderArray = value.split(',');
1634
+ tempHeaderArray = tempHeaderArray.map((el) => el.trim()).filter((string, index) => tempHeaderArray.indexOf(string) === index);
1635
+ fetchHeaders.set(key, tempHeaderArray.join(', '));
1636
+ });
1637
+
1638
+ curlHeaders = Array.from(fetchHeaders).map(([key, value]) => ` -H "${key}: ${value}"`).join('\\\n');
1639
+ if (curlHeaders) {
1640
+ curlHeaders = `${curlHeaders} \\\n`;
1641
+ }
1642
+ if (fetchOptions.body instanceof URLSearchParams) {
1643
+ curlData = ` -d ${fetchOptions.body.toString()} \\\n`;
1644
+ } else if (fetchOptions.body instanceof File) {
1645
+ curlData = ` --data-binary @${fetchOptions.body.name} \\\n`;
1646
+ } else if (fetchOptions.body instanceof FormData) {
1647
+ curlForm = Array.from(fetchOptions.body).reduce((aggregator, [key, value]) => {
1648
+ if (value instanceof File) {
1649
+ return [...aggregator, ` -F "${key}=@${value.name}"`];
1650
+ }
1651
+
1652
+ const multiple = value.match(/([^,],)/gm);
1653
+
1654
+ if (multiple) {
1655
+ const multipleResults = multiple.map((one) => `-F "${key}[]=${one}"`);
1656
+
1657
+ return [...aggregator, ...multipleResults];
1658
+ }
1659
+
1660
+ return [...aggregator, ` -F "${key}=${value}"`];
1661
+ }, []).join('\\\n');
1662
+ } else if (requestBodyContainerEl && requestBodyContainerEl.dataset.selectedRequestBodyType) {
1663
+ const requestBodyType = requestBodyContainerEl.dataset.selectedRequestBodyType;
1664
+ const exampleTextAreaEl = requestPanelEl.querySelector('.request-body-param-user-input');
1665
+ if (exampleTextAreaEl?.value) {
1666
+ fetchOptions.body = exampleTextAreaEl.value;
1667
+ if (requestBodyType.includes('json')) {
1668
+ try {
1669
+ curlData = ` -d '${JSON.stringify(JSON.parse(exampleTextAreaEl.value))}' \\\n`;
1670
+ } catch (err) {
1671
+ // Ignore.
1672
+ }
1673
+ }
1674
+ if (!curlData) {
1675
+ curlData = ` -d '${exampleTextAreaEl.value.replace(/'/g, '\'"\'"\'')}' \\\n`;
1676
+ }
1677
+ }
1678
+ }
1679
+
1680
+ return `${curl}${curlHeaders}${curlData}${curlForm}`;
1681
+ }
1682
+
1540
1683
  onAddRemoveFileInput(e, pname, ptype) {
1541
1684
  if (e.target.tagName.toLowerCase() !== 'button') {
1542
1685
  return;
@@ -1592,7 +1735,8 @@ export default class ApiRequest extends LitElement {
1592
1735
  }
1593
1736
 
1594
1737
  disconnectedCallback() {
1595
- // Cleanup ObjectURL forthe blob data if this component created one
1738
+ this.curlSyntax = '';
1739
+ // Cleanup ObjectURL for the blob data if this component created one
1596
1740
  if (this.responseBlobUrl) {
1597
1741
  URL.revokeObjectURL(this.responseBlobUrl);
1598
1742
  this.responseBlobUrl = '';