rapidoc 12.0.0 → 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 (77) hide show
  1. package/README.md +127 -25
  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 +541 -1309
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +21 -18
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +43 -0
  10. package/src/components/api-response.ts +9 -4
  11. package/src/components/schema-base-element.ts +18 -4
  12. package/src/components/schema-table.ts +58 -13
  13. package/src/components/schema-tree.ts +7 -0
  14. package/src/components/tag-input.ts +5 -9
  15. package/src/flow.js +3 -0
  16. package/src/rapi-flow.ts +1178 -0
  17. package/src/rapidoc-mini.ts +27 -6
  18. package/src/rapidoc.ts +108 -62
  19. package/src/styles/README.md +96 -0
  20. package/src/styles/css-bundle.js +2 -2
  21. package/src/styles/endpoint-styles.js +6 -1
  22. package/src/styles/flow-styles.js +248 -0
  23. package/src/styles/nav-styles.js +39 -15
  24. package/src/styles/schema-styles.js +13 -5
  25. package/src/styles/theme-tokens.js +2 -2
  26. package/src/templates/README.md +89 -0
  27. package/src/templates/api-response-template.js +14 -10
  28. package/src/templates/callback-template.js +1 -1
  29. package/src/templates/downloads-template.js +12 -27
  30. package/src/templates/endpoint-template.js +15 -24
  31. package/src/templates/expanded-endpoint-template.js +8 -4
  32. package/src/templates/focused-endpoint-template.js +4 -3
  33. package/src/templates/navbar-template.js +20 -11
  34. package/src/templates/request-body-template.js +29 -7
  35. package/src/templates/request-params-template.js +15 -4
  36. package/src/templates/security-scheme-template.js +11 -2
  37. package/src/templates/server-template.js +3 -2
  38. package/src/utils/README.md +146 -0
  39. package/src/utils/arazzo/README.md +119 -0
  40. package/src/utils/arazzo/components.js +15 -0
  41. package/src/utils/arazzo/graph.js +269 -0
  42. package/src/utils/arazzo/index.js +52 -0
  43. package/src/utils/arazzo/operations.js +106 -0
  44. package/src/utils/arazzo/parse.js +196 -0
  45. package/src/utils/arazzo/refs.js +40 -0
  46. package/src/utils/arazzo/svg.js +707 -0
  47. package/src/utils/curl-utils.js +5 -2
  48. package/src/utils/highlighter.js +3 -0
  49. package/src/utils/mcp/README.md +155 -0
  50. package/src/utils/mcp/common.js +45 -346
  51. package/src/utils/mcp/manifest.js +382 -0
  52. package/src/utils/mcp/project/server.ts +53 -0
  53. package/src/utils/mcp/project.js +88 -0
  54. package/src/utils/mcp/runtime.js +289 -0
  55. package/src/utils/mcp/schema.js +98 -0
  56. package/src/utils/mcp/workflow-runtime.js +374 -0
  57. package/src/utils/mcp/workflows.js +240 -0
  58. package/src/utils/mcp/zip.js +102 -0
  59. package/src/utils/mcp-generator.js +29 -125
  60. package/src/utils/mock-interceptor.js +134 -85
  61. package/src/utils/request-utils.js +61 -0
  62. package/src/utils/schema-utils.js +33 -6
  63. package/src/utils/slugger.js +5 -7
  64. package/src/utils/spec-parser.js +190 -164
  65. package/src/utils/url-utils.js +36 -1
  66. package/dist/rapidoc-static-min.js +0 -13542
  67. package/src/static/index.d.ts +0 -149
  68. package/src/static/index.js +0 -62
  69. package/src/static/navigation.js +0 -497
  70. package/src/static/render.js +0 -490
  71. package/src/static/serializer.js +0 -216
  72. package/src/utils/mcp/generators/csharp.js +0 -242
  73. package/src/utils/mcp/generators/go.js +0 -298
  74. package/src/utils/mcp/generators/java.js +0 -330
  75. package/src/utils/mcp/generators/python.js +0 -248
  76. package/src/utils/mcp/generators/rust.js +0 -281
  77. package/src/utils/mcp/generators/typescript.js +0 -290
@@ -171,7 +171,7 @@ export function expandedEndpointBodyTemplate(
171
171
  schema-hide-write-only="${this.schemaHideWriteOnly === "never" ? "false" : path.isWebhook ? "true" : "false"}"
172
172
  fetch-credentials="${this.fetchCredentials}"
173
173
  exportparts="section-request-title:section-request-title, heading:heading, heading-4:heading-4, heading-5:heading-5, heading-6:heading-6,
174
- 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,
174
+ 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-fill-example:btn-fill-example, btn-clear-resp:btn-clear-resp,
175
175
  tab-panel:tab-panel, tab-btn:tab-btn, tab-btn-row:tab-btn-row, tab-content:tab-content,
176
176
  file-input:file-input, textbox:textbox, textbox-param:textbox-param, textarea:textarea, textarea-param:textarea-param,
177
177
  anchor:anchor, anchor-param-example:anchor-param-example, anchor-param-constraint:anchor-param-constraint, param-list:param-list, param:param, param-label:param-label, param-name:param-name, param-type:param-type, param-input:param-input, param-constraint:param-constraint, param-constraint-label:param-constraint-label, param-details:param-details, param-example-list:param-example-list, value-list:value-list, value-list-label:value-list-label, value-list-items:value-list-items, schema-description:schema-description, schema-multiline-toggle:schema-multiline-toggle"
@@ -228,9 +228,13 @@ export default function expandedEndpointTemplate() {
228
228
  )}
229
229
  </div>`)}
230
230
  </section>
231
- <section class="regular-font section-gap--read-mode" part="section-operations-in-tag">
232
- ${tag.paths.map((path, idx) => expandedEndpointBodyTemplate.call(this, path, tag.name, tag.description, idx === 0))}
233
- </section>
231
+ ${
232
+ tag.paths.length > 0
233
+ ? html`<section class="regular-font section-gap--read-mode" part="section-operations-in-tag">
234
+ ${tag.paths.map((path, idx) => expandedEndpointBodyTemplate.call(this, path, tag.name, tag.description, idx === 0))}
235
+ </section>`
236
+ : ""
237
+ }
234
238
  `;
235
239
 
236
240
  if (this.resolvedSpec.tagGroups && this.resolvedSpec.tagGroups.length > 0) {
@@ -36,8 +36,9 @@ function defaultContentTemplate() {
36
36
  if (this.showInfo === "true") {
37
37
  return wrapFocusedTemplate(html`${overviewTemplate.call(this)}${downloadsTemplate.call(this)}`);
38
38
  }
39
- const selectedTagObj = this.resolvedSpec.tags[0];
40
- const selectedPathObj = this.resolvedSpec.tags[0]?.paths[0];
39
+ // the first tag with operations: a description-only tag (show-unused-tags) has none
40
+ const selectedTagObj = this.resolvedSpec.tags.find((t) => t.paths.length > 0);
41
+ const selectedPathObj = selectedTagObj?.paths[0];
41
42
  return selectedTagObj && selectedPathObj
42
43
  ? wrapFocusedTemplate(
43
44
  expandedEndpointBodyTemplate.call(
@@ -79,7 +80,7 @@ export default function focusedEndpointTemplate() {
79
80
  if (!this.focusedElementId && this.resolvedSpec) {
80
81
  this.focusedElementId =
81
82
  this.showInfo === "false" || this.showInfo === false
82
- ? this.resolvedSpec.tags?.[0]?.paths?.[0]?.elementId || ""
83
+ ? this.resolvedSpec.tags?.find((t) => t.paths.length > 0)?.paths[0]?.elementId || ""
83
84
  : "overview";
84
85
  }
85
86
  if (!this.focusedElementId || !this.resolvedSpec) {
@@ -7,19 +7,13 @@ import { getMatchedPaths, isInternalOrHidden } from "~/utils/common-utils";
7
7
 
8
8
  export function expandCollapseNavBarTag(navLinkEl, action = "toggle") {
9
9
  const tagAndPathEl = navLinkEl?.closest(".nav-bar-tag-and-paths");
10
- const pathsUnderTagEl = tagAndPathEl?.querySelector(".nav-bar-paths-under-tag");
11
10
  if (tagAndPathEl) {
12
11
  const isExpanded = tagAndPathEl.classList.contains("expanded");
13
12
  if (isExpanded && (action === "toggle" || action === "collapse")) {
14
- if (pathsUnderTagEl) pathsUnderTagEl.style.maxHeight = 0;
15
13
  tagAndPathEl.classList.replace("expanded", "collapsed");
16
14
  tagAndPathEl.querySelector(".nav-bar-tag")?.setAttribute("aria-expanded", "false");
17
15
  } else if (!isExpanded && (action === "toggle" || action === "expand")) {
18
16
  tagAndPathEl.classList.replace("collapsed", "expanded");
19
- if (pathsUnderTagEl)
20
- pathsUnderTagEl.style.maxHeight = pathsUnderTagEl.scrollHeight
21
- ? `${pathsUnderTagEl.scrollHeight}px`
22
- : null;
23
17
  tagAndPathEl.querySelector(".nav-bar-tag")?.setAttribute("aria-expanded", "true");
24
18
  }
25
19
  }
@@ -61,6 +55,10 @@ export function isNavTagVisible(tag, searchVal) {
61
55
  if (isInternalOrHidden(tag)) {
62
56
  return false;
63
57
  }
58
+ // A description-only tag (show-unused-tags) has no operations to match, so the filter hides it
59
+ if (tag.descriptionOnly) {
60
+ return !searchVal;
61
+ }
64
62
  return (tag.paths || []).some((path) => isNavPathVisible(path, searchVal, tag.name));
65
63
  }
66
64
 
@@ -91,16 +89,11 @@ export function expandCollapseAll(event, action = "expand-all") {
91
89
  const elList = [...navEl.querySelectorAll(".nav-bar-tag-and-paths")];
92
90
  if (action === "expand-all") {
93
91
  elList.forEach((el) => {
94
- const navBarPathsUnderTagEl = el.querySelector(".nav-bar-paths-under-tag");
95
92
  el.classList.replace("collapsed", "expanded");
96
- if (navBarPathsUnderTagEl) {
97
- navBarPathsUnderTagEl.style.maxHeight = `${navBarPathsUnderTagEl?.scrollHeight}px`;
98
- }
99
93
  });
100
94
  } else {
101
95
  elList.forEach((el) => {
102
96
  el.classList.replace("expanded", "collapsed");
103
- // el.querySelector('.nav-bar-paths-under-tag').style.maxHeight = 0;
104
97
  });
105
98
  }
106
99
  }
@@ -338,6 +331,22 @@ export default function navbarTemplate() {
338
331
  <!-- TAGS AND PATHS (3-tier if tagGroups exist, else 2-tier) -->
339
332
  ${(() => {
340
333
  const renderNavTagItem = (tag) => {
334
+ // A description-only tag is a link to its section: no operations to expand or collapse
335
+ if (tag.descriptionOnly) {
336
+ if (!isNavTagVisible(tag, this.searchVal)) return "";
337
+ return html` <div class="nav-bar-tag-and-paths description-only">
338
+ <div
339
+ class="nav-bar-tag"
340
+ part="section-navbar-item section-navbar-tag"
341
+ id="link-${tag.elementId}"
342
+ data-action="navigate"
343
+ data-content-id="${tag.elementId}"
344
+ tabindex="0"
345
+ >
346
+ <div class="nav-bar-tag-title">${tag.displayName || tag.name}</div>
347
+ </div>
348
+ </div>`;
349
+ }
341
350
  const visiblePaths = (tag.paths || []).filter((p) =>
342
351
  isNavPathVisible(p, this.searchVal, tag.name),
343
352
  );
@@ -18,6 +18,7 @@ import {
18
18
  EMPTY_ARRAY,
19
19
  getMergedProperties,
20
20
  getMergedRequired,
21
+ exampleLabel,
21
22
  } from "~/utils/schema-utils";
22
23
  import "~/components/schema-tree";
23
24
  import "~/components/schema-table";
@@ -28,8 +29,14 @@ import {
28
29
  examplesBesideConstraints,
29
30
  renderExample,
30
31
  } from "~/templates/request-params-template";
32
+ import { multipartPartContentType } from "~/utils/request-utils";
31
33
 
32
- export function formDataParamAsObjectTemplate(fieldName, fieldSchema, mimeType) {
34
+ export function formDataParamAsObjectTemplate(
35
+ fieldName,
36
+ fieldSchema,
37
+ mimeType,
38
+ partContentType = "",
39
+ ) {
33
40
  // This template is used when form-data param should be send as a object (application/json, application/xml)
34
41
  const formdataPartSchema = schemaToAST(fieldSchema);
35
42
  const formdataPartExample = generateExample(
@@ -95,6 +102,7 @@ export function formDataParamAsObjectTemplate(fieldName, fieldSchema, mimeType)
95
102
  data-array="false"
96
103
  data-ptype="${mimeType.includes("form-urlencode") ? "form-urlencode" : "form-data"}"
97
104
  data-pname="${fieldName}"
105
+ data-content-type="${partContentType}"
98
106
  data-example="${formdataPartExample[0]?.exampleValue || ""}"
99
107
  .value="${live(this.fillRequestFieldsWithExample === "true" ? formdataPartExample[0]?.exampleValue || "" : "")}"
100
108
  spellcheck="false"
@@ -133,7 +141,7 @@ export function formDataParamAsObjectTemplate(fieldName, fieldSchema, mimeType)
133
141
  `;
134
142
  }
135
143
 
136
- export function formDataTemplate(schema, mimeType, exampleValue = "") {
144
+ export function formDataTemplate(schema, mimeType, exampleValue = "", encoding = {}) {
137
145
  const formFieldItems = [];
138
146
  const properties = getMergedProperties(schema);
139
147
  const required = getMergedRequired(schema);
@@ -145,6 +153,10 @@ export function formDataTemplate(schema, mimeType, exampleValue = "") {
145
153
  }
146
154
  const fieldExamples = fieldSchema.examples || fieldSchema.example || "";
147
155
  const fieldType = fieldSchema.type;
156
+ // multipart parts are sent with this Content-Type (application/json for objects); empty means text/plain
157
+ const partContentType = mimeType.includes("form-data")
158
+ ? multipartPartContentType(fieldSchema, encoding?.[fieldName])
159
+ : "";
148
160
  const paramSchema = getTypeInfo(fieldSchema);
149
161
  const example = normalizeExamples(
150
162
  paramSchema.examples || paramSchema.example,
@@ -169,7 +181,7 @@ export function formDataTemplate(schema, mimeType, exampleValue = "") {
169
181
  >
170
182
  <div class="param-label" part="param-label">
171
183
  <div class="param-name ${fieldSchema.deprecated ? "deprecated" : ""}" part="param-name">
172
- ${fieldName}${required.includes(fieldName) || fieldSchema.required ? html`<span class="param-required" title="Required">*</span>` : ""}
184
+ ${fieldName}${required.includes(fieldName) ? html`<span class="param-required" title="Required">*</span>` : ""}
173
185
  </div>
174
186
  <div class="param-type" part="param-type">
175
187
  ${
@@ -227,6 +239,7 @@ export function formDataTemplate(schema, mimeType, exampleValue = "") {
227
239
  style="width:100%"
228
240
  data-ptype="${mimeType.includes("form-urlencode") ? "form-urlencode" : "form-data"}"
229
241
  data-pname="${fieldName}"
242
+ data-content-type="${partContentType}"
230
243
  data-example="${Array.isArray(fieldExamples) ? fieldExamples.join("~|~") : fieldExamples}"
231
244
  data-array="true"
232
245
  placeholder="add-multiple &#x21a9;"
@@ -247,6 +260,7 @@ export function formDataTemplate(schema, mimeType, exampleValue = "") {
247
260
  fieldName,
248
261
  fieldSchema,
249
262
  mimeType,
263
+ partContentType,
250
264
  )
251
265
  : html`
252
266
  ${
@@ -259,6 +273,7 @@ export function formDataTemplate(schema, mimeType, exampleValue = "") {
259
273
  style="width:100%"
260
274
  data-ptype="${mimeType.includes("form-urlencode") ? "form-urlencode" : "form-data"}"
261
275
  data-pname="${fieldName}"
276
+ data-content-type="${partContentType}"
262
277
  data-example="${Array.isArray(fieldExamples) ? fieldExamples[0] : fieldExamples}"
263
278
  data-array="false"
264
279
  />`
@@ -379,6 +394,7 @@ export default function requestBodyTemplate() {
379
394
  schema: content[mimeType].schema,
380
395
  example: content[mimeType].example,
381
396
  examples: content[mimeType].examples,
397
+ encoding: content[mimeType].encoding,
382
398
  });
383
399
  // Fallback guard: If selectedRequestBodyType was cleared (e.g., after focused-mode navigation),
384
400
  // automatically latch onto the first available MIME type (e.g., application/json).
@@ -436,11 +452,16 @@ export default function requestBodyTemplate() {
436
452
  reqBodyExamples.length === 1
437
453
  ? ""
438
454
  : html`
439
- <select class="select-sm example-select" aria-label="request body examples" @change="${(e) => this.onSelectExample(e)}">
455
+ <select
456
+ class="select-sm example-select"
457
+ aria-label="request body examples"
458
+ title="${exampleLabel(reqBodyExamples.find((v) => v.exampleId === this.selectedRequestBodyExample))}"
459
+ @change="${(e) => this.onSelectExample(e)}"
460
+ >
440
461
  ${reqBodyExamples.map(
441
462
  (v) =>
442
- html`<option value="${v.exampleId}" ?selected=${v.exampleId === this.selectedRequestBodyExample}>
443
- ${v.exampleSummary.length > 80 ? v.exampleId : v.exampleSummary ? v.exampleSummary : v.exampleId}
463
+ html`<option value="${v.exampleId}" title="${exampleLabel(v)}" ?selected=${v.exampleId === this.selectedRequestBodyExample}>
464
+ ${exampleLabel(v)}
444
465
  </option>`,
445
466
  )}
446
467
  </select>
@@ -454,7 +475,7 @@ export default function requestBodyTemplate() {
454
475
  class="example ${v.exampleId === this.selectedRequestBodyExample ? "example-selected" : ""}"
455
476
  data-example="${v.exampleId}"
456
477
  >
457
- ${v.exampleSummary && v.exampleSummary.length > 80 ? html`<div style="padding: 4px 0">${v.exampleSummary}</div>` : ""}
478
+ ${reqBodyExamples.length === 1 && v.exampleSummary && v.exampleSummary.length > 80 ? html`<div style="padding: 4px 0">${v.exampleSummary}</div>` : ""}
458
479
  ${
459
480
  v.exampleDescription
460
481
  ? html`<div class="m-markdown-small" style="padding: 4px 0">
@@ -524,6 +545,7 @@ ${v.exampleFormat === "text" ? v.exampleValue : JSON.stringify(v.exampleValue, n
524
545
  reqBody.schema,
525
546
  reqBody.mimeType,
526
547
  ex[0] ? ex[0].exampleValue : "",
548
+ reqBody.encoding,
527
549
  );
528
550
  }
529
551
  }
@@ -148,6 +148,12 @@ export function exampleListTemplate(paramName, paramType, exampleList = []) {
148
148
  }`;
149
149
  }
150
150
 
151
+ // A singular `example` is the value itself, even an object; only an `examples` map wraps its values in { value }
152
+ const singleExample = (ex) =>
153
+ ex !== null && typeof ex === "object" && !Array.isArray(ex)
154
+ ? { Example: { value: ex } }
155
+ : standardizeExample(ex);
156
+
151
157
  export function inputParametersTemplate(paramType) {
152
158
  const filteredParams = this.parameters
153
159
  ? this.parameters.filter((param) => param.in === paramType)
@@ -196,8 +202,8 @@ export function inputParametersTemplate(paramType) {
196
202
  // openapi 3.1.0 spec based examples (which must be Object(string : { value:any, summary?: string, description?: string})
197
203
  const example = normalizeExamples(
198
204
  standardizeExample(param.examples) ||
199
- standardizeExample(param.example) ||
200
- standardizeExample(mimeTypeElem?.example) ||
205
+ singleExample(param.example) ||
206
+ singleExample(mimeTypeElem?.example) ||
201
207
  standardizeExample(mimeTypeElem?.examples) ||
202
208
  standardizeExample(paramSchema.examples) ||
203
209
  standardizeExample(paramSchema.example),
@@ -263,6 +269,7 @@ export function inputParametersTemplate(paramType) {
263
269
  style="width:100%"
264
270
  data-ptype="${paramType}"
265
271
  data-pname="${param.name}"
272
+ data-item-type="${declaredParamSchema.items?.type || ""}"
266
273
  data-example="${Array.isArray(example.exampleVal) ? example.exampleVal.join("~|~") : example.exampleVal}"
267
274
  data-param-serialize-style="${paramStyle}"
268
275
  data-param-serialize-explode="${paramExplode}"
@@ -337,7 +344,11 @@ export function inputParametersTemplate(paramType) {
337
344
  part="textarea textarea-param"
338
345
  data-ptype="${paramType}-object"
339
346
  data-pname="${param.name}"
340
- data-example="${example.exampleVal}"
347
+ data-example="${
348
+ typeof example.exampleVal === "object"
349
+ ? JSON.stringify(example.exampleVal, null, 2)
350
+ : example.exampleVal
351
+ }"
341
352
  data-param-serialize-style="${paramStyle}"
342
353
  data-param-serialize-explode="${paramExplode}"
343
354
  data-param-allow-reserved="${paramAllowReserved}"
@@ -380,7 +391,7 @@ export function inputParametersTemplate(paramType) {
380
391
  allow-schema-description-expand-toggle="${this.allowSchemaDescriptionExpandToggle}"
381
392
  schema-hide-read-only="${this.schemaHideReadOnly.includes(this.method)}"
382
393
  schema-hide-write-only="${this.schemaHideWriteOnly.includes(this.method)}"
383
- 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,
394
+ 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-fill-example:btn-fill-example, btn-clear-resp:btn-clear-resp,
384
395
  file-input:file-input, textbox:textbox, textbox-param:textbox-param, textarea:textarea, textarea-param:textarea-param,
385
396
  anchor:anchor, anchor-param-example:anchor-param-example"
386
397
  >
@@ -119,6 +119,9 @@ export function onClearAllApiKeys() {
119
119
  v.finalKeyValue = "";
120
120
  v.hostNote = "";
121
121
  });
122
+ if (this.persistAuth === "true") {
123
+ setPersistedApiKeys.call(this, {});
124
+ }
122
125
  this.requestUpdate();
123
126
  }
124
127
 
@@ -133,7 +136,7 @@ function setPersistedApiKeys(obj) {
133
136
  export function recoverPersistedApiKeys() {
134
137
  const rapidocLs = getPersistedApiKeys.call(this);
135
138
  Object.values(rapidocLs).forEach((p) => {
136
- applyApiKey.call(this, p.securitySchemeId, p.username, p.password, p.value);
139
+ applyApiKey.call(this, p.securitySchemeId, "", "", p.value);
137
140
  });
138
141
  }
139
142
 
@@ -159,8 +162,14 @@ function onApiKeyChange(securitySchemeId) {
159
162
  applyApiKey.call(this, securitySchemeId, "", "", apiKeyValue);
160
163
  }
161
164
  if (this.persistAuth === "true") {
165
+ // Only what recoverPersistedApiKeys restores: the apiKey / bearer value. Basic credentials
166
+ // (and the derived Authorization header) are never written to localStorage.
162
167
  const rapidocLs = getPersistedApiKeys.call(this);
163
- rapidocLs[securitySchemeId] = securityObj;
168
+ if (securityObj.value) {
169
+ rapidocLs[securitySchemeId] = { securitySchemeId, value: securityObj.value };
170
+ } else {
171
+ delete rapidocLs[securitySchemeId];
172
+ }
164
173
  setPersistedApiKeys.call(this, rapidocLs);
165
174
  }
166
175
  }
@@ -26,7 +26,8 @@ export function setApiServer(serverUrl) {
26
26
  }
27
27
 
28
28
  function onApiServerVarChange(e, serverObj) {
29
- const inputEls = [...e.currentTarget.closest("table").querySelectorAll("input, select")];
29
+ // The variables render as a list, <ul class="server-vars">
30
+ const inputEls = [...e.currentTarget.closest(".server-vars").querySelectorAll("input, select")];
30
31
  let tempUrl = serverObj.url;
31
32
  inputEls.forEach((v) => {
32
33
  const regex = new RegExp(`{${v.dataset.var}}`, "g");
@@ -134,7 +135,7 @@ export default function serverTemplate() {
134
135
  .checked="${this.selectedServer.url === server.url}"
135
136
  />
136
137
  <label for="srvr-opt-${i}">
137
- ${server.url} ${server.description ? html`- <span class="regular-font">${server.description} </span>` : ""}
138
+ ${server.url} ${server.description ? html`- <span class="regular-font">${unsafeHTML(sanitizeHTML(marked.parseInline(server.description)))} </span>` : ""}
138
139
  </label>
139
140
  </div>
140
141
  `,
@@ -0,0 +1,146 @@
1
+ # RapiDoc Utilities
2
+
3
+ ## Overview
4
+
5
+ Framework-free helpers behind the `<rapi-doc>` component (`src/rapidoc.ts`), its templates and
6
+ the static renderer (`@rapidoc/prerender`, `packages/rapidoc-prerender`). `utils/` owns the spec pipeline (load,
7
+ dereference, group, anchor), schema rendering and example generation, request/response/mock
8
+ helpers, and UI helpers (highlighting, Markdown, sanitizing, navbar binding, color, OAuth). It
9
+ does not own rendering (`templates/`, `components/`) or styles. The MCP manifest, runtime and
10
+ project writer are documented in [`./mcp/README.md`](./mcp/README.md); Arazzo parsing and workflow diagrams in
11
+ [`./arazzo/README.md`](./arazzo/README.md).
12
+
13
+ Everything except `arazzo/` is bundled into `dist/rapidoc-min.js`; `arazzo/` goes into
14
+ `dist/rapidoc-flow-min.js` and `@rapidoc/prerender`. `@rapidoc/portal` depends on this folder
15
+ indirectly through `@rapidoc/prerender` (`methodPathSlug`, `operationAnchorSource`, `toKebabCase`)
16
+ and the anchors `<rapi-doc>` emits, so anchor generation is public behavior.
17
+ Several modules run in Node (static renderer, unit tests) and must guard `window`/`document`.
18
+
19
+ ## Interface
20
+
21
+ Spec pipeline:
22
+
23
+ - `spec-parser.js`: default `ProcessSpec` (spec URL, text or object to the render model of
24
+ tags, tag groups, components, security schemes, servers); `isTagExpanded`, `groupByTagGroups`.
25
+ - `slugger.js`: `OperationSlugger` (`slug`, `slugOp`, `slugTag`, `slugTagGroup`, `reset`),
26
+ `operationAnchorSource`, `methodPathSlug`, `toKebabCase`, `clampWordBoundary`, `createSlugger`.
27
+ - `common-utils.js`: search (`advancedSearch`, `getMatchedPaths`), `debounce`, `sleep`,
28
+ clipboard and download helpers, `rapidocApiKey`.
29
+
30
+ Schema rendering:
31
+
32
+ - `schema-utils.js`: `getTypeInfo`, `schemaToAST` (schema tree for renderers),
33
+ `schemaInObjectNotation` (older tree format), `schemaToSampleObj` and `generateExample`
34
+ (examples), `json2xml`, `getMergedProperties`/`getMergedRequired`.
35
+
36
+ Request, response, mock:
37
+
38
+ - `url-utils.js`: path/query serialization, `constructFetchURL`, `oauthReceiverUrl`.
39
+ - `request-utils.js`: headers, url-encoded and multipart bodies.
40
+ - `response-utils.js`: `processFetchResponse` (decode a `Response`),
41
+ `prepareResponseDefinitions`.
42
+ - `curl-utils.js`: `generateCurlCommand`.
43
+ - `mock-interceptor.js`: `enableMockServer`, `disableMockServer`, `updateMockConfig`,
44
+ `isMockServerActive`, `compileRoutes` (from a `ProcessSpec` result).
45
+
46
+ UI helpers:
47
+
48
+ - `highlighter.js`: `highlightAllCode`, `scheduleHighlight` (CSS Custom Highlight API).
49
+ - `markdown-utils.js`: `configureMarkdown` and token helpers for the global `marked`.
50
+ - `sanitize.js`: `sanitizeHTML`.
51
+ - `bind-navbar.js`: `bindNavBar` ties a static `getNavBarHtml` sidebar to `<rapi-doc>`.
52
+ - `auto-color-scheme.js`: `AutoColorSchemeController`, `resolvePageColorScheme`.
53
+ - `oauth-utils.js`: PKCE (`randomUrlSafe`, `pkceChallenge`), `readOAuthReply`,
54
+ `isExpectedOAuthReply`.
55
+ - `color-utils.js`, `xml-utils.js` (`formatXml`): pure helpers.
56
+ - `mcp-generator.js`: `generateMcpServer(spec, options) -> { manifest, files, budget, hostConfig }`,
57
+ re-exporting `./mcp` (`buildMcpManifest`, `createMcpRuntime`, `writeMcpProject`, `createZip`,
58
+ `parseMcpEnabled`, ...).
59
+
60
+ ## Invariants
61
+
62
+ - **`ProcessSpec` never rejects** (`[spec-parser.js:ProcessSpec]`). Loading and post-load
63
+ processing are each wrapped in `try/catch`. Every failure resolves to
64
+ `{ specLoadError: true, isSpecLoading: false, info: { title, description, version }, tags: [],
65
+ tagGroups: [] }`, where `description` is `"<url> ┃ Not Found (404)"`,
66
+ `"Unable to load <url> ┃ <status|message>"`, `"Invalid Spec Definition"`, or
67
+ `"Unable to process the spec ┃ <message>"` for failures after loading. Callers check
68
+ `specLoadError`, not exceptions. It uses an optional `this` (`requestUpdate`,
69
+ `before-render` event), so the component calls it with `.call(element, ...)`.
70
+ - **Webhooks** (`[spec-parser.js:groupByTags]`) are grouped after paths and flagged
71
+ `isWebhook`; they are never written into the spec's `paths`, so a webhook named like a path
72
+ does not replace it.
73
+ - **Stable anchors** (`[slugger.js:operationAnchorSource]`, `[slugger.js:OperationSlugger.slug]`):
74
+ - Source priority: `operationId` (trimmed, 1-45 chars, not a placeholder like `operation12`),
75
+ then `summary` (the parser passes `summary || description`; clamped to 40 chars at a word
76
+ boundary), then `methodPathSlug(method, path)`.
77
+ - Memoized per method + path (and tag slug), so one operation keeps one `elementId` per run.
78
+ - Tag prefix `<tagSlug>--` is applied only when the spec has non-empty `x-tagGroups` or
79
+ `x-tag-groups` (`[spec-parser.js:ProcessSpec]`), and never for the tag `general`.
80
+ - Collisions try `<prefix><method>-<slug>` first, then `github-slugger`'s `-1`, `-2` suffixes,
81
+ which depend on encounter order (paths, then webhooks; methods in `supportedMethods` order).
82
+ - Tag and tag-group ids (`tag--<kebab>`, `tag-group--<kebab>`) are not deduplicated.
83
+ - The portal links to these ids and checks them with `operationAnchorSource`/`methodPathSlug`;
84
+ changing any rule above is a breaking change for published portals.
85
+ - **Circular references** (`[spec-parser.js:breakCircularRefs]`): after dereferencing, the tree
86
+ is cloned without `x-ref` keys and each cycle becomes `{ $ref }` (fallback
87
+ `#/components/schemas/Circular`); shared non-cyclic nodes stay shared. Schema code treats a
88
+ remaining `$ref` as the cycle marker, so renderers must not recurse into it.
89
+ - **Schema depth limits** (`[schema-utils.js]`): `schemaToSampleObj` returns `undefined` and
90
+ `schemaInObjectNotation` returns a `::type` stub when `level > 8`. `schemaToAST` expands to
91
+ `MAX_SCHEMA_DEPTH` (8) and, while the tree exceeds `SCHEMA_NODE_BUDGET` (8000 nodes), retries
92
+ with a smaller depth down to `MIN_SCHEMA_DEPTH` (4); cut containers become nodes with
93
+ `truncated: true`. Its module state makes it synchronous and non-reentrant.
94
+ - **Sanitization** (`[sanitize.js:sanitizeHTML]`): non-strings return `""`. The native
95
+ `Sanitizer` path allows all attributes; the fallback removes only `<script>`, `on*` attributes
96
+ and `javascript:` / `data:text/html` in `href`/`src`. Without `DOMParser` (Node) the input is
97
+ returned unchanged, so server-side output is not sanitized.
98
+ - **Mock server** (`[mock-interceptor.js:enableMockServer]`): one mock per `options.owner` (the
99
+ element; omitted, a page-wide default), so several `<rapi-doc>` / `<rapi-doc-mini>` on a page
100
+ mock their own APIs with their own status codes, delay and cycle positions. Repeated calls for an
101
+ owner refresh its routes and config but never stack wrappers; `disableMockServer(owner)` drops
102
+ that mock and restores the originals once none is left. A request is answered by the most
103
+ specific match across the mocks (server origin and base path, then base path, then bare path;
104
+ ties to the mock enabled first); unmatched requests fall through to the real `fetch`/XHR. Mock
105
+ responses carry `X-Mock-Server: RapiDoc`.
106
+ - **Highlighter** (`[highlighter.js]`): `highlightAllCode` and `scheduleHighlight` are no-ops
107
+ without `window` and `CSS.highlights`; highlighting never mutates the DOM.
108
+ - **Headless boundary**: `spec-parser.js`, `schema-utils.js`, `slugger.js`, `mcp-generator.js`,
109
+ `mcp/`, `arazzo/` and everything they import are shared with the Node builds (`@rapidoc/prerender`,
110
+ `@rapidoc/mcp`). They never import Lit, elements, `components/`, `templates/`, `styles/` or
111
+ `static/`, and use only the third-party packages allowed in `tests/unit/headless-boundary.test.js`.
112
+ DOM calls inside functions are allowed (Node never calls them); DOM access at import time is not.
113
+
114
+ ## Side Effects
115
+
116
+ - `ProcessSpec` fetches the spec and external `$ref`s, logs load errors with `console.info`, and
117
+ mutates the spec it processes: `x-ref` during dereference, `paths` replaced by `filterPaths`,
118
+ `callbacks` filtered, relative `servers[*].url` prefixed with `location.origin`,
119
+ `computedUrl` added.
120
+ - `mock-interceptor.js` replaces `window.fetch` and `XMLHttpRequest.prototype` methods while
121
+ active and keeps routes and config in module state.
122
+ - `markdown-utils.js` configures the global `marked` at import time.
123
+ - `highlighter.js` writes the global `CSS.highlights` registry and schedules
124
+ `requestAnimationFrame`.
125
+ - `bind-navbar.js` adds DOM and `window` listeners, writes `history`; some timers and the
126
+ `spec-loaded` listener outlive `unbind()`.
127
+ - `auto-color-scheme.js` observes `<html>`/`<body>` attributes and `prefers-color-scheme`.
128
+ - `response-utils.js` creates object URLs it does not revoke; `common-utils.js` clipboard and
129
+ download helpers create temporary elements.
130
+
131
+ ## Verification
132
+
133
+ Run `npm run test:unit` from the repo root before and after changing `spec-parser.js` or
134
+ `schema-utils.js`, then `npm run lint` and `npm run format-fix`. Files in `tests/unit/`:
135
+
136
+ - `spec-parser.js`: `spec-parser-relative-url`, `spec-parser-security`,
137
+ `spec-parser-tag-groups`, `spec-parser-webhooks`, `unused-tags`, `openapi-3-2-bookstore`.
138
+ - `slugger.js`: `slugger`.
139
+ - `schema-utils.js`: `schema-utils`, `schema-ast`, `schema-in-object-notation`,
140
+ `schema-nullable`, `schema-modernization`, `multipart-form-curl`.
141
+ - `url-utils.js`: `query-param-serialization`, `oauth-receiver-url`.
142
+ - `request-utils.js`, `curl-utils.js`: `request-utils`, `multipart-part-types`, `curl-command`.
143
+ - `response-utils.js`: `api-request-response`.
144
+ - `mock-interceptor.js`, `oauth-utils.js`, `markdown-utils.js`, `mcp-generator.js`: the
145
+ test file of the same name.
146
+ - Headless boundary (imports of the modules above, transitively): `headless-boundary`.
@@ -0,0 +1,119 @@
1
+ # Arazzo Workflow Diagrams
2
+
3
+ ## Overview
4
+
5
+ Reads Arazzo 1.x documents and draws one workflow as an SVG string: the workflow's inputs and
6
+ outputs, its steps in order, the values passed between steps, and each step's success and failure
7
+ actions. The inputs box (each input's type, required flag, default and enum values; a string enum's type
8
+ reads `enum`, as `<rapi-doc>` labels it) and the
9
+ outputs table are the same in both views, which differ in the step boxes: `expanded` (the
10
+ default) lists each step's values with a keyword for where they come from (`input`, `step`,
11
+ `fixed`, `expr`), its checks (the success criteria, unnumbered), its actions as "condition →
12
+ what happens" under "On success · first match runs" / "On failure · first match runs", and the
13
+ outputs something reads; `compact` shows each step's name, method, path and number of checks,
14
+ with labelled value lines and no actions. Used by `<rapi-flow>` (`src/rapi-flow.ts`, bundled as `dist/rapidoc-flow-min.js`) and by
15
+ the build-time renderer (`@rapidoc/prerender`: `renderArazzoWorkflow`, `getFlowCss`).
16
+
17
+ Not owned here: running workflows (nothing here makes a request), the element's loading and
18
+ events (`src/rapi-flow.ts`), and the diagram's look (`src/styles/flow-styles.js`). `dist/rapidoc-min.js`
19
+ imports only the YAML-free modules `components.js`, `operations.js` and `refs.js`, through
20
+ `utils/mcp/workflows.js`, which compiles workflows into the MCP manifest; `parse.js` (the `yaml`
21
+ package) stays out of it.
22
+
23
+ ## Interface
24
+
25
+ - `parseDocument(input)` [parse.js]: JSON or YAML text to an object; an object passes through.
26
+ Throws on empty input or a non-object.
27
+ - `validateArazzo(doc) -> { level, path, message }[]` [parse.js]: structural checks the diagram
28
+ relies on; an empty list means valid.
29
+ - `resolveComponentRef(doc, obj, kind)` [components.js, re-exported by parse.js]: `{ reference: "$components.<kind>.<name>" }`
30
+ to the component, or null.
31
+ - `collectRefs(value)` [refs.js]: `$inputs`, `$steps` and `$workflows` expressions anywhere in a
32
+ JSON value, distinct, in first-seen order.
33
+ - `indexOperations(openapi)`, `indexResolvedSpec(resolvedSpec)` (the same index from
34
+ `<rapi-doc>`'s processed `tags[].paths[]`, webhooks skipped, the first entry kept for an
35
+ operation with several tags), `resolveOperation(step, sources)` [operations.js]: operation lookup
36
+ by operationId (plain or `$sourceDescriptions.<name>.` qualified) and by `operationPath`.
37
+ - `buildFlowGraph(doc, workflowId?, { sources })` [graph.js]: the graph the SVG draws; null when
38
+ the workflow does not exist.
39
+ - `renderFlowDiagram(graph, { view, idPrefix, interactive }) -> { svg, legend }` [svg.js]:
40
+ `legend` names the kinds of lines drawn (`seq`, `data`, `output`, `success`, `failure`);
41
+ `renderFlowSvg` returns only the SVG. `actionLabel` (short: "✗ retry 3× after 2s (if)"),
42
+ `actionSentence` (with criteria), `describeAction(action, checks, index)` (`{ when, does, name, tag }`, the
43
+ wording of the expanded boxes; an action without criteria reads "checks
44
+ fail" / "check fails" / "request fails" by the step's number of checks, or "otherwise" after
45
+ other actions) and `escapeXml` are exported too.
46
+ - `renderArazzoWorkflow(arazzo, options)` [index.js]: parse, validate, index `options.sources`,
47
+ build and draw in one call; returns `{ svg, legend, graph, problems, workflowIds }`.
48
+
49
+ ## Invariants
50
+
51
+ - **No DOM, no network.** Text widths are estimated from character counts, so output is the same
52
+ in Node and the browser and long labels are cut with `…` rather than measured.
53
+ - **OpenAPI is not dereferenced.** Only `paths.<path>.<method>.operationId` and `summary` are read,
54
+ so an operation reached through a path-item `$ref` is not found.
55
+ - **Validation is structural.** It checks required fields, duplicate ids, exactly one of
56
+ `operationId` / `operationPath` / `workflowId` per step, action types (retry only on failure),
57
+ goto and `dependsOn` targets, and `$steps.<id>` references. It does not check expression syntax,
58
+ `inputs` JSON Schema, or criteria.
59
+ - **Edges.** A data edge goes from a step to a later step (or to the outputs box) whenever a
60
+ `$steps.<id>.outputs.<name>` expression appears in the later step's `parameters` or
61
+ `requestBody` (or a workflow output). References to the same or an earlier step are not drawn.
62
+ Edges into the outputs box are drawn gray, the others in the primary color. Compact: edges from
63
+ one step with the same labels share a lane and arrive at the middle of the target step; edges
64
+ into the outputs box carry no label and arrive at the step's group, as in expanded. Expanded: one lane per source step, starting at each of its Outputs rows
65
+ that is read and branching to each line (a step binding, or the step's group heading in the
66
+ outputs box) that reads one; no labels, since the lines name the value.
67
+ - **Step outputs.** Each step's `usedOutputs` are its outputs that a later step or the workflow
68
+ reads. Expanded boxes end with an Outputs section listing them, the source shortened
69
+ (`$response.body#/status` reads `response body /status`, the full expression a tooltip), and
70
+ "+N unused" for the rest.
71
+ - **Outputs box.** A table in both views: the output names in one flat column (the workflow's
72
+ result is a flat object), and on the right, under "From step", the step each group comes from,
73
+ once per group, with a rule between groups. Groups follow the order each step first appears;
74
+ outputs not taken from a step come last, their source kind (`input`, `fixed`, `expr`) in the
75
+ step column. A step output whose name differs from the workflow output's follows as
76
+ `← <name>`. A step's line arrives at its group's first row, beside the step name.
77
+ - **Step names** (box titles, the step in `step` value lines, the From step column) share one
78
+ color, `--primary` mixed toward the text color so it passes 4.5:1 on light cards; variable
79
+ names keep the text color.
80
+ - **Linking.** Step boxes carry `data-step-id`; output rows and value lines carry
81
+ `data-from-step`, so a caller can highlight a step with everything it feeds (`<rapi-flow>` does
82
+ on hover, with the `rf-linked` class). With `interactive`, those rows are also `rf-jump` links
83
+ (the outputs box's step names focusable, `role="link"`) for a caller to scroll to the step.
84
+ - **Actions.** A step's actions are its own `onSuccess` / `onFailure`, then the workflow's
85
+ `successActions` / `failureActions` (marked `inherited`) minus any the step overrides by `name`.
86
+ The graph also lists the workflow's actions once as `workflowActions`. Compact shows no
87
+ actions; expanded lists a step's own actions and the inherited ones in one list per outcome, in merge order, each with its name in parentheses after
88
+ what it does; inherited ones also carry a `workflow` tag on the right. A runner takes the first action whose criteria match, so every action after
89
+ one without criteria is marked `shadowed` (it never runs). Shadowed actions are left out of the
90
+ boxes and draw no goto line; the graph's `warnings` (`{ level, path,
91
+ message }`, one per step and outcome) name them, for author tooling;
92
+ `<rapi-flow>` does not show them. Only `goto` with a `stepId` in the same workflow draws a line, from its action line
93
+ (expanded) or the box's left edge (compact); `end`, `retry` and goto to a workflow draw none.
94
+ - **Details.** The graph carries more than the boxes show, for callers that render their own
95
+ documentation: `inputDetails` (type,
96
+ format, required, default, enum, description, from `inputs` or a `$ref` to
97
+ `#/components/inputs/<name>`), each step's `parameters` (component references resolved, their
98
+ `value` overriding), `requestBody`, `criteria` and `outputExprs`, and each output's `expr`.
99
+ Each step's `bindings` are the values it sends: parameters by name and request body fields as
100
+ `body.<path>` (objects flattened, arrays kept whole), each with a `source` (`input`, `step`,
101
+ `expression` or `value`) and the steps it reads (`fromSteps`). Expanded boxes show at most ten.
102
+ Input descriptions and step descriptions are tooltips (`<title>`) on their lines and boxes.
103
+ - **Next-step arrows.** A step falls through to the next one unless it has a success action of type
104
+ `end` or `goto` without `criteria`; then no arrow is drawn into the next step, which is reached
105
+ only through goto lines. The last step always gets an arrow into the outputs box.
106
+ - **Ids.** Marker ids are `<idPrefix>-<name>`; pages with several diagrams pass distinct prefixes.
107
+ Steps carry `data-step-id`; with `interactive`, steps whose operation or workflow is known get
108
+ `tabindex="0" role="button"` and the `rf-clickable` class.
109
+
110
+ ## Side Effects
111
+
112
+ - None. `parseDocument` uses the `yaml` package, which is why `dist/rapidoc-flow-min.js` carries it.
113
+
114
+ ## Verification
115
+
116
+ - `npm run test:unit` from the repo root; `tests/unit/arazzo-flow.test.js` covers parsing,
117
+ validation, lookup, the graph and the SVG, using `docs/public/specs/petstore.arazzo.yaml`.
118
+ - The docs page `/examples/arazzo-flow` shows `<rapi-flow>` tied to a `<rapi-doc>`.
119
+ - After editing, `npm run lint` and `npm run format-fix`.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Arazzo reusable components. Kept apart from parse.js so the MCP manifest builder can compile
3
+ * workflows inside dist/rapidoc-min.js without pulling in the YAML parser.
4
+ */
5
+
6
+ /**
7
+ * Resolves `{ reference: "$components.<kind>.<name>" }` against the document's components.
8
+ */
9
+ export function resolveComponentRef(doc, obj, kind) {
10
+ if (!obj || typeof obj.reference !== "string") return obj;
11
+ const m = obj.reference.match(/^\$components\.([A-Za-z]+)\.([A-Za-z0-9_.-]+)$/);
12
+ if (!m || m[1] !== kind) return null;
13
+ const target = doc?.components?.[kind]?.[m[2]];
14
+ return target ? { ...target, ...(obj.value !== undefined ? { value: obj.value } : {}) } : null;
15
+ }