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.
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/dist/apidoc/api-doc-generator.d.ts +58 -0
- package/dist/apidoc/api-doc-generator.d.ts.map +1 -0
- package/dist/apidoc/api-doc-generator.js +201 -0
- package/dist/apidoc/api-doc-generator.js.map +1 -0
- package/dist/apidoc/config.d.ts +153 -0
- package/dist/apidoc/config.d.ts.map +1 -0
- package/dist/apidoc/config.js +254 -0
- package/dist/apidoc/config.js.map +1 -0
- package/dist/apidoc/controller-parser.d.ts +208 -0
- package/dist/apidoc/controller-parser.d.ts.map +1 -0
- package/dist/apidoc/controller-parser.js +686 -0
- package/dist/apidoc/controller-parser.js.map +1 -0
- package/dist/apidoc/html-generator.d.ts +290 -0
- package/dist/apidoc/html-generator.d.ts.map +1 -0
- package/dist/apidoc/html-generator.js +2295 -0
- package/dist/apidoc/html-generator.js.map +1 -0
- package/dist/apidoc/index.d.ts +20 -0
- package/dist/apidoc/index.d.ts.map +1 -0
- package/dist/apidoc/index.js +16 -0
- package/dist/apidoc/index.js.map +1 -0
- package/dist/apidoc/openapi-builder.d.ts +169 -0
- package/dist/apidoc/openapi-builder.d.ts.map +1 -0
- package/dist/apidoc/openapi-builder.js +634 -0
- package/dist/apidoc/openapi-builder.js.map +1 -0
- package/dist/apidoc/parameterGeneratorRegistry.d.ts +20 -0
- package/dist/apidoc/parameterGeneratorRegistry.d.ts.map +1 -0
- package/dist/apidoc/parameterGeneratorRegistry.js +6 -0
- package/dist/apidoc/parameterGeneratorRegistry.js.map +1 -0
- package/dist/apidoc/test-type-resolver.d.ts +2 -0
- package/dist/apidoc/test-type-resolver.d.ts.map +1 -0
- package/dist/apidoc/test-type-resolver.js +6 -0
- package/dist/apidoc/test-type-resolver.js.map +1 -0
- package/dist/apidoc/type-resolver.d.ts +266 -0
- package/dist/apidoc/type-resolver.d.ts.map +1 -0
- package/dist/apidoc/type-resolver.js +1226 -0
- package/dist/apidoc/type-resolver.js.map +1 -0
- package/dist/apidoc/verify-type-resolution.d.ts +3 -0
- package/dist/apidoc/verify-type-resolution.d.ts.map +1 -0
- package/dist/apidoc/verify-type-resolution.js +29 -0
- package/dist/apidoc/verify-type-resolution.js.map +1 -0
- package/dist/bun/bunRouter.d.ts +70 -0
- package/dist/bun/bunRouter.d.ts.map +1 -0
- package/dist/bun/bunRouter.js +324 -0
- package/dist/bun/bunRouter.js.map +1 -0
- package/dist/bun/bunServer.d.ts +72 -0
- package/dist/bun/bunServer.d.ts.map +1 -0
- package/dist/bun/bunServer.js +218 -0
- package/dist/bun/bunServer.js.map +1 -0
- package/dist/bun/bunStaticFiles.d.ts +76 -0
- package/dist/bun/bunStaticFiles.d.ts.map +1 -0
- package/dist/bun/bunStaticFiles.js +251 -0
- package/dist/bun/bunStaticFiles.js.map +1 -0
- package/dist/bun/index.d.ts +7 -0
- package/dist/bun/index.d.ts.map +1 -0
- package/dist/bun/index.js +7 -0
- package/dist/bun/index.js.map +1 -0
- package/dist/data-contracts.d.ts +132 -0
- package/dist/data-contracts.d.ts.map +1 -0
- package/dist/data-contracts.js +2 -0
- package/dist/data-contracts.js.map +1 -0
- package/dist/decorators.d.ts +75 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +101 -0
- package/dist/decorators.js.map +1 -0
- package/dist/express/expressFrontendRouter.d.ts +17 -0
- package/dist/express/expressFrontendRouter.d.ts.map +1 -0
- package/dist/express/expressFrontendRouter.js +33 -0
- package/dist/express/expressFrontendRouter.js.map +1 -0
- package/dist/express/expressRouter.d.ts +25 -0
- package/dist/express/expressRouter.d.ts.map +1 -0
- package/dist/express/expressRouter.js +150 -0
- package/dist/express/expressRouter.js.map +1 -0
- package/dist/express/index.d.ts +6 -0
- package/dist/express/index.d.ts.map +1 -0
- package/dist/express/index.js +6 -0
- package/dist/express/index.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +162 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +350 -0
- package/dist/router.js.map +1 -0
- package/dist/runtime-detect.d.ts +20 -0
- package/dist/runtime-detect.d.ts.map +1 -0
- package/dist/runtime-detect.js +20 -0
- package/dist/runtime-detect.js.map +1 -0
- package/dist/server.d.ts +126 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +181 -0
- package/dist/server.js.map +1 -0
- package/dist/utils.d.ts +83 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +157 -0
- package/dist/utils.js.map +1 -0
- 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">← 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<</code>${itemTypeHtml}<code>></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
|
+
'&': '&',
|
|
1763
|
+
'<': '<',
|
|
1764
|
+
'>': '>',
|
|
1765
|
+
'"': '"',
|
|
1766
|
+
'\'': ''',
|
|
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
|