inviton-backduck 1.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +302 -0
  3. package/dist/apidoc/api-doc-generator.d.ts +58 -0
  4. package/dist/apidoc/api-doc-generator.d.ts.map +1 -0
  5. package/dist/apidoc/api-doc-generator.js +201 -0
  6. package/dist/apidoc/api-doc-generator.js.map +1 -0
  7. package/dist/apidoc/config.d.ts +153 -0
  8. package/dist/apidoc/config.d.ts.map +1 -0
  9. package/dist/apidoc/config.js +254 -0
  10. package/dist/apidoc/config.js.map +1 -0
  11. package/dist/apidoc/controller-parser.d.ts +208 -0
  12. package/dist/apidoc/controller-parser.d.ts.map +1 -0
  13. package/dist/apidoc/controller-parser.js +686 -0
  14. package/dist/apidoc/controller-parser.js.map +1 -0
  15. package/dist/apidoc/html-generator.d.ts +290 -0
  16. package/dist/apidoc/html-generator.d.ts.map +1 -0
  17. package/dist/apidoc/html-generator.js +2295 -0
  18. package/dist/apidoc/html-generator.js.map +1 -0
  19. package/dist/apidoc/index.d.ts +20 -0
  20. package/dist/apidoc/index.d.ts.map +1 -0
  21. package/dist/apidoc/index.js +16 -0
  22. package/dist/apidoc/index.js.map +1 -0
  23. package/dist/apidoc/openapi-builder.d.ts +169 -0
  24. package/dist/apidoc/openapi-builder.d.ts.map +1 -0
  25. package/dist/apidoc/openapi-builder.js +634 -0
  26. package/dist/apidoc/openapi-builder.js.map +1 -0
  27. package/dist/apidoc/parameterGeneratorRegistry.d.ts +20 -0
  28. package/dist/apidoc/parameterGeneratorRegistry.d.ts.map +1 -0
  29. package/dist/apidoc/parameterGeneratorRegistry.js +6 -0
  30. package/dist/apidoc/parameterGeneratorRegistry.js.map +1 -0
  31. package/dist/apidoc/test-type-resolver.d.ts +2 -0
  32. package/dist/apidoc/test-type-resolver.d.ts.map +1 -0
  33. package/dist/apidoc/test-type-resolver.js +6 -0
  34. package/dist/apidoc/test-type-resolver.js.map +1 -0
  35. package/dist/apidoc/type-resolver.d.ts +266 -0
  36. package/dist/apidoc/type-resolver.d.ts.map +1 -0
  37. package/dist/apidoc/type-resolver.js +1226 -0
  38. package/dist/apidoc/type-resolver.js.map +1 -0
  39. package/dist/apidoc/verify-type-resolution.d.ts +3 -0
  40. package/dist/apidoc/verify-type-resolution.d.ts.map +1 -0
  41. package/dist/apidoc/verify-type-resolution.js +29 -0
  42. package/dist/apidoc/verify-type-resolution.js.map +1 -0
  43. package/dist/bun/bunRouter.d.ts +70 -0
  44. package/dist/bun/bunRouter.d.ts.map +1 -0
  45. package/dist/bun/bunRouter.js +324 -0
  46. package/dist/bun/bunRouter.js.map +1 -0
  47. package/dist/bun/bunServer.d.ts +72 -0
  48. package/dist/bun/bunServer.d.ts.map +1 -0
  49. package/dist/bun/bunServer.js +218 -0
  50. package/dist/bun/bunServer.js.map +1 -0
  51. package/dist/bun/bunStaticFiles.d.ts +76 -0
  52. package/dist/bun/bunStaticFiles.d.ts.map +1 -0
  53. package/dist/bun/bunStaticFiles.js +251 -0
  54. package/dist/bun/bunStaticFiles.js.map +1 -0
  55. package/dist/bun/index.d.ts +7 -0
  56. package/dist/bun/index.d.ts.map +1 -0
  57. package/dist/bun/index.js +7 -0
  58. package/dist/bun/index.js.map +1 -0
  59. package/dist/data-contracts.d.ts +132 -0
  60. package/dist/data-contracts.d.ts.map +1 -0
  61. package/dist/data-contracts.js +2 -0
  62. package/dist/data-contracts.js.map +1 -0
  63. package/dist/decorators.d.ts +75 -0
  64. package/dist/decorators.d.ts.map +1 -0
  65. package/dist/decorators.js +101 -0
  66. package/dist/decorators.js.map +1 -0
  67. package/dist/express/expressFrontendRouter.d.ts +17 -0
  68. package/dist/express/expressFrontendRouter.d.ts.map +1 -0
  69. package/dist/express/expressFrontendRouter.js +33 -0
  70. package/dist/express/expressFrontendRouter.js.map +1 -0
  71. package/dist/express/expressRouter.d.ts +25 -0
  72. package/dist/express/expressRouter.d.ts.map +1 -0
  73. package/dist/express/expressRouter.js +150 -0
  74. package/dist/express/expressRouter.js.map +1 -0
  75. package/dist/express/index.d.ts +6 -0
  76. package/dist/express/index.d.ts.map +1 -0
  77. package/dist/express/index.js +6 -0
  78. package/dist/express/index.js.map +1 -0
  79. package/dist/index.d.ts +47 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +52 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/router.d.ts +162 -0
  84. package/dist/router.d.ts.map +1 -0
  85. package/dist/router.js +350 -0
  86. package/dist/router.js.map +1 -0
  87. package/dist/runtime-detect.d.ts +20 -0
  88. package/dist/runtime-detect.d.ts.map +1 -0
  89. package/dist/runtime-detect.js +20 -0
  90. package/dist/runtime-detect.js.map +1 -0
  91. package/dist/server.d.ts +126 -0
  92. package/dist/server.d.ts.map +1 -0
  93. package/dist/server.js +181 -0
  94. package/dist/server.js.map +1 -0
  95. package/dist/utils.d.ts +83 -0
  96. package/dist/utils.d.ts.map +1 -0
  97. package/dist/utils.js +157 -0
  98. package/dist/utils.js.map +1 -0
  99. package/package.json +65 -0
@@ -0,0 +1,2295 @@
1
+ import { ApiDocConfig } from './config';
2
+ /**
3
+ * HTTP method badge color configuration
4
+ */
5
+ const METHOD_BADGES = {
6
+ GET: { color: '#198754', bgClass: 'bg-success' },
7
+ POST: { color: '#0d6efd', bgClass: 'bg-primary' },
8
+ PUT: { color: '#fd7e14', bgClass: 'bg-warning' },
9
+ PATCH: { color: '#6f42c1', bgClass: 'bg-purple' },
10
+ DELETE: { color: '#dc3545', bgClass: 'bg-danger' },
11
+ };
12
+ /**
13
+ * Generator for readme.io-style static HTML documentation
14
+ * Uses Bootstrap 5 for styling
15
+ */
16
+ export class HtmlGenerator {
17
+ endpoints = [];
18
+ schemas = new Map();
19
+ endpointGroups = [];
20
+ /**
21
+ * Generate a complete HTML documentation page (index with intro)
22
+ */
23
+ generatePage(endpoints) {
24
+ this.endpoints = endpoints;
25
+ this.endpointGroups = this.groupEndpointsByPath(endpoints);
26
+ const groupedEndpoints = this.groupEndpointsByTag(endpoints);
27
+ const tags = Array.from(groupedEndpoints.keys()).sort();
28
+ return `<!DOCTYPE html>
29
+ <html lang="en">
30
+ <head>
31
+ <meta charset="UTF-8">
32
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
33
+ <title>${this.escapeHtml(ApiDocConfig.title)} - API Documentation</title>
34
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
35
+ ${this.generateStyles()}
36
+ </head>
37
+ <body>
38
+ <div class="container-fluid">
39
+ <div class="row">
40
+ ${this.generateSidebar(tags, groupedEndpoints, 'index.html')}
41
+ ${this.generateMainContent(tags, groupedEndpoints)}
42
+ </div>
43
+ </div>
44
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
45
+ ${this.generateScripts()}
46
+ </body>
47
+ </html>`;
48
+ }
49
+ /**
50
+ * Get all endpoint groups for generating individual pages
51
+ */
52
+ getEndpointGroups() {
53
+ return this.endpointGroups;
54
+ }
55
+ /**
56
+ * Generate a standalone HTML page for an endpoint (all methods at that path)
57
+ */
58
+ generateEndpointPage(group, allEndpoints) {
59
+ const groupedEndpoints = this.groupEndpointsByTag(allEndpoints);
60
+ const tags = Array.from(groupedEndpoints.keys()).sort();
61
+ // Generate code snippets for the first method (used in sticky panel)
62
+ const primaryEndpoint = group.methods[0];
63
+ return `<!DOCTYPE html>
64
+ <html lang="en">
65
+ <head>
66
+ <meta charset="UTF-8">
67
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
68
+ <title>${this.escapeHtml(group.path)} - ${this.escapeHtml(ApiDocConfig.title)}</title>
69
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
70
+ ${this.generateStyles()}
71
+ </head>
72
+ <body>
73
+ <div class="container-fluid">
74
+ <div class="row">
75
+ ${this.generateSidebar(tags, groupedEndpoints, `${group.slug}.html`)}
76
+ <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
77
+ <nav aria-label="breadcrumb" class="pt-3">
78
+ <ol class="breadcrumb">
79
+ <li class="breadcrumb-item"><a href="index.html">${this.escapeHtml(ApiDocConfig.title)}</a></li>
80
+ <li class="breadcrumb-item"><a href="index.html#section-${this.slugify(group.tag)}">${this.escapeHtml(this.capitalize(group.tag))}</a></li>
81
+ <li class="breadcrumb-item active" aria-current="page">${this.escapeHtml(group.path.split('/').slice(-1)[0])}</li>
82
+ </ol>
83
+ </nav>
84
+
85
+ <div class="endpoint-header mb-4">
86
+ <h1 class="h3"><code>${this.escapeHtml(group.path)}</code></h1>
87
+ <div class="d-flex gap-2 mt-2">
88
+ ${group.methods.map(m => `<span class="badge ${METHOD_BADGES[m.method]?.bgClass || 'bg-secondary'}">${m.method}</span>`).join('')}
89
+ </div>
90
+ </div>
91
+
92
+ <div class="endpoint-page-content">
93
+ <div class="endpoint-details">
94
+ ${group.methods.map(endpoint => this.generateEndpointMethodSection(endpoint)).join('\n')}
95
+
96
+ ${this.generateSchemaDefinitions(group.methods)}
97
+
98
+ <div class="mt-4 mb-4">
99
+ <a href="index.html" class="btn btn-outline-secondary">&larr; Back to all endpoints</a>
100
+ </div>
101
+ </div>
102
+ <div class="code-panel">
103
+ ${this.generateCodeSnippets(primaryEndpoint)}
104
+ </div>
105
+ </div>
106
+ </main>
107
+ </div>
108
+ </div>
109
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
110
+ ${this.generateScripts()}
111
+ </body>
112
+ </html>`;
113
+ }
114
+ /**
115
+ * Generate an endpoint card HTML for a single endpoint (used in index)
116
+ */
117
+ generateEndpointCard(endpoint) {
118
+ const method = endpoint.method || 'GET';
119
+ const path = endpoint.path || '/';
120
+ const badge = METHOD_BADGES[method] || METHOD_BADGES.GET;
121
+ // Support both auth object and requiresAuth convenience flag
122
+ const auth = endpoint.auth || (endpoint.requiresAuth ? { hasAuth: true, requiresLogin: false, requiresVerifiedAccount: false, isEnterpriseOnly: false } : undefined);
123
+ const description = endpoint.description || '';
124
+ const operationId = endpoint.operationId || this.generateOperationId(path, method);
125
+ const authClass = auth?.hasAuth ? ' requires-auth' : '';
126
+ return `<div class="endpoint-card card mb-3${authClass}" id="${this.escapeHtml(operationId)}" data-auth="${auth?.hasAuth ? 'true' : 'false'}">
127
+ <div class="card-header d-flex align-items-center justify-content-between">
128
+ <div class="d-flex align-items-center gap-2">
129
+ <span class="method-badge badge ${badge.bgClass}">${this.escapeHtml(method)}</span>
130
+ <code class="endpoint-path">${this.escapeHtml(path)}</code>
131
+ </div>
132
+ <div class="d-flex align-items-center gap-2">
133
+ ${auth?.hasAuth ? this.generateAuthBadges(auth) : '<span class="badge bg-secondary">Public</span>'}
134
+ </div>
135
+ </div>
136
+ <div class="card-body">
137
+ ${description ? this.convertDescriptionToHtml(description) : ''}
138
+ ${endpoint.requestSchema || endpoint.requestTypeName ? this.generateRequestSection(endpoint) : ''}
139
+ ${endpoint.responseSchema || endpoint.responseTypeName ? this.generateResponseSection(endpoint) : ''}
140
+ </div>
141
+ </div>`;
142
+ }
143
+ /**
144
+ * Generate an endpoint method card for individual endpoint pages
145
+ * Includes a two-column layout with endpoint details on the left and code snippets on the right
146
+ */
147
+ generateEndpointMethodCard(endpoint) {
148
+ const method = endpoint.method;
149
+ const badge = METHOD_BADGES[method] || METHOD_BADGES.GET;
150
+ const auth = endpoint.auth;
151
+ const description = endpoint.description || '';
152
+ const operationId = endpoint.operationId || this.generateOperationId(endpoint.path, method);
153
+ return `<div class="card mb-4" id="${this.escapeHtml(operationId)}">
154
+ <div class="card-header d-flex align-items-center justify-content-between">
155
+ <div class="d-flex align-items-center gap-2">
156
+ <span class="method-badge badge ${badge.bgClass} fs-6">${this.escapeHtml(method)}</span>
157
+ <span class="text-muted">${this.escapeHtml(endpoint.path)}</span>
158
+ </div>
159
+ <div class="d-flex align-items-center gap-2">
160
+ ${auth?.hasAuth ? this.generateAuthBadges(auth) : '<span class="badge bg-secondary">Public</span>'}
161
+ </div>
162
+ </div>
163
+ <div class="card-body">
164
+ <div class="row">
165
+ <div class="col-lg-7">
166
+ ${description ? this.convertDescriptionToHtml(description) : ''}
167
+ ${endpoint.requestSchema || endpoint.requestTypeName ? this.generateRequestSection(endpoint) : ''}
168
+ ${endpoint.responseSchema || endpoint.responseTypeName ? this.generateResponseSection(endpoint) : ''}
169
+ </div>
170
+ <div class="col-lg-5">
171
+ ${this.generateCodeSnippets(endpoint)}
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>`;
176
+ }
177
+ /**
178
+ * Generate endpoint method section for the new two-column page layout
179
+ * Does not include code snippets - those are in the sticky sidebar
180
+ */
181
+ generateEndpointMethodSection(endpoint) {
182
+ const method = endpoint.method;
183
+ const badge = METHOD_BADGES[method] || METHOD_BADGES.GET;
184
+ const auth = endpoint.auth;
185
+ const description = endpoint.description || '';
186
+ const operationId = endpoint.operationId || this.generateOperationId(endpoint.path, method);
187
+ return `<div class="card mb-4" id="${this.escapeHtml(operationId)}">
188
+ <div class="card-header d-flex align-items-center justify-content-between">
189
+ <div class="d-flex align-items-center gap-2">
190
+ <span class="method-badge badge ${badge.bgClass} fs-6">${this.escapeHtml(method)}</span>
191
+ <span class="text-muted">${this.escapeHtml(endpoint.path)}</span>
192
+ </div>
193
+ <div class="d-flex align-items-center gap-2">
194
+ ${auth?.hasAuth ? this.generateAuthBadges(auth) : '<span class="badge bg-secondary">Public</span>'}
195
+ </div>
196
+ </div>
197
+ <div class="card-body">
198
+ ${description ? this.convertDescriptionToHtml(description) : ''}
199
+ ${endpoint.requestSchema || endpoint.requestTypeName ? this.generateRequestSection(endpoint) : ''}
200
+ ${endpoint.responseSchema || endpoint.responseTypeName ? this.generateResponseSection(endpoint) : ''}
201
+ </div>
202
+ </div>`;
203
+ }
204
+ /**
205
+ * Generate a schema table for displaying type information
206
+ */
207
+ generateSchemaTable(schema) {
208
+ if (!schema || schema.$ref) {
209
+ const refName = schema?.$ref?.replace('#/components/schemas/', '') || 'Unknown';
210
+ return `<div class="schema-ref">
211
+ <span class="badge bg-info">$ref</span> <code>${this.escapeHtml(refName)}</code>
212
+ </div>`;
213
+ }
214
+ if (schema.type !== 'object' || !schema.properties) {
215
+ return this.generateSimpleTypeDisplay(schema);
216
+ }
217
+ const required = schema.required || [];
218
+ return `<table class="table table-sm table-bordered schema-table">
219
+ <thead class="table-light">
220
+ <tr>
221
+ <th>Property</th>
222
+ <th>Type</th>
223
+ <th>Required</th>
224
+ <th>Description</th>
225
+ </tr>
226
+ </thead>
227
+ <tbody>
228
+ ${Object.entries(schema.properties)
229
+ .map(([name, propSchema,]) => this.generateSchemaTableRow(name, propSchema, required))
230
+ .join('\n\t\t')}
231
+ </tbody>
232
+ </table>`;
233
+ }
234
+ /**
235
+ * Add schemas for reference lookup
236
+ */
237
+ addSchemas(schemas) {
238
+ this.schemas = schemas;
239
+ }
240
+ /**
241
+ * Collect all schemas referenced in an endpoint (request and response)
242
+ */
243
+ collectReferencedSchemas(endpoint) {
244
+ const referenced = new Set();
245
+ const visited = new Set();
246
+ const collectFromSchema = (schema) => {
247
+ if (!schema) {
248
+ return;
249
+ }
250
+ if (schema.$ref) {
251
+ const refName = schema.$ref.replace('#/components/schemas/', '');
252
+ // Skip if already visited to prevent circular reference infinite loop
253
+ if (visited.has(refName)) {
254
+ return;
255
+ }
256
+ visited.add(refName);
257
+ referenced.add(refName);
258
+ // Recursively collect from the referenced schema
259
+ const refSchema = this.schemas.get(refName);
260
+ if (refSchema) {
261
+ collectFromSchema(refSchema);
262
+ }
263
+ }
264
+ if (schema.properties) {
265
+ for (const prop of Object.values(schema.properties)) {
266
+ collectFromSchema(prop);
267
+ }
268
+ }
269
+ if (schema.items) {
270
+ collectFromSchema(schema.items);
271
+ }
272
+ if (schema.oneOf) {
273
+ for (const item of schema.oneOf) {
274
+ collectFromSchema(item);
275
+ }
276
+ }
277
+ };
278
+ collectFromSchema(endpoint.requestSchema);
279
+ collectFromSchema(endpoint.responseSchema);
280
+ return referenced;
281
+ }
282
+ /**
283
+ * Generate schema definitions section with anchor IDs for navigation
284
+ */
285
+ generateSchemaDefinitions(endpoints) {
286
+ // Collect all referenced schemas from all endpoints
287
+ const allReferenced = new Set();
288
+ for (const endpoint of endpoints) {
289
+ const refs = this.collectReferencedSchemas(endpoint);
290
+ for (const ref of refs) {
291
+ allReferenced.add(ref);
292
+ }
293
+ }
294
+ if (allReferenced.size === 0) {
295
+ return '';
296
+ }
297
+ // Sort schemas alphabetically
298
+ const sortedSchemas = Array.from(allReferenced).sort();
299
+ const schemaCards = sortedSchemas.map((schemaName) => {
300
+ const schema = this.schemas.get(schemaName);
301
+ if (!schema) {
302
+ return '';
303
+ }
304
+ const slug = this.slugify(schemaName);
305
+ return `<div class="card mb-3 schema-definition" id="schema-${slug}">
306
+ <div class="card-header">
307
+ <h6 class="mb-0">
308
+ <code>${this.escapeHtml(schemaName)}</code>
309
+ ${schema.description ? `<small class="text-muted ms-2">${this.convertJsDocLinks(schema.description)}</small>` : ''}
310
+ </h6>
311
+ </div>
312
+ <div class="card-body p-0">
313
+ ${this.generateSchemaTable(schema)}
314
+ </div>
315
+ </div>`;
316
+ }).filter(Boolean).join('\n\t\t');
317
+ return `<section id="schema-definitions" class="mt-5 pt-4 border-top">
318
+ <h4 class="mb-4">Schema Definitions</h4>
319
+ <p class="text-muted mb-4">Reference documentation for data types used in this API.</p>
320
+ <div class="schema-definitions-list">
321
+ ${schemaCards}
322
+ </div>
323
+ </section>`;
324
+ }
325
+ /**
326
+ * Group endpoints by their path (for individual endpoint pages)
327
+ */
328
+ groupEndpointsByPath(endpoints) {
329
+ const pathMap = new Map();
330
+ for (const endpoint of endpoints) {
331
+ const existing = pathMap.get(endpoint.path) || [];
332
+ existing.push(endpoint);
333
+ pathMap.set(endpoint.path, existing);
334
+ }
335
+ const groups = [];
336
+ for (const [path, methods,] of pathMap) {
337
+ // Sort methods by order
338
+ methods.sort((a, b) => this.getMethodOrder(a.method) - this.getMethodOrder(b.method));
339
+ groups.push({
340
+ path,
341
+ tag: methods[0].tag,
342
+ methods,
343
+ slug: this.pathToSlug(path),
344
+ });
345
+ }
346
+ // Sort groups by path
347
+ groups.sort((a, b) => a.path.localeCompare(b.path));
348
+ return groups;
349
+ }
350
+ /**
351
+ * Group endpoints by their tag
352
+ */
353
+ groupEndpointsByTag(endpoints) {
354
+ const grouped = new Map();
355
+ for (const endpoint of endpoints) {
356
+ const tag = endpoint.tag || 'general';
357
+ const existing = grouped.get(tag) || [];
358
+ existing.push(endpoint);
359
+ grouped.set(tag, existing);
360
+ }
361
+ // Sort endpoints within each group by path and method
362
+ for (const [tag, tagEndpoints,] of grouped) {
363
+ tagEndpoints.sort((a, b) => {
364
+ const pathCompare = a.path.localeCompare(b.path);
365
+ if (pathCompare !== 0) {
366
+ return pathCompare;
367
+ }
368
+ return this.getMethodOrder(a.method) - this.getMethodOrder(b.method);
369
+ });
370
+ grouped.set(tag, tagEndpoints);
371
+ }
372
+ return grouped;
373
+ }
374
+ /**
375
+ * Get method sort order
376
+ */
377
+ getMethodOrder(method) {
378
+ const order = { GET: 0, POST: 1, PUT: 2, PATCH: 3, DELETE: 4 };
379
+ return order[method] ?? 99;
380
+ }
381
+ /**
382
+ * Convert path to URL-safe slug for filename
383
+ */
384
+ pathToSlug(path) {
385
+ const slug = path
386
+ .replace(/^\/api\/(shop|admin)\//, '')
387
+ .replace(/[^a-z0-9]+/gi, '-')
388
+ .replace(/^-+|-+$/g, '')
389
+ .toLowerCase();
390
+ // Handle edge case where path is just /api/shop/ or /api/admin/
391
+ return slug || 'root';
392
+ }
393
+ /**
394
+ * Generate the sidebar navigation
395
+ */
396
+ generateSidebar(tags, groupedEndpoints, currentPage) {
397
+ return `<nav id="sidebar" class="col-md-3 col-lg-2 d-md-block sidebar collapse">
398
+ <div class="position-sticky">
399
+ <div class="sidebar-header px-3 mb-3">
400
+ <a href="index.html" class="text-decoration-none text-dark">
401
+ <h4 class="mb-0">${this.escapeHtml(ApiDocConfig.title)}</h4>
402
+ </a>
403
+ <small class="text-muted">v${this.escapeHtml(ApiDocConfig.version)}</small>
404
+ </div>
405
+ <ul class="nav flex-column">
406
+ <li class="nav-item">
407
+ <a class="nav-link sidebar-intro-link${currentPage === 'index.html' ? ' active' : ''}" href="index.html#getting-started">
408
+ Getting Started
409
+ </a>
410
+ </li>
411
+ ${tags.map(tag => this.generateSidebarGroup(tag, groupedEndpoints.get(tag) || [], currentPage)).join('\n\t\t\t')}
412
+ </ul>
413
+ </div>
414
+ </nav>`;
415
+ }
416
+ /**
417
+ * Generate a sidebar group for a tag
418
+ */
419
+ generateSidebarGroup(tag, endpoints, currentPage) {
420
+ const tagId = this.slugify(tag);
421
+ // Group endpoints by path for sidebar display
422
+ const pathGroups = new Map();
423
+ for (const ep of endpoints) {
424
+ const existing = pathGroups.get(ep.path) || [];
425
+ existing.push(ep);
426
+ pathGroups.set(ep.path, existing);
427
+ }
428
+ return `<li class="nav-item">
429
+ <a class="nav-link sidebar-group-header" data-bs-toggle="collapse" href="#nav-${tagId}" role="button" aria-expanded="true" aria-controls="nav-${tagId}">
430
+ <span class="sidebar-tag-name">${this.escapeHtml(this.capitalize(tag))}</span>
431
+ <span class="badge bg-secondary ms-auto">${pathGroups.size}</span>
432
+ </a>
433
+ <div class="collapse show" id="nav-${tagId}">
434
+ <ul class="nav flex-column sidebar-submenu">
435
+ ${Array.from(pathGroups.entries()).map(([path, eps,]) => this.generateSidebarItem(path, eps, currentPage)).join('\n\t\t\t')}
436
+ </ul>
437
+ </div>
438
+ </li>`;
439
+ }
440
+ /**
441
+ * Generate a sidebar item for an endpoint path
442
+ */
443
+ generateSidebarItem(path, endpoints, currentPage) {
444
+ const slug = this.pathToSlug(path);
445
+ const shortPath = path.split('/').slice(-2).join('/');
446
+ const isActive = currentPage === `${slug}.html`;
447
+ return `<li class="nav-item">
448
+ <a class="nav-link sidebar-endpoint${isActive ? ' active' : ''}" href="${slug}.html">
449
+ <span class="d-flex gap-1">
450
+ ${endpoints.map(ep => `<span class="method-badge-sm badge ${METHOD_BADGES[ep.method]?.bgClass || 'bg-secondary'}">${ep.method.substring(0, 3)}</span>`).join('')}
451
+ </span>
452
+ <span class="endpoint-path-sm">${this.escapeHtml(shortPath)}</span>
453
+ </a>
454
+ </li>`;
455
+ }
456
+ /**
457
+ * Generate the main content area with intro section only (no endpoint cards)
458
+ */
459
+ generateMainContent(tags, groupedEndpoints) {
460
+ return `<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
461
+ <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
462
+ <h1 class="h2">${this.escapeHtml(ApiDocConfig.title)}</h1>
463
+ <div class="btn-toolbar mb-2 mb-md-0">
464
+ <div class="btn-group me-2">
465
+ <a href="./swagger/index.html" class="btn btn-sm btn-outline-secondary">Swagger UI</a>
466
+ </div>
467
+ </div>
468
+ </div>
469
+
470
+ ${this.generateGettingStartedSection()}
471
+
472
+ ${this.generateApiOverviewSection(tags, groupedEndpoints)}
473
+ </main>`;
474
+ }
475
+ /**
476
+ * Generate the getting started / intro section
477
+ */
478
+ generateGettingStartedSection() {
479
+ const config = ApiDocConfig.gettingStarted;
480
+ return `<section id="getting-started" class="mb-5">
481
+ <h2 class="tag-header">Getting Started</h2>
482
+
483
+ <div class="intro-content">
484
+ <p class="lead">${this.escapeHtml(ApiDocConfig.description)}</p>
485
+
486
+ <p>${config.introText}</p>
487
+
488
+ <h4 class="mt-4">API Call Structure</h4>
489
+ <h5>URI Syntax</h5>
490
+ <p>The API URI is assembled from multiple parts:</p>
491
+ <div class="alert alert-info">
492
+ <code>https://{server_url}/api/{interface}/{resource}/{action}?{query_parameters}</code>
493
+ </div>
494
+
495
+ <table class="table table-bordered">
496
+ <tbody>
497
+ <tr>
498
+ <td><code>{server_url}</code></td>
499
+ <td>The server URL for your environment (see Servers section below)</td>
500
+ </tr>
501
+ <tr>
502
+ <td><code>{interface}</code></td>
503
+ <td>${config.apiInterfaces.map(i => `<strong>${this.escapeHtml(i.name)}</strong> - ${this.escapeHtml(i.description)}`).join('<br>')}</td>
504
+ </tr>
505
+ <tr>
506
+ <td><code>{resource}</code></td>
507
+ <td>The resource you need to access (e.g., ${config.exampleResources.map(r => `<code>${this.escapeHtml(r)}</code>`).join(', ')})</td>
508
+ </tr>
509
+ <tr>
510
+ <td><code>{action}</code></td>
511
+ <td>Optional action to perform on the resource</td>
512
+ </tr>
513
+ <tr>
514
+ <td><code>{query_parameters}</code></td>
515
+ <td>Optional parameters for filtering, pagination, etc.</td>
516
+ </tr>
517
+ </tbody>
518
+ </table>
519
+
520
+ ${this.generateServersList()}
521
+
522
+ <h4 class="mt-4">Request Body</h4>
523
+ <p>The API uses JSON format for request bodies. When creating or updating resources,
524
+ include the required fields in the request body as JSON.</p>
525
+
526
+ <h5>Required Parameters</h5>
527
+ <p>If a parameter is marked as <span class="badge bg-danger">Required</span> in the documentation,
528
+ it must be included in your API call.</p>
529
+
530
+ ${this.generateHttpHeadersTable()}
531
+
532
+ <h4 class="mt-4">HTTP Methods</h4>
533
+ <table class="table table-bordered">
534
+ <tbody>
535
+ <tr>
536
+ <td><span class="badge bg-success">GET</span></td>
537
+ <td>Retrieve data from the server</td>
538
+ </tr>
539
+ <tr>
540
+ <td><span class="badge bg-primary">POST</span></td>
541
+ <td>Create new resources or perform actions</td>
542
+ </tr>
543
+ <tr>
544
+ <td><span class="badge bg-warning text-dark">PUT</span></td>
545
+ <td>Update an entire resource</td>
546
+ </tr>
547
+ <tr>
548
+ <td><span class="badge bg-purple">PATCH</span></td>
549
+ <td>Partially update a resource</td>
550
+ </tr>
551
+ <tr>
552
+ <td><span class="badge bg-danger">DELETE</span></td>
553
+ <td>Remove a resource</td>
554
+ </tr>
555
+ </tbody>
556
+ </table>
557
+
558
+ <h4 class="mt-4">Status Codes</h4>
559
+ <table class="table table-bordered">
560
+ <thead class="table-light">
561
+ <tr>
562
+ <th>Code</th>
563
+ <th>Description</th>
564
+ </tr>
565
+ </thead>
566
+ <tbody>
567
+ <tr>
568
+ <td><span class="badge bg-success">200 OK</span></td>
569
+ <td>Successful request</td>
570
+ </tr>
571
+ <tr>
572
+ <td><span class="badge bg-success">201 Created</span></td>
573
+ <td>Resource was created successfully</td>
574
+ </tr>
575
+ <tr>
576
+ <td><span class="badge bg-warning text-dark">400 Bad Request</span></td>
577
+ <td>Invalid input parameters</td>
578
+ </tr>
579
+ <tr>
580
+ <td><span class="badge bg-warning text-dark">401 Unauthorized</span></td>
581
+ <td>Authentication required or invalid credentials</td>
582
+ </tr>
583
+ <tr>
584
+ <td><span class="badge bg-warning text-dark">403 Forbidden</span></td>
585
+ <td>Access denied to the resource</td>
586
+ </tr>
587
+ <tr>
588
+ <td><span class="badge bg-warning text-dark">404 Not Found</span></td>
589
+ <td>Resource not found</td>
590
+ </tr>
591
+ <tr>
592
+ <td><span class="badge bg-danger">500 Internal Server Error</span></td>
593
+ <td>Server-side error</td>
594
+ </tr>
595
+ </tbody>
596
+ </table>
597
+
598
+ ${this.generateAuthenticationSection()}
599
+ </div>
600
+ </section>`;
601
+ }
602
+ /**
603
+ * Generate HTTP headers table from config
604
+ */
605
+ generateHttpHeadersTable() {
606
+ const headers = ApiDocConfig.gettingStarted.httpHeaders;
607
+ const rows = headers.map(h => `
608
+ <tr>
609
+ <td><code>${this.escapeHtml(h.name)}</code></td>
610
+ <td>${this.escapeHtml(h.description)}</td>
611
+ <td><code>${this.escapeHtml(h.example)}</code></td>
612
+ </tr>`).join('');
613
+ return `<h4 class="mt-4">HTTP Headers</h4>
614
+ <table class="table table-bordered">
615
+ <thead class="table-light">
616
+ <tr>
617
+ <th>Header</th>
618
+ <th>Description</th>
619
+ <th>Example</th>
620
+ </tr>
621
+ </thead>
622
+ <tbody>${rows}
623
+ </tbody>
624
+ </table>`;
625
+ }
626
+ /**
627
+ * Generate authentication section from config
628
+ */
629
+ generateAuthenticationSection() {
630
+ const auth = ApiDocConfig.gettingStarted.authentication;
631
+ const bearerSteps = auth.bearerToken.steps.map(s => `<li>${s}</li>`).join('\n\t\t\t');
632
+ const apiKeySteps = auth.apiKey.steps.map(s => `<li>${s}</li>`).join('\n\t\t\t');
633
+ return `<h4 class="mt-4">Authentication</h4>
634
+ <p>Protected endpoints require authentication. Endpoints marked with
635
+ <span class="badge bg-warning text-dark">Auth</span> require one of the following authentication methods:</p>
636
+
637
+ <h5 class="mt-3">${this.escapeHtml(auth.bearerToken.title)}</h5>
638
+ <p>${this.escapeHtml(auth.bearerToken.description)}</p>
639
+ <ol>
640
+ ${bearerSteps}
641
+ </ol>
642
+
643
+ <h5 class="mt-3">${this.escapeHtml(auth.apiKey.title)}</h5>
644
+ <p>${this.escapeHtml(auth.apiKey.description)}</p>
645
+ <ol>
646
+ ${apiKeySteps}
647
+ </ol>
648
+ <p class="text-muted"><small>${this.escapeHtml(auth.apiKey.note)}</small></p>
649
+
650
+ <h5 class="mt-3">Enterprise Mode</h5>
651
+ <p>Endpoints marked with <span class="badge bg-purple">Enterprise</span> are only available when authenticated
652
+ using an API key. These endpoints provide advanced functionality for enterprise integrations and may include
653
+ additional fields in responses that are not available to regular user sessions.</p>
654
+ <p>Fields marked with the <span class="badge bg-purple enterprise-field-badge">Enterprise</span> badge within
655
+ request/response schemas are only populated when the request is made using enterprise authentication.</p>`;
656
+ }
657
+ /**
658
+ * Generate servers list
659
+ */
660
+ generateServersList() {
661
+ return `<h5 class="mt-3">Servers</h5>
662
+ <table class="table table-bordered">
663
+ <thead class="table-light">
664
+ <tr>
665
+ <th>Environment</th>
666
+ <th>URL</th>
667
+ <th>Description</th>
668
+ </tr>
669
+ </thead>
670
+ <tbody>
671
+ <tr>
672
+ <td>Development</td>
673
+ <td><code>${this.escapeHtml(ApiDocConfig.servers.development.url)}</code></td>
674
+ <td>${this.escapeHtml(ApiDocConfig.servers.development.description)}</td>
675
+ </tr>
676
+ <tr>
677
+ <td>Staging</td>
678
+ <td><code>${this.escapeHtml(ApiDocConfig.servers.staging.url)}</code></td>
679
+ <td>${this.escapeHtml(ApiDocConfig.servers.staging.description)}</td>
680
+ </tr>
681
+ <tr>
682
+ <td>Production</td>
683
+ <td><code>${this.escapeHtml(ApiDocConfig.servers.production.url)}</code></td>
684
+ <td>${this.escapeHtml(ApiDocConfig.servers.production.description)}</td>
685
+ </tr>
686
+ </tbody>
687
+ </table>`;
688
+ }
689
+ /**
690
+ * Generate API overview section with endpoint summary cards (no full details)
691
+ */
692
+ generateApiOverviewSection(tags, groupedEndpoints) {
693
+ const tagCards = tags.map((tag) => {
694
+ const endpoints = groupedEndpoints.get(tag) || [];
695
+ const tagId = this.slugify(tag);
696
+ const tagDescription = ApiDocConfig.tagDescriptions[tag] || `Operations related to ${tag}`;
697
+ // Group endpoints by path for display
698
+ const pathGroups = new Map();
699
+ for (const ep of endpoints) {
700
+ const existing = pathGroups.get(ep.path) || [];
701
+ existing.push(ep);
702
+ pathGroups.set(ep.path, existing);
703
+ }
704
+ const endpointLinks = Array.from(pathGroups.entries()).map(([path, eps,]) => {
705
+ const slug = this.pathToSlug(path);
706
+ const shortPath = path.replace(/^\/api\/(shop|admin)\//, '');
707
+ return `<a href="${slug}.html" class="endpoint-link">
708
+ <span class="method-badges">
709
+ ${eps.map(ep => `<span class="method-badge-sm badge ${METHOD_BADGES[ep.method]?.bgClass || 'bg-secondary'}">${ep.method}</span>`).join('')}
710
+ </span>
711
+ <span class="endpoint-link-path">${this.escapeHtml(shortPath)}</span>
712
+ </a>`;
713
+ }).join('\n\t\t\t\t');
714
+ return `<div class="api-section-card card mb-4" id="section-${tagId}">
715
+ <div class="card-header">
716
+ <h5 class="mb-0">${this.escapeHtml(this.capitalize(tag))}</h5>
717
+ <small class="text-muted">${this.escapeHtml(tagDescription)}</small>
718
+ </div>
719
+ <div class="card-body">
720
+ <div class="endpoint-links">
721
+ ${endpointLinks}
722
+ </div>
723
+ </div>
724
+ </div>`;
725
+ }).join('\n\t');
726
+ return `<section id="api-reference" class="mb-5">
727
+ <h2 class="tag-header">API Reference</h2>
728
+ <p class="text-muted mb-4">Click on any endpoint to view detailed documentation, request/response schemas, and code examples.</p>
729
+
730
+ ${tagCards}
731
+ </section>`;
732
+ }
733
+ /**
734
+ * Generate a tag section with all its endpoints
735
+ */
736
+ generateTagSection(tag, endpoints) {
737
+ const tagId = this.slugify(tag);
738
+ const tagDescription = ApiDocConfig.tagDescriptions[tag] || `Operations related to ${tag}`;
739
+ return `<section id="section-${tagId}" class="tag-section mb-5">
740
+ <h2 class="tag-header">${this.escapeHtml(this.capitalize(tag))}</h2>
741
+ <p class="tag-description text-muted">${this.escapeHtml(tagDescription)}</p>
742
+ <div class="endpoints">
743
+ ${endpoints.map(ep => this.generateEndpointCard(ep)).join('\n\t\t')}
744
+ </div>
745
+ </section>`;
746
+ }
747
+ /**
748
+ * Generate authentication badges
749
+ */
750
+ generateAuthBadges(auth) {
751
+ const badges = [];
752
+ if (auth.isEnterpriseOnly) {
753
+ badges.push('<span class="badge bg-purple enterprise-badge" title="Enterprise Mode Only - Requires API Key"><i class="bi bi-building"></i> Enterprise</span>');
754
+ }
755
+ if (auth.hasAuth) {
756
+ badges.push('<span class="badge bg-warning text-dark" title="Authentication Required"><i class="bi bi-lock-fill"></i> Auth</span>');
757
+ }
758
+ if (auth.requiresLogin) {
759
+ badges.push('<span class="badge bg-danger" title="Login Required"><i class="bi bi-person-fill"></i> Login</span>');
760
+ }
761
+ if (auth.requiresVerifiedAccount) {
762
+ badges.push('<span class="badge bg-info" title="Verified Account Required"><i class="bi bi-patch-check-fill"></i> Verified</span>');
763
+ }
764
+ return badges.join(' ');
765
+ }
766
+ /**
767
+ * Generate request section
768
+ */
769
+ generateRequestSection(endpoint) {
770
+ const typeName = endpoint.requestTypeName || 'Request';
771
+ const paramDescription = endpoint.paramDescription || '';
772
+ return `<div class="request-section mt-3">
773
+ <h6 class="section-header">
774
+ <a class="text-decoration-none" data-bs-toggle="collapse" href="#request-${this.slugify(endpoint.operationId || endpoint.path || '')}" role="button" aria-expanded="true">
775
+ Request Body <span class="badge bg-secondary">${this.escapeHtml(typeName)}</span>
776
+ </a>
777
+ </h6>
778
+ <div class="collapse show" id="request-${this.slugify(endpoint.operationId || endpoint.path || '')}">
779
+ ${paramDescription ? `<p class="text-muted mb-2">${this.convertJsDocLinks(paramDescription)}</p>` : ''}
780
+ ${endpoint.requestSchema ? this.generateSchemaTable(endpoint.requestSchema) : `<p class="text-muted">Schema: <code>${this.escapeHtml(typeName)}</code></p>`}
781
+ </div>
782
+ </div>`;
783
+ }
784
+ /**
785
+ * Generate response section
786
+ */
787
+ generateResponseSection(endpoint) {
788
+ const typeName = endpoint.responseTypeName || 'Response';
789
+ return `<div class="response-section mt-3">
790
+ <h6 class="section-header">
791
+ <a class="text-decoration-none" data-bs-toggle="collapse" href="#response-${this.slugify(endpoint.operationId || endpoint.path || '')}" role="button" aria-expanded="true">
792
+ Response <span class="badge bg-success">200</span> <span class="badge bg-secondary">${this.escapeHtml(typeName)}</span>
793
+ </a>
794
+ </h6>
795
+ <div class="collapse show" id="response-${this.slugify(endpoint.operationId || endpoint.path || '')}">
796
+ ${endpoint.responseSchema ? this.generateSchemaTable(endpoint.responseSchema) : `<p class="text-muted">Schema: <code>${this.escapeHtml(typeName)}</code></p>`}
797
+ </div>
798
+ </div>`;
799
+ }
800
+ /**
801
+ * Generate a schema table row
802
+ */
803
+ generateSchemaTableRow(name, schema, required) {
804
+ const isRequired = required.includes(name);
805
+ const typeDisplay = this.getTypeDisplay(schema);
806
+ const isEnterpriseOnly = schema['x-enterprise-only'] === true;
807
+ let description = schema.description || '';
808
+ // Add enterprise-only badge to description if applicable
809
+ const enterpriseBadge = isEnterpriseOnly
810
+ ? '<span class="badge bg-purple enterprise-field-badge me-1" title="This field is only available in enterprise mode">Enterprise</span>'
811
+ : '';
812
+ // Add enum member descriptions if available
813
+ const enumMembers = schema['x-enum-members'];
814
+ if (enumMembers && enumMembers.length > 0) {
815
+ const memberDescriptions = enumMembers
816
+ .filter(m => m.description)
817
+ .map(m => `<code>${m.value}</code> (${this.escapeHtml(m.name)}) - ${this.escapeHtml(m.description)}`)
818
+ .join('<br>');
819
+ if (memberDescriptions) {
820
+ description = description
821
+ ? `${enterpriseBadge}${this.convertJsDocLinks(description)}<br><small class="text-muted mt-1 d-block"><strong>Values:</strong><br>${memberDescriptions}</small>`
822
+ : `${enterpriseBadge}<small class="text-muted"><strong>Values:</strong><br>${memberDescriptions}</small>`;
823
+ return `<tr${isEnterpriseOnly ? ' class="enterprise-field"' : ''}>
824
+ <td><code>${this.escapeHtml(name)}</code></td>
825
+ <td>${typeDisplay}</td>
826
+ <td>${isRequired ? '<span class="badge bg-danger">Required</span>' : '<span class="badge bg-secondary">Optional</span>'}</td>
827
+ <td>${description}</td>
828
+ </tr>`;
829
+ }
830
+ }
831
+ return `<tr${isEnterpriseOnly ? ' class="enterprise-field"' : ''}>
832
+ <td><code>${this.escapeHtml(name)}</code></td>
833
+ <td>${typeDisplay}</td>
834
+ <td>${isRequired ? '<span class="badge bg-danger">Required</span>' : '<span class="badge bg-secondary">Optional</span>'}</td>
835
+ <td>${enterpriseBadge}${description ? this.convertJsDocLinks(description) : ''}</td>
836
+ </tr>`;
837
+ }
838
+ /**
839
+ * Create a clickable schema link if the schema exists, otherwise return plain code
840
+ */
841
+ createSchemaLink(typeName) {
842
+ const slug = this.slugify(typeName);
843
+ // Check if this type exists in the schemas map
844
+ if (this.schemas.has(typeName)) {
845
+ return `<a href="#schema-${slug}" class="schema-link"><code>${this.escapeHtml(typeName)}</code></a>`;
846
+ }
847
+ // Return plain code if schema doesn't exist (prevents broken links)
848
+ return `<code>${this.escapeHtml(typeName)}</code>`;
849
+ }
850
+ /**
851
+ * Get type display HTML for a schema
852
+ */
853
+ getTypeDisplay(schema) {
854
+ if (schema.$ref) {
855
+ const refName = schema.$ref.replace('#/components/schemas/', '');
856
+ return this.createSchemaLink(refName);
857
+ }
858
+ if (schema.type === 'array' && schema.items) {
859
+ const itemTypeHtml = this.getTypeDisplay(schema.items);
860
+ // Extract the inner type text (removing code tags) for the array display
861
+ const innerType = itemTypeHtml.replace(/<\/?code>/g, '');
862
+ return `<code>Array&lt;</code>${itemTypeHtml}<code>&gt;</code>`;
863
+ }
864
+ if (schema.enum) {
865
+ // If we have enum members with names, show those instead of raw values
866
+ const enumMembers = schema['x-enum-members'];
867
+ if (enumMembers && enumMembers.length > 0) {
868
+ const memberDisplay = enumMembers.map(m => `${m.name}=${m.value}`).join(' | ');
869
+ return `<code>${this.escapeHtml(schema.type || 'string')}</code> <small class="text-muted">(${this.escapeHtml(memberDisplay)})</small>`;
870
+ }
871
+ const enumValues = schema.enum.slice(0, 3).map(v => typeof v === 'string' ? `"${v}"` : v).join(' | ');
872
+ const suffix = schema.enum.length > 3 ? ' | ...' : '';
873
+ return `<code>${this.escapeHtml(schema.type || 'string')}</code> <small class="text-muted">(${enumValues}${suffix})</small>`;
874
+ }
875
+ if (schema.oneOf && schema.oneOf.length > 0) {
876
+ return '<code>oneOf</code> <small class="text-muted">(union)</small>';
877
+ }
878
+ // Check if this might be a known schema type (not a primitive)
879
+ const typeName = schema.type || 'object';
880
+ const isPrimitive = [
881
+ 'string',
882
+ 'number',
883
+ 'integer',
884
+ 'boolean',
885
+ 'null',
886
+ 'object',
887
+ 'array',
888
+ ].includes(typeName);
889
+ let display;
890
+ if (isPrimitive) {
891
+ display = `<code>${this.escapeHtml(typeName)}</code>`;
892
+ }
893
+ else {
894
+ // Non-primitive type name - check if it's a known schema
895
+ display = this.createSchemaLink(typeName);
896
+ }
897
+ if (schema.format) {
898
+ display += ` <small class="text-muted">(${this.escapeHtml(schema.format)})</small>`;
899
+ }
900
+ if (schema.nullable) {
901
+ display += ' <small class="text-muted">| null</small>';
902
+ }
903
+ return display;
904
+ }
905
+ /**
906
+ * Generate simple type display for non-object types
907
+ */
908
+ generateSimpleTypeDisplay(schema) {
909
+ const typeDisplay = this.getTypeDisplay(schema);
910
+ return `<div class="simple-type-display p-2 bg-light rounded">
911
+ <span class="text-muted">Type:</span> ${typeDisplay}
912
+ ${schema.description ? `<br><span class="text-muted">Description:</span> ${this.escapeHtml(schema.description)}` : ''}
913
+ </div>`;
914
+ }
915
+ /**
916
+ * Generate custom styles
917
+ */
918
+ generateStyles() {
919
+ return `<style>
920
+ :root {
921
+ --sidebar-bg: #1e2530;
922
+ --sidebar-header-bg: #161b22;
923
+ --sidebar-text: #c9d1d9;
924
+ --sidebar-text-muted: #8b949e;
925
+ --sidebar-hover: rgba(255, 255, 255, 0.05);
926
+ --sidebar-active: rgba(13, 110, 253, 0.15);
927
+ --sidebar-active-border: #0d6efd;
928
+ --primary-accent: #0d6efd;
929
+ --content-bg: #ffffff;
930
+ --card-border: #e5e7eb;
931
+ --text-primary: #1f2937;
932
+ --text-secondary: #6b7280;
933
+ }
934
+
935
+ body {
936
+ background-color: #f9fafb;
937
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
938
+ }
939
+
940
+ /* Global table border colors */
941
+ .table,
942
+ .table > :not(caption) > * > * {
943
+ border-color: var(--card-border) !important;
944
+ }
945
+
946
+ .table-bordered,
947
+ .table-bordered > :not(caption) > * > * {
948
+ border-color: var(--card-border) !important;
949
+ }
950
+
951
+ .table tr,
952
+ .table thead tr,
953
+ .table tbody tr {
954
+ border-color: var(--card-border) !important;
955
+ }
956
+
957
+ /* Sidebar styles - Dark theme like Guidex */
958
+ .sidebar {
959
+ position: fixed;
960
+ top: 0;
961
+ bottom: 0;
962
+ left: 0;
963
+ z-index: 100;
964
+ padding: 0;
965
+ background-color: var(--sidebar-bg);
966
+ overflow-y: auto;
967
+ scrollbar-width: thin;
968
+ scrollbar-color: #3f4957 transparent;
969
+ }
970
+
971
+ .sidebar::-webkit-scrollbar {
972
+ width: 6px;
973
+ }
974
+
975
+ .sidebar::-webkit-scrollbar-track {
976
+ background: transparent;
977
+ }
978
+
979
+ .sidebar::-webkit-scrollbar-thumb {
980
+ background-color: #3f4957;
981
+ border-radius: 3px;
982
+ }
983
+
984
+ .sidebar-header {
985
+ background-color: var(--sidebar-header-bg);
986
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
987
+ padding: 1.25rem 1rem 1rem;
988
+ }
989
+
990
+ .sidebar-header a {
991
+ color: #ffffff !important;
992
+ }
993
+
994
+ .sidebar-header h4 {
995
+ font-weight: 700;
996
+ font-size: 1.1rem;
997
+ letter-spacing: -0.01em;
998
+ }
999
+
1000
+ .sidebar-header small {
1001
+ color: var(--sidebar-text-muted) !important;
1002
+ }
1003
+
1004
+ .sidebar .nav-link {
1005
+ color: var(--sidebar-text);
1006
+ }
1007
+
1008
+ .sidebar-group-header {
1009
+ font-weight: 600;
1010
+ color: var(--sidebar-text);
1011
+ display: flex;
1012
+ align-items: center;
1013
+ padding: 0.6rem 1rem;
1014
+ font-size: 0.9rem;
1015
+ transition: background-color 0.15s;
1016
+ }
1017
+
1018
+ .sidebar-group-header:hover {
1019
+ background-color: var(--sidebar-hover);
1020
+ color: #ffffff;
1021
+ }
1022
+
1023
+ .sidebar-group-header .badge {
1024
+ background-color: rgba(255, 255, 255, 0.1) !important;
1025
+ color: var(--sidebar-text-muted);
1026
+ font-weight: 500;
1027
+ }
1028
+
1029
+ .sidebar-submenu {
1030
+ padding-left: 0.5rem;
1031
+ margin-bottom: 0.5rem;
1032
+ }
1033
+
1034
+ .sidebar-endpoint {
1035
+ font-size: 0.82rem;
1036
+ padding: 0.4rem 0.75rem;
1037
+ display: flex;
1038
+ align-items: center;
1039
+ gap: 0.5rem;
1040
+ color: var(--sidebar-text-muted);
1041
+ border-left: 2px solid transparent;
1042
+ margin-left: 0.5rem;
1043
+ transition: all 0.15s;
1044
+ }
1045
+
1046
+ .sidebar-endpoint:hover {
1047
+ background-color: var(--sidebar-hover);
1048
+ color: var(--sidebar-text);
1049
+ }
1050
+
1051
+ .sidebar-endpoint.active {
1052
+ background-color: var(--sidebar-active);
1053
+ color: #ffffff;
1054
+ border-left-color: var(--sidebar-active-border);
1055
+ font-weight: 500;
1056
+ }
1057
+
1058
+ .sidebar-intro-link {
1059
+ font-weight: 500;
1060
+ color: var(--sidebar-text);
1061
+ padding: 0.6rem 1rem;
1062
+ margin-bottom: 0.5rem;
1063
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1064
+ transition: all 0.15s;
1065
+ }
1066
+
1067
+ .sidebar-intro-link:hover {
1068
+ background-color: var(--sidebar-hover);
1069
+ color: #ffffff;
1070
+ }
1071
+
1072
+ .sidebar-intro-link.active {
1073
+ background-color: var(--sidebar-active);
1074
+ color: #ffffff;
1075
+ border-left: 3px solid var(--sidebar-active-border);
1076
+ }
1077
+
1078
+ /* Method badges */
1079
+ .method-badge {
1080
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
1081
+ font-weight: 600;
1082
+ min-width: 60px;
1083
+ text-align: center;
1084
+ font-size: 0.75rem;
1085
+ letter-spacing: 0.02em;
1086
+ }
1087
+
1088
+ .method-badge-sm {
1089
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
1090
+ font-size: 0.6rem;
1091
+ font-weight: 600;
1092
+ min-width: 28px;
1093
+ text-align: center;
1094
+ padding: 0.1rem 0.25rem;
1095
+ letter-spacing: 0.02em;
1096
+ }
1097
+
1098
+ .bg-purple {
1099
+ background-color: #6f42c1 !important;
1100
+ }
1101
+
1102
+ /* Enterprise badges */
1103
+ .enterprise-badge {
1104
+ background-color: #6f42c1 !important;
1105
+ font-weight: 600;
1106
+ }
1107
+
1108
+ .enterprise-field-badge {
1109
+ font-size: 0.7rem;
1110
+ vertical-align: middle;
1111
+ }
1112
+
1113
+ .enterprise-field {
1114
+ background-color: rgba(111, 66, 193, 0.05);
1115
+ }
1116
+
1117
+ .enterprise-field td:first-child code {
1118
+ border-left: 2px solid #6f42c1;
1119
+ padding-left: 0.5rem;
1120
+ }
1121
+
1122
+ /* Main content */
1123
+ .main-content {
1124
+ margin-left: auto;
1125
+ padding-top: 1rem;
1126
+ background-color: var(--content-bg);
1127
+ min-height: 100vh;
1128
+ }
1129
+
1130
+ /* Tag sections */
1131
+ .tag-section {
1132
+ scroll-margin-top: 10px;
1133
+ }
1134
+
1135
+ .tag-header {
1136
+ font-weight: 700;
1137
+ font-size: 1.5rem;
1138
+ color: var(--text-primary);
1139
+ border-bottom: 2px solid var(--card-border);
1140
+ padding-bottom: 0.75rem;
1141
+ margin-bottom: 1.5rem;
1142
+ }
1143
+
1144
+ /* Endpoint cards */
1145
+ .endpoint-card {
1146
+ border: 1px solid var(--card-border);
1147
+ border-left: 4px solid var(--card-border);
1148
+ border-radius: 8px;
1149
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
1150
+ transition: box-shadow 0.15s;
1151
+ }
1152
+
1153
+ .endpoint-card:hover {
1154
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
1155
+ }
1156
+
1157
+ .endpoint-card:has(.badge.bg-success:first-child) {
1158
+ border-left-color: #198754;
1159
+ }
1160
+
1161
+ .endpoint-card:has(.badge.bg-primary:first-child) {
1162
+ border-left-color: #0d6efd;
1163
+ }
1164
+
1165
+ .endpoint-card:has(.badge.bg-danger:first-child) {
1166
+ border-left-color: #dc3545;
1167
+ }
1168
+
1169
+ .endpoint-card:has(.badge.bg-warning:first-child) {
1170
+ border-left-color: #fd7e14;
1171
+ }
1172
+
1173
+ .endpoint-card:has(.badge.bg-purple:first-child) {
1174
+ border-left-color: #6f42c1;
1175
+ }
1176
+
1177
+ .endpoint-path {
1178
+ font-size: 0.95rem;
1179
+ color: var(--text-primary);
1180
+ }
1181
+
1182
+ .endpoint-path-sm {
1183
+ font-size: 0.8rem;
1184
+ color: var(--sidebar-text-muted);
1185
+ white-space: nowrap;
1186
+ overflow: hidden;
1187
+ text-overflow: ellipsis;
1188
+ }
1189
+
1190
+ /* API Section Cards (for index page overview) */
1191
+ .api-section-card {
1192
+ border: 1px solid var(--card-border);
1193
+ border-radius: 12px;
1194
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
1195
+ transition: box-shadow 0.15s;
1196
+ }
1197
+
1198
+ .api-section-card:hover {
1199
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
1200
+ }
1201
+
1202
+ .api-section-card .card-header {
1203
+ background-color: #f8fafc;
1204
+ border-bottom: 1px solid var(--card-border);
1205
+ padding: 1rem 1.25rem;
1206
+ }
1207
+
1208
+ .api-section-card .card-header h5 {
1209
+ font-weight: 600;
1210
+ color: var(--text-primary);
1211
+ margin-bottom: 0.25rem;
1212
+ }
1213
+
1214
+ .api-section-card .card-body {
1215
+ padding: 1rem 1.25rem;
1216
+ }
1217
+
1218
+ .endpoint-links {
1219
+ display: flex;
1220
+ flex-direction: column;
1221
+ gap: 0.5rem;
1222
+ }
1223
+
1224
+ .endpoint-link {
1225
+ display: flex;
1226
+ align-items: center;
1227
+ gap: 0.75rem;
1228
+ padding: 0.5rem 0.75rem;
1229
+ border-radius: 6px;
1230
+ text-decoration: none;
1231
+ color: var(--text-primary);
1232
+ transition: background-color 0.15s;
1233
+ }
1234
+
1235
+ .endpoint-link:hover {
1236
+ background-color: #f1f5f9;
1237
+ color: var(--primary-accent);
1238
+ }
1239
+
1240
+ .endpoint-link .method-badges {
1241
+ display: flex;
1242
+ gap: 0.25rem;
1243
+ min-width: 70px;
1244
+ }
1245
+
1246
+ .endpoint-link-path {
1247
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
1248
+ font-size: 0.85rem;
1249
+ }
1250
+
1251
+ /* Schema tables */
1252
+ .schema-table {
1253
+ font-size: 0.875rem;
1254
+ border-radius: 8px;
1255
+ overflow: hidden;
1256
+ border-color: var(--card-border) !important;
1257
+ }
1258
+
1259
+ .schema-table tr {
1260
+ border-color: var(--card-border) !important;
1261
+ }
1262
+
1263
+ .schema-table th {
1264
+ font-weight: 600;
1265
+ background-color: #f8fafc;
1266
+ border-color: var(--card-border) !important;
1267
+ }
1268
+
1269
+ .schema-table td, .schema-table th {
1270
+ padding: 0.75rem 1rem;
1271
+ border-color: var(--card-border) !important;
1272
+ }
1273
+
1274
+ .schema-table td {
1275
+ border-color: var(--card-border) !important;
1276
+ }
1277
+
1278
+ .schema-link {
1279
+ text-decoration: none;
1280
+ color: var(--primary-accent);
1281
+ }
1282
+
1283
+ .schema-link:hover {
1284
+ text-decoration: underline;
1285
+ }
1286
+
1287
+ /* Schema definitions section */
1288
+ .schema-definition {
1289
+ scroll-margin-top: 10px;
1290
+ border-radius: 8px;
1291
+ overflow: hidden;
1292
+ }
1293
+
1294
+ .schema-definition .card-header {
1295
+ background-color: #f8fafc;
1296
+ border-bottom: 1px solid var(--card-border);
1297
+ }
1298
+
1299
+ .schema-definition .card-header code {
1300
+ font-size: 0.95rem;
1301
+ color: var(--primary-accent);
1302
+ }
1303
+
1304
+ .schema-definition .card-body {
1305
+ padding: 0;
1306
+ }
1307
+
1308
+ .schema-definition .schema-table {
1309
+ margin-bottom: 0;
1310
+ border: 0;
1311
+ border-radius: 0;
1312
+ }
1313
+
1314
+ .schema-definition .schema-table thead th:first-child,
1315
+ .schema-definition .schema-table tbody td:first-child {
1316
+ padding-left: 1rem;
1317
+ }
1318
+
1319
+ .schema-definition.highlight {
1320
+ animation: highlightFade 2s ease-out;
1321
+ }
1322
+
1323
+ @keyframes highlightFade {
1324
+ 0% {
1325
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.4);
1326
+ }
1327
+ 100% {
1328
+ box-shadow: none;
1329
+ }
1330
+ }
1331
+
1332
+ /* Section headers */
1333
+ .section-header {
1334
+ color: var(--text-secondary);
1335
+ border-bottom: 1px solid var(--card-border);
1336
+ padding-bottom: 0.5rem;
1337
+ margin-bottom: 0.75rem;
1338
+ font-weight: 600;
1339
+ }
1340
+
1341
+ /* Intro content */
1342
+ .intro-content {
1343
+ max-width: 900px;
1344
+ }
1345
+
1346
+ .intro-content h4 {
1347
+ font-weight: 600;
1348
+ color: var(--text-primary);
1349
+ border-bottom: 1px solid var(--card-border);
1350
+ padding-bottom: 0.75rem;
1351
+ margin-top: 2rem;
1352
+ }
1353
+
1354
+ .intro-content h5 {
1355
+ color: var(--text-secondary);
1356
+ font-weight: 500;
1357
+ }
1358
+
1359
+ .intro-content .table {
1360
+ border-radius: 8px;
1361
+ overflow: hidden;
1362
+ border: 1px solid var(--card-border);
1363
+ border-color: var(--card-border) !important;
1364
+ }
1365
+
1366
+ .intro-content .table tr {
1367
+ border-color: var(--card-border) !important;
1368
+ }
1369
+
1370
+ .intro-content .table th,
1371
+ .intro-content .table td {
1372
+ border-color: var(--card-border) !important;
1373
+ }
1374
+
1375
+ .intro-content .table th {
1376
+ background-color: #f8fafc;
1377
+ }
1378
+
1379
+ /* Responsive adjustments */
1380
+ @media (max-width: 767.98px) {
1381
+ .sidebar {
1382
+ position: relative;
1383
+ height: auto;
1384
+ }
1385
+
1386
+ .main-content {
1387
+ margin-left: 0;
1388
+ }
1389
+ }
1390
+
1391
+ /* Servers list */
1392
+ .servers-list code {
1393
+ background-color: #f1f5f9;
1394
+ padding: 2px 6px;
1395
+ border-radius: 4px;
1396
+ }
1397
+
1398
+ /* Copy button */
1399
+ .copy-btn {
1400
+ cursor: pointer;
1401
+ opacity: 0.7;
1402
+ }
1403
+
1404
+ .copy-btn:hover {
1405
+ opacity: 1;
1406
+ }
1407
+
1408
+ /* Code Snippets Panel */
1409
+ .code-snippets-panel {
1410
+ background-color: #1e1e1e;
1411
+ border-radius: 8px;
1412
+ overflow: hidden;
1413
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
1414
+ }
1415
+
1416
+ .snippets-header {
1417
+ background-color: #2d2d2d;
1418
+ padding: 0.75rem 1rem;
1419
+ border-bottom: 1px solid #404040;
1420
+ }
1421
+
1422
+ .snippets-title {
1423
+ color: #e0e0e0;
1424
+ font-weight: 600;
1425
+ font-size: 0.9rem;
1426
+ }
1427
+
1428
+ /* Snippet tabs */
1429
+ .snippet-tabs {
1430
+ background-color: #252526;
1431
+ padding: 0.5rem 0.75rem 0;
1432
+ border-bottom: 1px solid #404040;
1433
+ gap: 0.25rem;
1434
+ flex-wrap: wrap;
1435
+ }
1436
+
1437
+ .snippet-tabs .nav-link {
1438
+ color: #9d9d9d;
1439
+ background-color: transparent;
1440
+ border: none;
1441
+ border-radius: 4px 4px 0 0;
1442
+ padding: 0.4rem 0.75rem;
1443
+ font-size: 0.8rem;
1444
+ font-weight: 500;
1445
+ transition: all 0.15s ease-in-out;
1446
+ }
1447
+
1448
+ .snippet-tabs .nav-link:hover {
1449
+ color: #e0e0e0;
1450
+ background-color: #3c3c3c;
1451
+ }
1452
+
1453
+ .snippet-tabs .nav-link.active {
1454
+ color: #ffffff;
1455
+ background-color: #1e1e1e;
1456
+ border-bottom: 2px solid #007acc;
1457
+ }
1458
+
1459
+ /* Snippet content */
1460
+ .snippet-content {
1461
+ background-color: #1e1e1e;
1462
+ }
1463
+
1464
+ .snippet-wrapper {
1465
+ position: relative;
1466
+ padding: 0;
1467
+ }
1468
+
1469
+ .snippet-code {
1470
+ background-color: #1e1e1e;
1471
+ color: #d4d4d4;
1472
+ margin: 0;
1473
+ padding: 1rem;
1474
+ border-radius: 0;
1475
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
1476
+ font-size: 0.85rem;
1477
+ line-height: 1.5;
1478
+ overflow-x: auto;
1479
+ max-height: 500px;
1480
+ }
1481
+
1482
+ .snippet-code code {
1483
+ color: inherit;
1484
+ background: none;
1485
+ padding: 0;
1486
+ font-size: inherit;
1487
+ white-space: pre;
1488
+ }
1489
+
1490
+ /* Copy snippet button */
1491
+ .copy-snippet-btn {
1492
+ position: absolute;
1493
+ top: 0.5rem;
1494
+ right: 0.5rem;
1495
+ background-color: #3c3c3c;
1496
+ border: 1px solid #555;
1497
+ color: #d4d4d4;
1498
+ padding: 0.3rem 0.5rem;
1499
+ font-size: 0.75rem;
1500
+ border-radius: 4px;
1501
+ opacity: 0;
1502
+ transition: opacity 0.2s ease-in-out, background-color 0.15s ease-in-out;
1503
+ z-index: 10;
1504
+ }
1505
+
1506
+ .snippet-wrapper:hover .copy-snippet-btn {
1507
+ opacity: 1;
1508
+ }
1509
+
1510
+ .copy-snippet-btn:hover {
1511
+ background-color: #505050;
1512
+ color: #ffffff;
1513
+ }
1514
+
1515
+ .copy-snippet-btn .copy-icon,
1516
+ .copy-snippet-btn .copied-icon {
1517
+ font-size: 0.85rem;
1518
+ }
1519
+
1520
+ .copy-snippet-btn.copied {
1521
+ background-color: #28a745;
1522
+ border-color: #28a745;
1523
+ }
1524
+
1525
+ /* Syntax highlighting approximation (basic) */
1526
+ .snippet-code .keyword { color: #569cd6; }
1527
+ .snippet-code .string { color: #ce9178; }
1528
+ .snippet-code .comment { color: #6a9955; }
1529
+ .snippet-code .number { color: #b5cea8; }
1530
+ .snippet-code .function { color: #dcdcaa; }
1531
+
1532
+ /* Code panel in endpoint cards */
1533
+ .endpoint-card .code-snippets-panel,
1534
+ .card .code-snippets-panel {
1535
+ border-radius: 6px;
1536
+ }
1537
+
1538
+ /* Two-column layout for endpoint pages */
1539
+ .endpoint-page-content {
1540
+ display: grid;
1541
+ grid-template-columns: 1fr 400px;
1542
+ gap: 2rem;
1543
+ align-items: start;
1544
+ }
1545
+
1546
+ .endpoint-page-content .endpoint-details {
1547
+ min-width: 0;
1548
+ }
1549
+
1550
+ .endpoint-page-content .code-panel {
1551
+ position: sticky;
1552
+ top: 1rem;
1553
+ }
1554
+
1555
+ .endpoint-page-content .code-snippets-panel {
1556
+ margin-top: 0;
1557
+ }
1558
+
1559
+ @media (max-width: 1199.98px) {
1560
+ .endpoint-page-content {
1561
+ grid-template-columns: 1fr;
1562
+ }
1563
+
1564
+ .endpoint-page-content .code-panel {
1565
+ position: relative;
1566
+ top: 0;
1567
+ }
1568
+ }
1569
+
1570
+ /* Endpoint card two-column layout (legacy support) */
1571
+ .card-body > .row {
1572
+ --bs-gutter-x: 1.5rem;
1573
+ }
1574
+
1575
+ .card-body > .row > .col-lg-7 {
1576
+ padding-right: 1rem;
1577
+ }
1578
+
1579
+ .card-body > .row > .col-lg-5 .code-snippets-panel {
1580
+ margin-top: 0;
1581
+ position: sticky;
1582
+ top: 1rem;
1583
+ }
1584
+
1585
+ @media (max-width: 991.98px) {
1586
+ .card-body > .row > .col-lg-7 {
1587
+ padding-right: calc(var(--bs-gutter-x) * 0.5);
1588
+ }
1589
+
1590
+ .card-body > .row > .col-lg-5 .code-snippets-panel {
1591
+ margin-top: 1.5rem;
1592
+ position: relative;
1593
+ }
1594
+ }
1595
+
1596
+ /* Responsive adjustments for code panel */
1597
+ @media (max-width: 575.98px) {
1598
+ .snippet-tabs .nav-link {
1599
+ padding: 0.3rem 0.5rem;
1600
+ font-size: 0.7rem;
1601
+ }
1602
+
1603
+ .snippet-code {
1604
+ font-size: 0.75rem;
1605
+ padding: 0.75rem;
1606
+ }
1607
+
1608
+ .copy-snippet-btn {
1609
+ opacity: 1;
1610
+ }
1611
+ }
1612
+
1613
+ /* Breadcrumb styling */
1614
+ .breadcrumb {
1615
+ background-color: transparent;
1616
+ padding: 0;
1617
+ margin-bottom: 0;
1618
+ font-size: 0.875rem;
1619
+ }
1620
+
1621
+ .breadcrumb-item a {
1622
+ color: var(--text-secondary);
1623
+ text-decoration: none;
1624
+ }
1625
+
1626
+ .breadcrumb-item a:hover {
1627
+ color: var(--primary-accent);
1628
+ }
1629
+
1630
+ .breadcrumb-item.active {
1631
+ color: var(--text-primary);
1632
+ }
1633
+
1634
+ /* Card header improvements */
1635
+ .card-header {
1636
+ background-color: #f8fafc;
1637
+ border-bottom: 1px solid var(--card-border);
1638
+ }
1639
+
1640
+ /* Alert styling */
1641
+ .alert-info {
1642
+ background-color: #eff6ff;
1643
+ border-color: #bfdbfe;
1644
+ color: #1e40af;
1645
+ }
1646
+
1647
+ .alert-info code {
1648
+ color: #1e40af;
1649
+ }
1650
+ </style>`;
1651
+ }
1652
+ /**
1653
+ * Generate JavaScript for interactive features
1654
+ */
1655
+ generateScripts() {
1656
+ return `<script>
1657
+ // Smooth scrolling for sidebar links that point to anchors
1658
+ document.querySelectorAll('.sidebar-endpoint[href^="#"], .sidebar-intro-link[href*="#"]').forEach(link => {
1659
+ link.addEventListener('click', function(e) {
1660
+ const href = this.getAttribute('href');
1661
+ if (href.startsWith('#')) {
1662
+ e.preventDefault();
1663
+ const targetId = href.slice(1);
1664
+ const target = document.getElementById(targetId);
1665
+ if (target) {
1666
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1667
+ history.pushState(null, null, '#' + targetId);
1668
+ }
1669
+ }
1670
+ });
1671
+ });
1672
+
1673
+ // Smooth scrolling for schema links within the page
1674
+ document.querySelectorAll('.schema-link[href^="#schema-"]').forEach(link => {
1675
+ link.addEventListener('click', function(e) {
1676
+ e.preventDefault();
1677
+ const href = this.getAttribute('href');
1678
+ const targetId = href.slice(1);
1679
+ const target = document.getElementById(targetId);
1680
+ if (target) {
1681
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1682
+ history.pushState(null, null, href);
1683
+ // Flash effect to highlight the target
1684
+ target.classList.add('highlight');
1685
+ setTimeout(() => target.classList.remove('highlight'), 2000);
1686
+ }
1687
+ });
1688
+ });
1689
+
1690
+ // Highlight current endpoint on scroll (only on index page)
1691
+ const endpoints = document.querySelectorAll('.endpoint-card');
1692
+ const sidebarLinks = document.querySelectorAll('.sidebar-endpoint');
1693
+
1694
+ if (endpoints.length > 0 && sidebarLinks.length > 0) {
1695
+ const observer = new IntersectionObserver((entries) => {
1696
+ entries.forEach(entry => {
1697
+ if (entry.isIntersecting) {
1698
+ const id = entry.target.id;
1699
+ sidebarLinks.forEach(link => {
1700
+ link.classList.remove('active');
1701
+ if (link.getAttribute('href') === '#' + id) {
1702
+ link.classList.add('active');
1703
+ }
1704
+ });
1705
+ }
1706
+ });
1707
+ }, { threshold: 0.5 });
1708
+
1709
+ endpoints.forEach(endpoint => observer.observe(endpoint));
1710
+ }
1711
+
1712
+ // Copy code snippet functionality
1713
+ document.querySelectorAll('.copy-snippet-btn').forEach(btn => {
1714
+ btn.addEventListener('click', async function() {
1715
+ const snippetId = this.getAttribute('data-snippet-id');
1716
+ const codeEl = document.getElementById(snippetId);
1717
+ if (!codeEl) return;
1718
+
1719
+ const code = codeEl.textContent || '';
1720
+
1721
+ try {
1722
+ await navigator.clipboard.writeText(code);
1723
+
1724
+ // Show copied state
1725
+ const copyIcon = this.querySelector('.copy-icon');
1726
+ const copiedIcon = this.querySelector('.copied-icon');
1727
+
1728
+ if (copyIcon) copyIcon.classList.add('d-none');
1729
+ if (copiedIcon) copiedIcon.classList.remove('d-none');
1730
+ this.classList.add('copied');
1731
+
1732
+ // Reset after 2 seconds
1733
+ setTimeout(() => {
1734
+ if (copyIcon) copyIcon.classList.remove('d-none');
1735
+ if (copiedIcon) copiedIcon.classList.add('d-none');
1736
+ this.classList.remove('copied');
1737
+ }, 2000);
1738
+ } catch (err) {
1739
+ // Fallback for older browsers
1740
+ const textarea = document.createElement('textarea');
1741
+ textarea.value = code;
1742
+ textarea.style.position = 'fixed';
1743
+ textarea.style.opacity = '0';
1744
+ document.body.appendChild(textarea);
1745
+ textarea.select();
1746
+ document.execCommand('copy');
1747
+ document.body.removeChild(textarea);
1748
+
1749
+ // Show copied state
1750
+ this.classList.add('copied');
1751
+ setTimeout(() => this.classList.remove('copied'), 2000);
1752
+ }
1753
+ });
1754
+ });
1755
+ </script>`;
1756
+ }
1757
+ /**
1758
+ * Escape HTML special characters
1759
+ */
1760
+ escapeHtml(text) {
1761
+ const htmlEntities = {
1762
+ '&': '&amp;',
1763
+ '<': '&lt;',
1764
+ '>': '&gt;',
1765
+ '"': '&quot;',
1766
+ '\'': '&#39;',
1767
+ };
1768
+ return text.replace(/[&<>"']/g, char => htmlEntities[char] || char);
1769
+ }
1770
+ /**
1771
+ * Convert endpoint description with Markdown (code blocks, newlines) to HTML.
1772
+ * Preserves ``` code blocks as <pre><code> and newlines as <br> for proper Swagger-style display.
1773
+ * Returns HTML that can replace the description container (may contain multiple elements).
1774
+ */
1775
+ convertDescriptionToHtml(text) {
1776
+ if (!text || !text.trim()) {
1777
+ return '';
1778
+ }
1779
+ const codeBlocks = [];
1780
+ // eslint-disable-next-line regexp/no-useless-quantifier
1781
+ const codeBlockPattern = /```(\w*)?\s*\n([\s\S]*?)```/g;
1782
+ // Extract code blocks and replace with placeholders
1783
+ const textWithPlaceholders = text.replace(codeBlockPattern, (_, lang, code) => {
1784
+ const index = codeBlocks.length;
1785
+ codeBlocks.push(`<pre class="border rounded p-3 mt-2 mb-2"><code class="language-${lang || 'json'}">${this.escapeHtml(code.trim())}</code></pre>`);
1786
+ return `__CODE_BLOCK_${index}__`;
1787
+ });
1788
+ // Split by double newlines for paragraphs
1789
+ // eslint-disable-next-line regexp/optimal-quantifier-concatenation
1790
+ const paragraphs = textWithPlaceholders.split(/\n\n+/).filter(p => p.trim());
1791
+ const segments = [];
1792
+ for (const para of paragraphs) {
1793
+ const trimmed = para.trim();
1794
+ // Split by code block placeholders to handle mixed content
1795
+ const placeholderRegex = /(__CODE_BLOCK_(\d+)__)/g;
1796
+ const parts = trimmed.split(placeholderRegex);
1797
+ for (let i = 0; i < parts.length; i++) {
1798
+ const part = parts[i];
1799
+ const placeholderMatch = part.match(/^__CODE_BLOCK_(\d+)__$/);
1800
+ if (placeholderMatch) {
1801
+ segments.push(codeBlocks[Number.parseInt(placeholderMatch[1], 10)]);
1802
+ }
1803
+ else if (part.trim()) {
1804
+ const processed = this.convertJsDocLinks(part.trim());
1805
+ segments.push(`<p class="card-text">${processed.replace(/\n/g, '<br>\n')}</p>`);
1806
+ }
1807
+ }
1808
+ }
1809
+ return segments.join('\n');
1810
+ }
1811
+ /**
1812
+ * Convert JSDoc {@link} tags to HTML anchor elements
1813
+ * Handles both {@link TypeName} and {@link TypeName|display text} formats
1814
+ * Also escapes HTML in the rest of the text to prevent XSS
1815
+ *
1816
+ * @example
1817
+ * // {@link UserClientsideModel} -> <a href="#schema-userclientsidemodel" class="schema-link">UserClientsideModel</a>
1818
+ * // {@link Cart|shopping cart} -> <a href="#schema-cart" class="schema-link">shopping cart</a>
1819
+ */
1820
+ convertJsDocLinks(text) {
1821
+ // Pattern to match {@link TypeName} and {@link TypeName|display text}
1822
+ const linkPattern = /\{@link\s+([^|}]+)(?:\|([^}]+))?\}/g;
1823
+ // Find all {@link} matches and store them with placeholders
1824
+ const links = [];
1825
+ let counter = 0;
1826
+ // Replace {@link} tags with unique placeholders and generate HTML
1827
+ const textWithPlaceholders = text.replace(linkPattern, (_, typeName, displayText) => {
1828
+ const name = typeName.trim();
1829
+ const display = displayText?.trim() || name;
1830
+ const slug = this.slugify(name);
1831
+ const placeholder = `__JSDOC_LINK_${counter++}__`;
1832
+ links.push({
1833
+ placeholder,
1834
+ html: `<a href="#schema-${slug}" class="schema-link">${this.escapeHtml(display)}</a>`,
1835
+ });
1836
+ return placeholder;
1837
+ });
1838
+ // Escape HTML in the text (placeholders will be escaped too, but they're unique)
1839
+ let escaped = this.escapeHtml(textWithPlaceholders);
1840
+ // Replace placeholders with actual anchor HTML
1841
+ for (const link of links) {
1842
+ escaped = escaped.replace(link.placeholder, link.html);
1843
+ }
1844
+ return escaped;
1845
+ }
1846
+ /**
1847
+ * Convert text to URL-safe slug
1848
+ */
1849
+ slugify(text) {
1850
+ return text
1851
+ .toLowerCase()
1852
+ .replace(/[^a-z0-9]+/g, '-')
1853
+ .replace(/^-+|-+$/g, '');
1854
+ }
1855
+ /**
1856
+ * Capitalize first letter
1857
+ */
1858
+ capitalize(text) {
1859
+ return text.charAt(0).toUpperCase() + text.slice(1);
1860
+ }
1861
+ /**
1862
+ * Generate operation ID from path and method
1863
+ */
1864
+ generateOperationId(path, method) {
1865
+ const pathPart = path
1866
+ .replace(/^\/api\/(shop|admin)\//, '')
1867
+ .replace(/[^a-z0-9]+/gi, '_')
1868
+ .replace(/^_+|_+$/g, '');
1869
+ return `${method.toLowerCase()}_${pathPart}`;
1870
+ }
1871
+ /**
1872
+ * Code snippet language configuration
1873
+ */
1874
+ snippetLanguages = [
1875
+ { id: 'nodejs', name: 'Node.js', langClass: 'javascript' },
1876
+ { id: 'shell', name: 'Shell', langClass: 'bash' },
1877
+ { id: 'php', name: 'PHP', langClass: 'php' },
1878
+ { id: 'rust', name: 'Rust', langClass: 'rust' },
1879
+ { id: 'csharp', name: 'C#', langClass: 'csharp' },
1880
+ { id: 'python', name: 'Python', langClass: 'python' },
1881
+ { id: 'kotlin', name: 'Kotlin', langClass: 'kotlin' },
1882
+ { id: 'swift', name: 'Swift', langClass: 'swift' },
1883
+ { id: 'java', name: 'Java', langClass: 'java' },
1884
+ ];
1885
+ /**
1886
+ * Generate code snippets panel with examples in 9 programming languages
1887
+ */
1888
+ generateCodeSnippets(endpoint) {
1889
+ const operationId = endpoint.operationId || this.generateOperationId(endpoint.path, endpoint.method);
1890
+ const tabsId = `snippets-${this.slugify(operationId)}`;
1891
+ // Generate tab navigation
1892
+ const tabNav = this.snippetLanguages.map((lang, index) => {
1893
+ const isActive = index === 0 ? ' active' : '';
1894
+ const ariaSelected = index === 0 ? 'true' : 'false';
1895
+ return `<li class="nav-item" role="presentation">
1896
+ <button class="nav-link${isActive}" id="${tabsId}-${lang.id}-tab" data-bs-toggle="pill" data-bs-target="#${tabsId}-${lang.id}" type="button" role="tab" aria-controls="${tabsId}-${lang.id}" aria-selected="${ariaSelected}">${this.escapeHtml(lang.name)}</button>
1897
+ </li>`;
1898
+ }).join('\n');
1899
+ // Generate tab content panels
1900
+ const tabContent = this.snippetLanguages.map((lang, index) => {
1901
+ const isActive = index === 0 ? ' show active' : '';
1902
+ const snippet = this.generateSnippetForLanguage(lang.id, endpoint);
1903
+ return `<div class="tab-pane fade${isActive}" id="${tabsId}-${lang.id}" role="tabpanel" aria-labelledby="${tabsId}-${lang.id}-tab" tabindex="0">
1904
+ <div class="snippet-wrapper">
1905
+ <button class="btn btn-sm btn-outline-light copy-snippet-btn" title="Copy to clipboard" data-snippet-id="${tabsId}-${lang.id}-code">
1906
+ <span class="copy-icon">📋</span>
1907
+ <span class="copied-icon d-none">✓</span>
1908
+ </button>
1909
+ <pre class="snippet-code"><code id="${tabsId}-${lang.id}-code" class="language-${lang.langClass}">${this.escapeHtml(snippet)}</code></pre>
1910
+ </div>
1911
+ </div>`;
1912
+ }).join('\n');
1913
+ return `<div class="code-snippets-panel">
1914
+ <div class="snippets-header">
1915
+ <span class="snippets-title">Code Examples</span>
1916
+ </div>
1917
+ <ul class="nav nav-pills snippet-tabs" id="${tabsId}-tabs" role="tablist">
1918
+ ${tabNav}
1919
+ </ul>
1920
+ <div class="tab-content snippet-content" id="${tabsId}-content">
1921
+ ${tabContent}
1922
+ </div>
1923
+ </div>`;
1924
+ }
1925
+ /**
1926
+ * Generate a code snippet for a specific language
1927
+ */
1928
+ generateSnippetForLanguage(langId, endpoint) {
1929
+ switch (langId) {
1930
+ case 'nodejs':
1931
+ return this.generateNodeJsSnippet(endpoint);
1932
+ case 'shell':
1933
+ return this.generateShellSnippet(endpoint);
1934
+ case 'php':
1935
+ return this.generatePhpSnippet(endpoint);
1936
+ case 'rust':
1937
+ return this.generateRustSnippet(endpoint);
1938
+ case 'csharp':
1939
+ return this.generateCSharpSnippet(endpoint);
1940
+ case 'python':
1941
+ return this.generatePythonSnippet(endpoint);
1942
+ case 'kotlin':
1943
+ return this.generateKotlinSnippet(endpoint);
1944
+ case 'swift':
1945
+ return this.generateSwiftSnippet(endpoint);
1946
+ case 'java':
1947
+ return this.generateJavaSnippet(endpoint);
1948
+ default:
1949
+ return '// Not implemented';
1950
+ }
1951
+ }
1952
+ /**
1953
+ * Get sample request body based on endpoint schema
1954
+ */
1955
+ getSampleRequestBody(endpoint) {
1956
+ if (!endpoint.requestSchema || endpoint.method === 'GET' || endpoint.method === 'DELETE') {
1957
+ return null;
1958
+ }
1959
+ // Generate a sample object based on schema properties
1960
+ if (endpoint.requestSchema.properties) {
1961
+ const sample = {};
1962
+ for (const [key, prop,] of Object.entries(endpoint.requestSchema.properties)) {
1963
+ sample[key] = this.getSampleValue(prop);
1964
+ }
1965
+ return sample;
1966
+ }
1967
+ return { example: 'data' };
1968
+ }
1969
+ /**
1970
+ * Get a sample value for a schema property
1971
+ */
1972
+ getSampleValue(schema) {
1973
+ if (schema.example !== undefined) {
1974
+ return schema.example;
1975
+ }
1976
+ if (schema.enum && schema.enum.length > 0) {
1977
+ return schema.enum[0];
1978
+ }
1979
+ switch (schema.type) {
1980
+ case 'string':
1981
+ if (schema.format === 'email') {
1982
+ return 'user@example.com';
1983
+ }
1984
+ if (schema.format === 'date-time') {
1985
+ return '2024-01-01T00:00:00Z';
1986
+ }
1987
+ if (schema.format === 'uuid') {
1988
+ return '550e8400-e29b-41d4-a716-446655440000';
1989
+ }
1990
+ return 'string';
1991
+ case 'number':
1992
+ case 'integer':
1993
+ return 0;
1994
+ case 'boolean':
1995
+ return true;
1996
+ case 'array':
1997
+ return [];
1998
+ case 'object':
1999
+ return {};
2000
+ default:
2001
+ return null;
2002
+ }
2003
+ }
2004
+ /**
2005
+ * Get the API base URL from ApiDocConfig
2006
+ */
2007
+ getApiBaseUrl() {
2008
+ return ApiDocConfig.getCurrentServerUrl();
2009
+ }
2010
+ /**
2011
+ * Generate Node.js (native fetch) code snippet
2012
+ */
2013
+ generateNodeJsSnippet(endpoint) {
2014
+ const body = this.getSampleRequestBody(endpoint);
2015
+ const hasBody = body !== null;
2016
+ return `const response = await fetch('${this.getApiBaseUrl()}${endpoint.path}', {
2017
+ method: '${endpoint.method}',
2018
+ headers: {
2019
+ 'Content-Type': 'application/json',${endpoint.auth?.hasAuth
2020
+ ? `
2021
+ 'Authorization': 'Bearer YOUR_TOKEN',`
2022
+ : ''}
2023
+ },${hasBody
2024
+ ? `
2025
+ body: JSON.stringify(${JSON.stringify(body, null, 2).split('\n').map((line, i) => i === 0 ? line : `\t${line}`).join('\n')}),`
2026
+ : ''}
2027
+ });
2028
+
2029
+ const data = await response.json();
2030
+ console.log(data);`;
2031
+ }
2032
+ /**
2033
+ * Generate Shell (curl) code snippet
2034
+ */
2035
+ generateShellSnippet(endpoint) {
2036
+ const body = this.getSampleRequestBody(endpoint);
2037
+ const hasBody = body !== null;
2038
+ let curl = `curl -X ${endpoint.method} \\
2039
+ '${this.getApiBaseUrl()}${endpoint.path}' \\
2040
+ -H 'Content-Type: application/json'`;
2041
+ if (endpoint.auth?.hasAuth) {
2042
+ curl += ` \\
2043
+ -H 'Authorization: Bearer YOUR_TOKEN'`;
2044
+ }
2045
+ if (hasBody) {
2046
+ curl += ` \\
2047
+ -d '${JSON.stringify(body)}'`;
2048
+ }
2049
+ return curl;
2050
+ }
2051
+ /**
2052
+ * Generate PHP (cURL) code snippet
2053
+ */
2054
+ generatePhpSnippet(endpoint) {
2055
+ const body = this.getSampleRequestBody(endpoint);
2056
+ const hasBody = body !== null;
2057
+ return `<?php
2058
+ $ch = curl_init();
2059
+
2060
+ curl_setopt_array($ch, [
2061
+ CURLOPT_URL => '${this.getApiBaseUrl()}${endpoint.path}',
2062
+ CURLOPT_RETURNTRANSFER => true,
2063
+ CURLOPT_CUSTOMREQUEST => '${endpoint.method}',
2064
+ CURLOPT_HTTPHEADER => [
2065
+ 'Content-Type: application/json',${endpoint.auth?.hasAuth
2066
+ ? `
2067
+ 'Authorization: Bearer YOUR_TOKEN',`
2068
+ : ''}
2069
+ ],${hasBody
2070
+ ? `
2071
+ CURLOPT_POSTFIELDS => json_encode(${this.phpArrayLiteral(body)}),`
2072
+ : ''}
2073
+ ]);
2074
+
2075
+ $response = curl_exec($ch);
2076
+ curl_close($ch);
2077
+
2078
+ $data = json_decode($response, true);
2079
+ print_r($data);`;
2080
+ }
2081
+ /**
2082
+ * Convert object to PHP array literal syntax
2083
+ */
2084
+ phpArrayLiteral(obj) {
2085
+ const entries = Object.entries(obj).map(([key, value,]) => {
2086
+ const valueStr = typeof value === 'string' ? `'${value}'` : JSON.stringify(value);
2087
+ return `'${key}' => ${valueStr}`;
2088
+ });
2089
+ return `[${entries.join(', ')}]`;
2090
+ }
2091
+ /**
2092
+ * Generate Rust (reqwest) code snippet
2093
+ */
2094
+ generateRustSnippet(endpoint) {
2095
+ const body = this.getSampleRequestBody(endpoint);
2096
+ const hasBody = body !== null;
2097
+ const methodLower = endpoint.method.toLowerCase();
2098
+ return `use reqwest::Client;
2099
+ use serde_json::json;
2100
+
2101
+ #[tokio::main]
2102
+ async fn main() -> Result<(), Box<dyn std::error::Error>> {
2103
+ let client = Client::new();
2104
+
2105
+ let response = client
2106
+ .${methodLower}("${this.getApiBaseUrl()}${endpoint.path}")
2107
+ .header("Content-Type", "application/json")${endpoint.auth?.hasAuth
2108
+ ? `
2109
+ .header("Authorization", "Bearer YOUR_TOKEN")`
2110
+ : ''}${hasBody
2111
+ ? `
2112
+ .json(&json!(${JSON.stringify(body)}))`
2113
+ : ''}
2114
+ .send()
2115
+ .await?;
2116
+
2117
+ let data: serde_json::Value = response.json().await?;
2118
+ println!("{:#?}", data);
2119
+
2120
+ Ok(())
2121
+ }`;
2122
+ }
2123
+ /**
2124
+ * Generate C# (Flurl) code snippet
2125
+ */
2126
+ generateCSharpSnippet(endpoint) {
2127
+ const body = this.getSampleRequestBody(endpoint);
2128
+ const hasBody = body !== null;
2129
+ const methodPascal = endpoint.method.charAt(0).toUpperCase() + endpoint.method.slice(1).toLowerCase();
2130
+ let code = `using Flurl.Http;
2131
+
2132
+ var response = await "${this.getApiBaseUrl()}${endpoint.path}"`;
2133
+ if (endpoint.auth?.hasAuth) {
2134
+ code += `
2135
+ .WithHeader("Authorization", "Bearer YOUR_TOKEN")`;
2136
+ }
2137
+ if (hasBody) {
2138
+ code += `
2139
+ .${methodPascal}JsonAsync(new {
2140
+ ${Object.entries(body).map(([k, v,]) => ` ${k} = ${typeof v === 'string' ? `"${v}"` : v}`).join(',\n')}
2141
+ });`;
2142
+ }
2143
+ else {
2144
+ code += `
2145
+ .${methodPascal}Async();`;
2146
+ }
2147
+ code += `
2148
+
2149
+ var data = await response.GetJsonAsync();
2150
+ Console.WriteLine(data);`;
2151
+ return code;
2152
+ }
2153
+ /**
2154
+ * Generate Python (requests) code snippet
2155
+ */
2156
+ generatePythonSnippet(endpoint) {
2157
+ const body = this.getSampleRequestBody(endpoint);
2158
+ const hasBody = body !== null;
2159
+ const methodLower = endpoint.method.toLowerCase();
2160
+ let code = `import requests
2161
+
2162
+ response = requests.${methodLower}(
2163
+ '${this.getApiBaseUrl()}${endpoint.path}',
2164
+ headers={
2165
+ 'Content-Type': 'application/json',${endpoint.auth?.hasAuth
2166
+ ? `
2167
+ 'Authorization': 'Bearer YOUR_TOKEN',`
2168
+ : ''}
2169
+ },`;
2170
+ if (hasBody) {
2171
+ code += `
2172
+ json=${JSON.stringify(body)},`;
2173
+ }
2174
+ code += `
2175
+ )
2176
+
2177
+ data = response.json()
2178
+ print(data)`;
2179
+ return code;
2180
+ }
2181
+ /**
2182
+ * Generate Kotlin (OkHttp) code snippet
2183
+ */
2184
+ generateKotlinSnippet(endpoint) {
2185
+ const body = this.getSampleRequestBody(endpoint);
2186
+ const hasBody = body !== null;
2187
+ let code = `import okhttp3.*
2188
+ import okhttp3.MediaType.Companion.toMediaType
2189
+ import okhttp3.RequestBody.Companion.toRequestBody
2190
+
2191
+ val client = OkHttpClient()
2192
+
2193
+ val request = Request.Builder()
2194
+ .url("${this.getApiBaseUrl()}${endpoint.path}")`;
2195
+ if (endpoint.auth?.hasAuth) {
2196
+ code += `
2197
+ .addHeader("Authorization", "Bearer YOUR_TOKEN")`;
2198
+ }
2199
+ if (hasBody) {
2200
+ code += `
2201
+ .${endpoint.method.toLowerCase()}("""${JSON.stringify(body)}""".toRequestBody("application/json".toMediaType()))`;
2202
+ }
2203
+ else if (endpoint.method === 'POST' || endpoint.method === 'PUT' || endpoint.method === 'PATCH') {
2204
+ code += `
2205
+ .${endpoint.method.toLowerCase()}("".toRequestBody(null))`;
2206
+ }
2207
+ else {
2208
+ code += `
2209
+ .${endpoint.method.toLowerCase()}()`;
2210
+ }
2211
+ code += `
2212
+ .build()
2213
+
2214
+ val response = client.newCall(request).execute()
2215
+ println(response.body?.string())`;
2216
+ return code;
2217
+ }
2218
+ /**
2219
+ * Generate Swift (URLSession) code snippet
2220
+ */
2221
+ generateSwiftSnippet(endpoint) {
2222
+ const body = this.getSampleRequestBody(endpoint);
2223
+ const hasBody = body !== null;
2224
+ let code = `import Foundation
2225
+
2226
+ let url = URL(string: "${this.getApiBaseUrl()}${endpoint.path}")!
2227
+ var request = URLRequest(url: url)
2228
+ request.httpMethod = "${endpoint.method}"
2229
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")`;
2230
+ if (endpoint.auth?.hasAuth) {
2231
+ code += `
2232
+ request.setValue("Bearer YOUR_TOKEN", forHTTPHeaderField: "Authorization")`;
2233
+ }
2234
+ if (hasBody) {
2235
+ code += `
2236
+
2237
+ let body: [String: Any] = ${this.swiftDictLiteral(body)}
2238
+ request.httpBody = try? JSONSerialization.data(withJSONObject: body)`;
2239
+ }
2240
+ code += `
2241
+
2242
+ let task = URLSession.shared.dataTask(with: request) { data, response, error in
2243
+ guard let data = data else { return }
2244
+ if let json = try? JSONSerialization.jsonObject(with: data) {
2245
+ print(json)
2246
+ }
2247
+ }
2248
+ task.resume()`;
2249
+ return code;
2250
+ }
2251
+ /**
2252
+ * Convert object to Swift dictionary literal syntax
2253
+ */
2254
+ swiftDictLiteral(obj) {
2255
+ const entries = Object.entries(obj).map(([key, value,]) => {
2256
+ const valueStr = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
2257
+ return `"${key}": ${valueStr}`;
2258
+ });
2259
+ return `[${entries.join(', ')}]`;
2260
+ }
2261
+ /**
2262
+ * Generate Java (HttpClient) code snippet
2263
+ */
2264
+ generateJavaSnippet(endpoint) {
2265
+ const body = this.getSampleRequestBody(endpoint);
2266
+ const hasBody = body !== null;
2267
+ let code = `import java.net.http.*;
2268
+ import java.net.URI;
2269
+
2270
+ HttpClient client = HttpClient.newHttpClient();
2271
+
2272
+ HttpRequest request = HttpRequest.newBuilder()
2273
+ .uri(URI.create("${this.getApiBaseUrl()}${endpoint.path}"))
2274
+ .header("Content-Type", "application/json")`;
2275
+ if (endpoint.auth?.hasAuth) {
2276
+ code += `
2277
+ .header("Authorization", "Bearer YOUR_TOKEN")`;
2278
+ }
2279
+ if (hasBody) {
2280
+ code += `
2281
+ .method("${endpoint.method}", HttpRequest.BodyPublishers.ofString("${JSON.stringify(body).replace(/"/g, '\\"')}"))`;
2282
+ }
2283
+ else {
2284
+ code += `
2285
+ .method("${endpoint.method}", HttpRequest.BodyPublishers.noBody())`;
2286
+ }
2287
+ code += `
2288
+ .build();
2289
+
2290
+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
2291
+ System.out.println(response.body());`;
2292
+ return code;
2293
+ }
2294
+ }
2295
+ //# sourceMappingURL=html-generator.js.map