rapidoc 11.0.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +46 -31
  2. package/dist/rapidoc-min.js +1424 -213
  3. package/dist/rapidoc-static-min.js +13542 -0
  4. package/package.json +48 -34
  5. package/src/components/api-request.ts +869 -0
  6. package/src/components/api-response.ts +509 -0
  7. package/src/components/json-tree.ts +171 -0
  8. package/src/components/schema-base-element.ts +315 -0
  9. package/src/components/schema-table.ts +430 -0
  10. package/src/components/schema-tree.ts +386 -0
  11. package/src/components/tag-input.ts +184 -0
  12. package/src/index.js +6 -0
  13. package/src/oauth-receiver.ts +29 -0
  14. package/src/rapidoc-mini.ts +582 -0
  15. package/src/rapidoc.ts +1808 -0
  16. package/src/static/index.d.ts +149 -0
  17. package/src/static/index.js +62 -0
  18. package/src/static/navigation.js +497 -0
  19. package/src/static/render.js +490 -0
  20. package/src/static/serializer.js +216 -0
  21. package/src/styles/auth-styles.js +318 -0
  22. package/src/styles/border-styles.js +24 -0
  23. package/src/styles/css-bundle.js +129 -0
  24. package/src/styles/custom-styles.js +8 -0
  25. package/src/styles/dialog-box-styles.js +102 -0
  26. package/src/styles/endpoint-styles.js +559 -0
  27. package/src/styles/flex-styles.js +16 -0
  28. package/src/styles/font-styles.js +432 -0
  29. package/src/styles/info-styles.js +36 -0
  30. package/src/styles/input-styles.js +555 -0
  31. package/src/styles/layout-styles.js +396 -0
  32. package/src/styles/microlighter-styles.js +99 -0
  33. package/src/styles/nav-styles.js +609 -0
  34. package/src/styles/param-list-styles.js +261 -0
  35. package/src/styles/schema-styles.js +288 -0
  36. package/src/styles/tab-styles.js +106 -0
  37. package/src/styles/theme-tokens.js +582 -0
  38. package/src/templates/advanced-search-dialog-template.js +94 -0
  39. package/src/templates/api-response-template.js +219 -0
  40. package/src/templates/callback-template.js +86 -0
  41. package/src/templates/code-samples-template.js +52 -0
  42. package/src/templates/components-template.js +86 -0
  43. package/src/templates/downloads-template.js +85 -0
  44. package/src/templates/endpoint-template.js +386 -0
  45. package/src/templates/expanded-endpoint-template.js +266 -0
  46. package/src/templates/focused-endpoint-template.js +145 -0
  47. package/src/templates/header-template.js +92 -0
  48. package/src/templates/logo-template.js +21 -0
  49. package/src/templates/main-body-template.js +102 -0
  50. package/src/templates/navbar-template.js +489 -0
  51. package/src/templates/overview-template.js +104 -0
  52. package/src/templates/request-body-template.js +651 -0
  53. package/src/templates/request-params-template.js +474 -0
  54. package/src/templates/security-scheme-template.js +1263 -0
  55. package/src/templates/server-template.js +152 -0
  56. package/src/utils/auto-color-scheme.js +99 -0
  57. package/src/utils/bind-navbar.js +175 -0
  58. package/src/utils/color-utils.js +59 -0
  59. package/src/utils/common-utils.js +191 -0
  60. package/src/utils/curl-utils.js +123 -0
  61. package/src/utils/highlighter.js +416 -0
  62. package/src/utils/markdown-utils.js +110 -0
  63. package/src/utils/mcp/common.js +467 -0
  64. package/src/utils/mcp/generators/csharp.js +242 -0
  65. package/src/utils/mcp/generators/go.js +298 -0
  66. package/src/utils/mcp/generators/java.js +330 -0
  67. package/src/utils/mcp/generators/python.js +248 -0
  68. package/src/utils/mcp/generators/rust.js +281 -0
  69. package/src/utils/mcp/generators/typescript.js +290 -0
  70. package/src/utils/mcp-generator.js +143 -0
  71. package/src/utils/mock-interceptor.js +508 -0
  72. package/src/utils/oauth-utils.js +87 -0
  73. package/src/utils/request-utils.js +203 -0
  74. package/src/utils/response-utils.js +203 -0
  75. package/src/utils/sanitize.js +57 -0
  76. package/src/utils/schema-utils.js +1927 -0
  77. package/src/utils/slugger.js +189 -0
  78. package/src/utils/spec-parser.js +915 -0
  79. package/src/utils/url-utils.js +211 -0
  80. package/src/utils/xml-utils.js +61 -0
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Direct OpenAPI Navigation HTML Serializer & Metadata Extractor.
3
+ *
4
+ * Directly renders framework-agnostic navigation HTML from OpenAPI specifications,
5
+ * eliminating intermediate AST node abstractions:
6
+ * - `getNavBarHtml(specInput, options)`: Resolves spec and renders navigation HTML directly
7
+ * - `renderNavBarHtml(resolvedSpec, options)`: Pure zero-dependency HTML generator
8
+ * - `getSpecNavigation(specInput, options)`: Extracts semantic navigation metadata (tagGroups & tags)
9
+ *
10
+ * Supports 3-tier navigation (x-tagGroups / x-tag-groups) with fallback to 2-tier tags.
11
+ */
12
+
13
+ import { resolveSpecInput } from "./render.js";
14
+ import { escapeHtml } from "./serializer.js";
15
+ import { OperationSlugger, toKebabCase } from "../utils/slugger.js";
16
+ import { bindNavBar } from "../utils/bind-navbar.js";
17
+ import { isInternalOrHidden } from "../utils/common-utils.js";
18
+
19
+ export { isInternalOrHidden };
20
+
21
+ /** Values of show-method-in-nav-bar (rapidoc.ts accepts the same). */
22
+ const METHOD_STYLES = ["false", "as-plain-text", "as-colored-text", "as-colored-block"];
23
+
24
+ /**
25
+ * Normalizes HTTP method badge string (3-character format matching core RapiDoc).
26
+ * e.g., 'delete' -> 'DEL', 'post' -> 'POS', 'get' -> 'GET', 'put' -> 'PUT'
27
+ *
28
+ * @param {string} method
29
+ * @param {'3-char'|'full'} [format='3-char']
30
+ * @returns {string}
31
+ */
32
+ export function getMethodBadge(method, format = "3-char") {
33
+ if (!method) return "";
34
+ const m = String(method).toLowerCase();
35
+ if (format === "3-char") {
36
+ return m.substring(0, 3).toUpperCase();
37
+ }
38
+ return m.toUpperCase();
39
+ }
40
+
41
+ /**
42
+ * Retrieves visible operations for a tag, supporting both parsed and raw specs.
43
+ *
44
+ * @param {Object} tag
45
+ * @param {Object} spec
46
+ * @param {OperationSlugger} [sharedSlugger]
47
+ */
48
+ function getTagOperations(tag, spec, sharedSlugger) {
49
+ if (Array.isArray(tag.paths) && tag.paths.length > 0) {
50
+ return tag.paths.filter((p) => !isInternalOrHidden(p));
51
+ }
52
+ if (!spec?.paths || typeof spec.paths !== "object") return [];
53
+
54
+ const tagName = (tag.name || "").toLowerCase();
55
+ const operations = [];
56
+ const httpMethods = ["get", "post", "put", "delete", "patch", "options", "head", "trace"];
57
+ const slugger = sharedSlugger || new OperationSlugger();
58
+ const tagSlug = toKebabCase(tag.name);
59
+
60
+ for (const [pathKey, pathObj] of Object.entries(spec.paths)) {
61
+ if (!pathObj || typeof pathObj !== "object") continue;
62
+ for (const method of httpMethods) {
63
+ const op = pathObj[method];
64
+ if (op && typeof op === "object" && !isInternalOrHidden(op)) {
65
+ const opTags = Array.isArray(op.tags) ? op.tags.map((t) => String(t).toLowerCase()) : [];
66
+ if (opTags.includes(tagName)) {
67
+ operations.push({
68
+ method,
69
+ path: pathKey,
70
+ summary: op.summary || "",
71
+ description: op.description || "",
72
+ deprecated: Boolean(op.deprecated),
73
+ operationId: op.operationId || "",
74
+ elementId: slugger.slug(
75
+ {
76
+ method,
77
+ path: pathKey,
78
+ operationId: op.operationId,
79
+ summary: op.summary || op.description,
80
+ },
81
+ tagSlug,
82
+ ),
83
+ });
84
+ }
85
+ }
86
+ }
87
+ }
88
+ return operations;
89
+ }
90
+
91
+ /**
92
+ * Extracts a complete, framework-agnostic navigation hierarchy from an OpenAPI spec.
93
+ *
94
+ * @param {string|Object} specInput - Specification file path, URL, raw string, or parsed spec
95
+ * @param {Object} [options]
96
+ * @returns {Promise<{ title: string, version: string, description: string, tagGroups: Array, tags: Array, remainingTags: Array }>}
97
+ */
98
+ export async function getSpecNavigation(specInput, options = {}) {
99
+ const resolvedSpec = await resolveSpecInput(specInput, options);
100
+ const slugger = new OperationSlugger();
101
+
102
+ const allTags = (resolvedSpec.tags || [])
103
+ .filter((t) => !isInternalOrHidden(t))
104
+ .map((t) => {
105
+ const paths = getTagOperations(t, resolvedSpec, slugger);
106
+ return {
107
+ name: t.name || t.displayName || "",
108
+ displayName: t.displayName || t.name || "",
109
+ description: t.description || "",
110
+ elementId: t.elementId || slugger.slugTag(t.name),
111
+ expanded: t.expanded !== false,
112
+ paths,
113
+ };
114
+ })
115
+ .filter((t) => t.paths.length > 0);
116
+
117
+ const assignedTagNames = new Set();
118
+ const tagGroups = (resolvedSpec.tagGroups || [])
119
+ .filter((group) => !isInternalOrHidden(group))
120
+ .map((group) => {
121
+ const groupTags = (group.tags || [])
122
+ .filter((t) => !isInternalOrHidden(t))
123
+ .map((t) => {
124
+ assignedTagNames.add((t.name || "").toLowerCase());
125
+ const paths = getTagOperations(t, resolvedSpec, slugger);
126
+ return {
127
+ name: t.name || t.displayName || "",
128
+ displayName: t.displayName || t.name || "",
129
+ description: t.description || "",
130
+ elementId: t.elementId || slugger.slugTag(t.name),
131
+ expanded: t.expanded !== false,
132
+ paths,
133
+ };
134
+ })
135
+ .filter((t) => t.paths.length > 0);
136
+
137
+ if (groupTags.length === 0) return null;
138
+ return {
139
+ name: group.name || group.displayName || "",
140
+ displayName: group.displayName || group.name || "",
141
+ description: group.description || "",
142
+ elementId: group.elementId || slugger.slugTagGroup(group.name),
143
+ expanded: group.expanded !== false,
144
+ tags: groupTags,
145
+ };
146
+ })
147
+ .filter(Boolean);
148
+
149
+ const remainingTags =
150
+ tagGroups.length > 0 ? allTags.filter((t) => !assignedTagNames.has(t.name.toLowerCase())) : [];
151
+
152
+ return {
153
+ title: resolvedSpec.info?.title || "",
154
+ version: resolvedSpec.info?.version || "",
155
+ description: resolvedSpec.info?.description || "",
156
+ tagGroups,
157
+ tags: allTags,
158
+ remainingTags,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Renders a single operation item link.
164
+ */
165
+ function renderOperation(op, tagSlug, ctx) {
166
+ if (!op || isInternalOrHidden(op)) return "";
167
+ const method = op.method ? op.method.toLowerCase() : "get";
168
+ const path = op.path || "";
169
+
170
+ // Extract clean operation slug without tag prefix
171
+ const rawElementId = op.elementId || ctx.slugger.slug(op, tagSlug);
172
+ let opSlug;
173
+ let hierarchicalId;
174
+
175
+ if (op.elementId) {
176
+ hierarchicalId = op.elementId;
177
+ if (tagSlug && tagSlug !== "general") {
178
+ const prefix = `${tagSlug}--`;
179
+ opSlug = hierarchicalId.startsWith(prefix)
180
+ ? hierarchicalId.slice(prefix.length)
181
+ : ctx.slugger.slugOp
182
+ ? ctx.slugger.slugOp(op)
183
+ : hierarchicalId;
184
+ } else {
185
+ opSlug = hierarchicalId;
186
+ }
187
+ } else {
188
+ opSlug = ctx.slugger.slugOp
189
+ ? ctx.slugger.slugOp(op)
190
+ : rawElementId.replace(/^[a-z0-9_-]+--/, "");
191
+ hierarchicalId = tagSlug && tagSlug !== "general" ? `${tagSlug}--${opSlug}` : opSlug;
192
+ }
193
+
194
+ let href;
195
+ let contentId;
196
+ if (ctx.routingMode === "tag-pages" && tagSlug && tagSlug !== "general") {
197
+ href = `${ctx.cleanBase}/${tagSlug}#${opSlug}`;
198
+ contentId = opSlug;
199
+ } else {
200
+ href = `${ctx.cleanBase}#${hierarchicalId}`;
201
+ contentId = hierarchicalId;
202
+ }
203
+
204
+ const label = ctx.usePathInNav
205
+ ? `<span class="mono-font">${escapeHtml(path)}</span>`
206
+ : escapeHtml(op.summary || op.shortSummary || `${method.toUpperCase()} ${path}`);
207
+ // As in <rapi-doc>: a three-letter badge for as-colored-block, the full method otherwise, nothing for false.
208
+ const methodStyle = ctx.showMethodInNavBar;
209
+ const methodBadge = getMethodBadge(
210
+ method,
211
+ methodStyle === "as-colored-block" ? ctx.methodBadgeFormat : "full",
212
+ );
213
+ const methodHtml =
214
+ methodStyle === "false"
215
+ ? ""
216
+ : `\n <span class="nav-method ${methodStyle} ${method}">${methodBadge}</span>`;
217
+ const isActive = Boolean(
218
+ ctx.activeHref &&
219
+ (ctx.activeHref === href ||
220
+ ctx.activeHref.endsWith(`#${contentId}`) ||
221
+ (opSlug && ctx.activeHref.endsWith(`#${opSlug}`))),
222
+ );
223
+ const deprecatedClass = op.deprecated ? " deprecated" : "";
224
+ const extraClass = ctx.itemClassName ? ` ${ctx.itemClassName}` : "";
225
+
226
+ return `
227
+ <a
228
+ href="${escapeHtml(href)}"
229
+ class="nav-bar-path${extraClass} ${isActive ? "active" : ""}${deprecatedClass}"
230
+ part="section-navbar-item section-navbar-path"
231
+ data-action="navigate"
232
+ data-content-id="${contentId}"
233
+ data-op-slug="${opSlug}"
234
+ data-path="${escapeHtml(path)}"
235
+ id="link-${contentId}"
236
+ tabindex="0"
237
+ >
238
+ <span class="nav-bar-path-label">${label}</span>${methodHtml}
239
+ </a>`;
240
+ }
241
+
242
+ /**
243
+ * Renders a tag accordion section and its underlying operations.
244
+ */
245
+ function renderTag(tag, ctx) {
246
+ if (!tag || isInternalOrHidden(tag)) return "";
247
+ const visiblePaths = getTagOperations(tag, ctx.resolvedSpec, ctx.slugger);
248
+ if (visiblePaths.length === 0) return "";
249
+ const tagSlug = toKebabCase(tag.name);
250
+ const elementId = tag.elementId || ctx.slugger.slugTag(tag.name);
251
+ const title = tag.displayName || tag.name || "";
252
+ const isExpanded = tag.expanded !== false;
253
+ const pathsHtml = visiblePaths.map((op) => renderOperation(op, tagSlug, ctx)).join("");
254
+ const tagHref =
255
+ ctx.routingMode === "tag-pages" && tagSlug && tagSlug !== "general"
256
+ ? `${ctx.cleanBase}/${tagSlug}`
257
+ : `${ctx.basePath}#${elementId}`;
258
+
259
+ return `
260
+ <div class="nav-bar-tag-and-paths ${isExpanded ? "expanded" : "collapsed"}" id="link-${elementId}">
261
+ <div
262
+ class="nav-bar-tag"
263
+ part="section-navbar-item section-navbar-tag"
264
+ id="link-${elementId}-header"
265
+ data-action="expand-collapse-tag"
266
+ data-tag-href="${escapeHtml(tagHref)}"
267
+ tabindex="0"
268
+ role="button"
269
+ aria-expanded="${isExpanded ? "true" : "false"}"
270
+ >
271
+ <div class="nav-bar-tag-title" data-tag-href="${escapeHtml(tagHref)}">${escapeHtml(title)}</div>
272
+ <div class="nav-bar-tag-icon" tabindex="0" data-action="expand-collapse-tag"></div>
273
+ </div>
274
+ <div class="nav-bar-paths-under-tag">
275
+ ${pathsHtml}
276
+ </div>
277
+ </div>`;
278
+ }
279
+
280
+ /**
281
+ * Renders a 3-tier tag group accordion.
282
+ */
283
+ function renderGroup(group, ctx) {
284
+ if (!group || isInternalOrHidden(group)) return "";
285
+ const groupTagsHtml = (group.tags || [])
286
+ .map((t) => renderTag(t, ctx))
287
+ .filter(Boolean)
288
+ .join("");
289
+ if (!groupTagsHtml) return "";
290
+ const groupId = group.elementId || ctx.slugger.slugTagGroup(group.name);
291
+ const title = group.displayName || group.name || "";
292
+ const isExpanded = group.expanded !== false;
293
+
294
+ return `
295
+ <div class="nav-bar-tag-group ${isExpanded ? "expanded" : "collapsed"}" id="link-${groupId}">
296
+ <div
297
+ class="nav-bar-tag-group-header"
298
+ part="section-navbar-item section-navbar-tag-group"
299
+ data-action="expand-collapse-tag-group"
300
+ tabindex="0"
301
+ role="button"
302
+ aria-expanded="${isExpanded ? "true" : "false"}"
303
+ >
304
+ <div class="nav-bar-tag-group-title">${escapeHtml(title)}</div>
305
+ <div class="nav-bar-tag-group-icon" tabindex="0" data-action="expand-collapse-tag-group"></div>
306
+ </div>
307
+ <div class="nav-bar-tags-under-group">
308
+ ${groupTagsHtml}
309
+ </div>
310
+ </div>`;
311
+ }
312
+
313
+ /**
314
+ * Renders standard RapiDoc navigation filter/search header HTML.
315
+ *
316
+ * @param {Object} [options]
317
+ * @param {string} [options.searchPlaceholder='Filter...']
318
+ * @param {string} [options.inputClassName='']
319
+ * @returns {string}
320
+ */
321
+ export function getNavFilterHtml(options = {}) {
322
+ const placeholder = options.searchPlaceholder || options.placeholder || "Filter...";
323
+ const extraClass = options.inputClassName ? ` ${options.inputClassName}` : "";
324
+ return `
325
+ <div class="nav-bar-header" part="section-navbar-header">
326
+ <div class="nav-bar-search-container" part="section-navbar-search">
327
+ <div class="nav-bar-search-input-wrapper">
328
+ <input
329
+ id="nav-bar-search"
330
+ part="textbox textbox-nav-filter"
331
+ class="nav-bar-search-input portal-filter-input${extraClass}"
332
+ type="text"
333
+ placeholder="${escapeHtml(placeholder)}"
334
+ aria-label="Filter endpoints"
335
+ spellcheck="false"
336
+ />
337
+ <svg class="nav-bar-search-icon" viewBox="0 0 256 256" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z"/></svg>
338
+ </div>
339
+ </div>
340
+ </div>`;
341
+ }
342
+
343
+ /**
344
+ * Asynchronously generates complete navigation HTML directly from a spec input (file, URL, or object).
345
+ *
346
+ * @param {string|Object} specInput
347
+ * @param {Object} [options]
348
+ * @returns {Promise<string>}
349
+ */
350
+ export async function getNavBarHtml(specInput, options = {}) {
351
+ const resolvedSpec = await resolveSpecInput(specInput, options);
352
+ return renderNavBarHtml(resolvedSpec, options);
353
+ }
354
+
355
+ /**
356
+ * Pure, zero-dependency HTML string generator for OpenAPI navigation.
357
+ * Produces standard RapiDoc navigation HTML directly from the spec.
358
+ *
359
+ * @param {Object} resolvedSpec - Resolved OpenAPI specification
360
+ * @param {Object} [options]
361
+ * @param {string} [options.basePath=''] - Base URL prefix for all links (e.g. '/apis/shipments')
362
+ * @param {string} [options.activeHref=''] - Currently active path or anchor
363
+ * @param {boolean} [options.includeSearch=true] - Render filter/search input header
364
+ * @param {string} [options.searchPlaceholder='Filter...'] - Search placeholder
365
+ * @param {string} [options.backToDocsHref=''] - Optional back-link to documentation
366
+ * @param {string} [options.backToDocsLabel='Documentation'] - Label for back-link
367
+ * @param {string} [options.itemClassName=''] - Additional CSS class for endpoint link items
368
+ * @param {boolean} [options.includeOverview=true] - Include overview/info link
369
+ * @param {'3-char'|'full'} [options.methodBadgeFormat='3-char'] - Badge format (with `as-colored-block`)
370
+ * @param {'false'|'as-plain-text'|'as-colored-text'|'as-colored-block'} [options.showMethodInNavBar='as-colored-block'] -
371
+ * How each operation shows its HTTP method, as `show-method-in-nav-bar` of <rapi-doc> (also accepted under that name)
372
+ * @param {boolean|string} [options.usePathInNav=false] - Display endpoint path instead of summary (also accepted as
373
+ * `use-path-in-nav-bar`, `'true'` or `true`)
374
+ * @returns {string} Standard RapiDoc navigation HTML
375
+ */
376
+ export function renderNavBarHtml(resolvedSpec = {}, options = {}) {
377
+ const {
378
+ basePath = "",
379
+ activeHref = "",
380
+ includeSearch = true,
381
+ searchPlaceholder = "Filter...",
382
+ backToDocsHref = "",
383
+ backToDocsLabel = "Documentation",
384
+ itemClassName = "",
385
+ includeOverview = true,
386
+ methodBadgeFormat = "3-char",
387
+ routingMode = options.routingMode ?? options["routing-mode"] ?? "single-page",
388
+ } = options;
389
+ // The same values as <rapi-doc>'s show-method-in-nav-bar and use-path-in-nav-bar; anything else is the default.
390
+ const methodStyle = options.showMethodInNavBar ?? options["show-method-in-nav-bar"];
391
+ const showMethodInNavBar = METHOD_STYLES.includes(String(methodStyle))
392
+ ? String(methodStyle)
393
+ : "as-colored-block";
394
+ const pathOption = options.usePathInNav ?? options["use-path-in-nav-bar"];
395
+ const usePathInNav = pathOption === true || pathOption === "true";
396
+
397
+ const slugger = new OperationSlugger();
398
+ const cleanBase = basePath.replace(/\/$/, "");
399
+
400
+ const ctx = {
401
+ basePath,
402
+ cleanBase,
403
+ activeHref,
404
+ routingMode,
405
+ itemClassName,
406
+ includeOverview,
407
+ methodBadgeFormat,
408
+ showMethodInNavBar,
409
+ usePathInNav,
410
+ resolvedSpec,
411
+ slugger,
412
+ };
413
+
414
+ let overviewHtml = "";
415
+ if (includeOverview && resolvedSpec.info) {
416
+ const overviewTitle = resolvedSpec.info.title || "Overview";
417
+ const href = `${basePath}#overview`;
418
+ const isActive = Boolean(
419
+ activeHref &&
420
+ (activeHref === href ||
421
+ activeHref === basePath ||
422
+ activeHref === `${basePath}/` ||
423
+ activeHref.endsWith("#overview")),
424
+ );
425
+ overviewHtml = `
426
+ <a
427
+ href="${escapeHtml(href)}"
428
+ class="nav-bar-info ${isActive ? "active" : ""}"
429
+ id="link-overview"
430
+ data-action="navigate"
431
+ data-content-id="overview"
432
+ tabindex="0"
433
+ part="section-navbar-item section-navbar-overview"
434
+ >
435
+ <span class="nav-bar-info-title">${escapeHtml(overviewTitle)}</span>
436
+ </a>`;
437
+ }
438
+
439
+ let bodyHtml;
440
+ if (resolvedSpec.tagGroups && resolvedSpec.tagGroups.length > 0) {
441
+ const assignedTagNames = new Set();
442
+ const groupsHtml = resolvedSpec.tagGroups
443
+ .filter((g) => !isInternalOrHidden(g))
444
+ .map((group) => {
445
+ (group.tags || []).forEach((t) => assignedTagNames.add((t.name || "").toLowerCase()));
446
+ return renderGroup(group, ctx);
447
+ })
448
+ .filter(Boolean)
449
+ .join("");
450
+
451
+ const remainingTagsHtml = (resolvedSpec.tags || [])
452
+ .filter((t) => !assignedTagNames.has((t.name || "").toLowerCase()) && !isInternalOrHidden(t))
453
+ .map((t) => renderTag(t, ctx))
454
+ .filter(Boolean)
455
+ .join("");
456
+
457
+ bodyHtml = groupsHtml + remainingTagsHtml;
458
+ } else {
459
+ bodyHtml = (resolvedSpec.tags || [])
460
+ .filter((t) => !isInternalOrHidden(t))
461
+ .map((t) => renderTag(t, ctx))
462
+ .filter(Boolean)
463
+ .join("");
464
+ }
465
+
466
+ // Dedicated in-place filter input header (without redundant SEARCH modal button)
467
+ const searchHeaderHtml = includeSearch ? getNavFilterHtml({ searchPlaceholder }) : "";
468
+
469
+ const backLinkHtml = backToDocsHref
470
+ ? `
471
+ <div class="portal-sidebar-footer-link">
472
+ <a href="${escapeHtml(backToDocsHref)}" class="nav-bar-path">
473
+ <span class="nav-bar-path-label">&larr; ${escapeHtml(backToDocsLabel)}</span>
474
+ </a>
475
+ </div>`
476
+ : "";
477
+
478
+ return `
479
+ ${searchHeaderHtml}
480
+ <div class="nav-scroll" tabindex="-1" part="section-navbar-scroll">
481
+ ${overviewHtml}
482
+ ${bodyHtml}
483
+ ${backLinkHtml}
484
+ </div>`;
485
+ }
486
+
487
+ export { bindNavBar };
488
+
489
+ export default {
490
+ getSpecNavigation,
491
+ getNavBarHtml,
492
+ renderNavBarHtml,
493
+ getNavFilterHtml,
494
+ getMethodBadge,
495
+ isInternalOrHidden,
496
+ bindNavBar,
497
+ };