apidocly 1.0.3
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 +246 -0
- package/bin/apidocly.js +62 -0
- package/lib/generator/data-builder.js +170 -0
- package/lib/generator/index.js +334 -0
- package/lib/index.js +59 -0
- package/lib/parser/annotations.js +230 -0
- package/lib/parser/index.js +86 -0
- package/lib/parser/languages.js +57 -0
- package/lib/utils/config-loader.js +67 -0
- package/lib/utils/file-scanner.js +64 -0
- package/lib/utils/minifier.js +106 -0
- package/package.json +46 -0
- package/template/css/style.css +2670 -0
- package/template/index.html +243 -0
- package/template/js/auth.js +281 -0
- package/template/js/main.js +2933 -0
|
@@ -0,0 +1,2933 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Settings Management
|
|
5
|
+
const SETTINGS_KEY = 'apidocly-settings';
|
|
6
|
+
const defaultSettings = {
|
|
7
|
+
expandExamples: false,
|
|
8
|
+
expandEndpoints: false,
|
|
9
|
+
autoCollapse: true,
|
|
10
|
+
showTryit: true,
|
|
11
|
+
showCodeSamples: true,
|
|
12
|
+
compactMode: false
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let settings = { ...defaultSettings };
|
|
16
|
+
|
|
17
|
+
function loadSettings() {
|
|
18
|
+
try {
|
|
19
|
+
const stored = localStorage.getItem(SETTINGS_KEY);
|
|
20
|
+
if (stored) {
|
|
21
|
+
settings = { ...defaultSettings, ...JSON.parse(stored) };
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.warn('Failed to load settings:', e);
|
|
25
|
+
}
|
|
26
|
+
applySettings();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function saveSettings() {
|
|
30
|
+
try {
|
|
31
|
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.warn('Failed to save settings:', e);
|
|
34
|
+
}
|
|
35
|
+
applySettings();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applySettings() {
|
|
39
|
+
const app = document.getElementById('app');
|
|
40
|
+
if (!app) return;
|
|
41
|
+
|
|
42
|
+
// Compact mode
|
|
43
|
+
app.classList.toggle('compact-mode', settings.compactMode);
|
|
44
|
+
|
|
45
|
+
// Hide try it out
|
|
46
|
+
app.classList.toggle('hide-tryit', !settings.showTryit);
|
|
47
|
+
|
|
48
|
+
// Hide code samples
|
|
49
|
+
app.classList.toggle('hide-codesamples', !settings.showCodeSamples);
|
|
50
|
+
|
|
51
|
+
// Update toggle states in UI
|
|
52
|
+
const toggles = {
|
|
53
|
+
'setting-expand-examples': settings.expandExamples,
|
|
54
|
+
'setting-expand-endpoints': settings.expandEndpoints,
|
|
55
|
+
'setting-auto-collapse': settings.autoCollapse,
|
|
56
|
+
'setting-show-tryit': settings.showTryit,
|
|
57
|
+
'setting-show-codesamples': settings.showCodeSamples,
|
|
58
|
+
'setting-compact-mode': settings.compactMode
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const [id, value] of Object.entries(toggles)) {
|
|
62
|
+
const el = document.getElementById(id);
|
|
63
|
+
if (el) el.checked = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// DOM Elements
|
|
68
|
+
const app = document.getElementById('app');
|
|
69
|
+
const groupsView = document.getElementById('groups-view');
|
|
70
|
+
const groupsList = document.getElementById('groups-list');
|
|
71
|
+
const endpointsView = document.getElementById('endpoints-view');
|
|
72
|
+
const endpointsList = document.getElementById('endpoints-list');
|
|
73
|
+
const searchView = document.getElementById('search-view');
|
|
74
|
+
const searchResults = document.getElementById('search-results');
|
|
75
|
+
const searchInput = document.getElementById('search-input');
|
|
76
|
+
const searchClear = document.getElementById('search-clear');
|
|
77
|
+
const searchQuery = document.getElementById('search-query');
|
|
78
|
+
const clearSearchBtn = document.getElementById('clear-search');
|
|
79
|
+
const backToGroups = document.getElementById('back-to-groups');
|
|
80
|
+
const currentGroupEl = document.getElementById('current-group');
|
|
81
|
+
|
|
82
|
+
// Stats elements
|
|
83
|
+
const statEndpoints = document.getElementById('stat-endpoints');
|
|
84
|
+
const statGroups = document.getElementById('stat-groups');
|
|
85
|
+
const statPrivate = document.getElementById('stat-private');
|
|
86
|
+
const statVersion = document.getElementById('stat-version');
|
|
87
|
+
|
|
88
|
+
// State
|
|
89
|
+
let currentGroup = null;
|
|
90
|
+
let groupColors = {};
|
|
91
|
+
let selectedEnvironment = null;
|
|
92
|
+
let selectedEndpointVersion = null; // null = show latest version of each endpoint
|
|
93
|
+
|
|
94
|
+
// Get all unique endpoint versions from apiData
|
|
95
|
+
function getEndpointVersions() {
|
|
96
|
+
const versions = new Set();
|
|
97
|
+
for (const group of apiData.groups) {
|
|
98
|
+
for (const ep of group.endpoints) {
|
|
99
|
+
if (ep.version) {
|
|
100
|
+
versions.add(ep.version);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Sort versions descending (newest first)
|
|
105
|
+
return Array.from(versions).sort((a, b) => {
|
|
106
|
+
const aParts = a.split('.').map(Number);
|
|
107
|
+
const bParts = b.split('.').map(Number);
|
|
108
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
109
|
+
const aNum = aParts[i] || 0;
|
|
110
|
+
const bNum = bParts[i] || 0;
|
|
111
|
+
if (aNum !== bNum) return bNum - aNum;
|
|
112
|
+
}
|
|
113
|
+
return 0;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Filter data to show only endpoints matching the selected version
|
|
118
|
+
function getActiveData() {
|
|
119
|
+
if (!selectedEndpointVersion) {
|
|
120
|
+
// Show latest version of each endpoint
|
|
121
|
+
return filterToLatestVersions(apiData);
|
|
122
|
+
}
|
|
123
|
+
// Filter to specific version
|
|
124
|
+
return filterToVersion(apiData, selectedEndpointVersion);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function filterToLatestVersions(data) {
|
|
128
|
+
const filtered = {
|
|
129
|
+
...data,
|
|
130
|
+
groups: []
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const group of data.groups) {
|
|
134
|
+
const endpointMap = new Map();
|
|
135
|
+
|
|
136
|
+
for (const ep of group.endpoints) {
|
|
137
|
+
// Create key without version
|
|
138
|
+
const normalizedPath = ep.path.replace(/^\/\d+\.\d+\.\d+/, '');
|
|
139
|
+
const key = ep.method + ':' + normalizedPath + ':' + (ep.name || '');
|
|
140
|
+
|
|
141
|
+
const existing = endpointMap.get(key);
|
|
142
|
+
if (!existing || compareVersions(ep.version || '0.0.0', existing.version || '0.0.0') > 0) {
|
|
143
|
+
endpointMap.set(key, ep);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (endpointMap.size > 0) {
|
|
148
|
+
filtered.groups.push({
|
|
149
|
+
...group,
|
|
150
|
+
endpoints: Array.from(endpointMap.values())
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return filtered;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function filterToVersion(data, version) {
|
|
159
|
+
const filtered = {
|
|
160
|
+
...data,
|
|
161
|
+
groups: []
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
for (const group of data.groups) {
|
|
165
|
+
const endpoints = group.endpoints.filter(ep => ep.version === version);
|
|
166
|
+
if (endpoints.length > 0) {
|
|
167
|
+
filtered.groups.push({
|
|
168
|
+
...group,
|
|
169
|
+
endpoints: endpoints
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return filtered;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function compareVersions(v1, v2) {
|
|
178
|
+
const parts1 = (v1 || '0.0.0').split('.').map(Number);
|
|
179
|
+
const parts2 = (v2 || '0.0.0').split('.').map(Number);
|
|
180
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
181
|
+
const num1 = parts1[i] || 0;
|
|
182
|
+
const num2 = parts2[i] || 0;
|
|
183
|
+
if (num1 > num2) return 1;
|
|
184
|
+
if (num1 < num2) return -1;
|
|
185
|
+
}
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function init() {
|
|
190
|
+
if (typeof apiData === 'undefined') {
|
|
191
|
+
console.error('API data not found');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
loadSettings();
|
|
196
|
+
generateGroupColors();
|
|
197
|
+
initEnvironmentSelector();
|
|
198
|
+
initVersionComparison();
|
|
199
|
+
updateStats();
|
|
200
|
+
renderGroups();
|
|
201
|
+
setupEventListeners();
|
|
202
|
+
handleInitialHash();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Environment Selector
|
|
206
|
+
function initEnvironmentSelector() {
|
|
207
|
+
const environments = apiData.project.environments || [];
|
|
208
|
+
const wrapper = document.getElementById('env-selector-wrapper');
|
|
209
|
+
const selector = document.getElementById('env-selector');
|
|
210
|
+
|
|
211
|
+
if (!wrapper || !selector) return;
|
|
212
|
+
|
|
213
|
+
// If no environments defined, hide the selector
|
|
214
|
+
if (environments.length === 0) {
|
|
215
|
+
wrapper.classList.add('hidden');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Show the selector
|
|
220
|
+
wrapper.classList.remove('hidden');
|
|
221
|
+
|
|
222
|
+
// Build options
|
|
223
|
+
let optionsHtml = '';
|
|
224
|
+
environments.forEach((env, index) => {
|
|
225
|
+
optionsHtml += `<option value="${index}">${escapeHtml(env.name)}</option>`;
|
|
226
|
+
});
|
|
227
|
+
selector.innerHTML = optionsHtml;
|
|
228
|
+
|
|
229
|
+
// Load saved selection from localStorage
|
|
230
|
+
const savedEnvIndex = localStorage.getItem('apidocly-environment');
|
|
231
|
+
if (savedEnvIndex !== null && environments[parseInt(savedEnvIndex)]) {
|
|
232
|
+
selector.value = savedEnvIndex;
|
|
233
|
+
selectedEnvironment = environments[parseInt(savedEnvIndex)];
|
|
234
|
+
} else {
|
|
235
|
+
selectedEnvironment = environments[0];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle change
|
|
239
|
+
selector.addEventListener('change', (e) => {
|
|
240
|
+
const index = parseInt(e.target.value);
|
|
241
|
+
selectedEnvironment = environments[index];
|
|
242
|
+
localStorage.setItem('apidocly-environment', index);
|
|
243
|
+
updateTryItOutUrls();
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getSelectedBaseUrl(endpoint) {
|
|
248
|
+
// Priority: endpoint.sampleRequest > selectedEnvironment > apiData.project.sampleUrl
|
|
249
|
+
if (endpoint.sampleRequest && endpoint.sampleRequest !== 'off') {
|
|
250
|
+
return endpoint.sampleRequest;
|
|
251
|
+
}
|
|
252
|
+
if (selectedEnvironment && selectedEnvironment.url) {
|
|
253
|
+
return selectedEnvironment.url;
|
|
254
|
+
}
|
|
255
|
+
return apiData.project.sampleUrl || '';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function updateTryItOutUrls() {
|
|
259
|
+
// Update all visible URL previews
|
|
260
|
+
document.querySelectorAll('.sample-request').forEach(container => {
|
|
261
|
+
const endpointId = container.dataset.endpointId;
|
|
262
|
+
const path = container.dataset.path;
|
|
263
|
+
const urlPreview = document.getElementById(`sample-url-preview-${endpointId}`);
|
|
264
|
+
|
|
265
|
+
if (urlPreview) {
|
|
266
|
+
// Find the endpoint to check for sampleRequest override
|
|
267
|
+
let endpoint = null;
|
|
268
|
+
for (const group of apiData.groups) {
|
|
269
|
+
endpoint = group.endpoints.find(ep => ep.id === endpointId);
|
|
270
|
+
if (endpoint) break;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const baseUrl = endpoint ? getSelectedBaseUrl(endpoint) : (selectedEnvironment?.url || apiData.project.sampleUrl || '');
|
|
274
|
+
urlPreview.textContent = baseUrl + path;
|
|
275
|
+
container.dataset.baseUrl = baseUrl;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Generate consistent colors for groups based on name hash
|
|
281
|
+
function generateGroupColors() {
|
|
282
|
+
const data = getActiveData();
|
|
283
|
+
// Premium color palette: cyan, purple, pink, amber, green, blue, orange, magenta
|
|
284
|
+
const colors = [
|
|
285
|
+
'hsl(186, 94%, 43%)', // Cyan (primary)
|
|
286
|
+
'hsl(271, 91%, 65%)', // Purple
|
|
287
|
+
'hsl(333, 84%, 63%)', // Pink
|
|
288
|
+
'hsl(43, 96%, 56%)', // Amber
|
|
289
|
+
'hsl(142, 76%, 36%)', // Green
|
|
290
|
+
'hsl(217, 91%, 60%)', // Blue
|
|
291
|
+
'hsl(25, 95%, 53%)', // Orange
|
|
292
|
+
'hsl(293, 69%, 49%)', // Magenta
|
|
293
|
+
'hsl(172, 66%, 50%)', // Teal
|
|
294
|
+
'hsl(348, 83%, 47%)' // Rose
|
|
295
|
+
];
|
|
296
|
+
data.groups.forEach((group, index) => {
|
|
297
|
+
groupColors[group.name] = colors[index % colors.length];
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getGroupColor(groupName) {
|
|
302
|
+
return groupColors[groupName] || 'hsl(186, 94%, 43%)';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function updateStats() {
|
|
306
|
+
const data = getActiveData();
|
|
307
|
+
let totalEndpoints = 0;
|
|
308
|
+
let privateCount = 0;
|
|
309
|
+
|
|
310
|
+
data.groups.forEach(group => {
|
|
311
|
+
totalEndpoints += group.endpoints.length;
|
|
312
|
+
group.endpoints.forEach(ep => {
|
|
313
|
+
if (ep.private) privateCount++;
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
statEndpoints.textContent = totalEndpoints;
|
|
318
|
+
statGroups.textContent = data.groups.length;
|
|
319
|
+
statVersion.textContent = data.project.version || '-';
|
|
320
|
+
|
|
321
|
+
// Only show private stat if there are private endpoints
|
|
322
|
+
const privateCard = statPrivate.closest('.stat-card');
|
|
323
|
+
if (privateCount > 0) {
|
|
324
|
+
statPrivate.textContent = privateCount;
|
|
325
|
+
privateCard.classList.remove('hidden');
|
|
326
|
+
} else {
|
|
327
|
+
privateCard.classList.add('hidden');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function renderGroups() {
|
|
332
|
+
const data = getActiveData();
|
|
333
|
+
let html = `
|
|
334
|
+
<table class="groups-table">
|
|
335
|
+
<thead>
|
|
336
|
+
<tr>
|
|
337
|
+
<th>Group</th>
|
|
338
|
+
<th>Endpoints</th>
|
|
339
|
+
<th></th>
|
|
340
|
+
<th></th>
|
|
341
|
+
</tr>
|
|
342
|
+
</thead>
|
|
343
|
+
<tbody>
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
data.groups.forEach(group => {
|
|
347
|
+
const color = getGroupColor(group.name);
|
|
348
|
+
const endpointCount = group.endpoints.length;
|
|
349
|
+
|
|
350
|
+
html += `
|
|
351
|
+
<tr class="group-row" data-group="${escapeHtml(group.name)}" style="--group-color: ${color}">
|
|
352
|
+
<td>
|
|
353
|
+
<div class="group-name-cell">
|
|
354
|
+
<span class="group-name">${escapeHtml(group.name)}</span>
|
|
355
|
+
</div>
|
|
356
|
+
</td>
|
|
357
|
+
<td class="group-count-cell">${endpointCount}</td>
|
|
358
|
+
<td class="group-export-cell">
|
|
359
|
+
<button class="export-group-btn" onclick="event.stopPropagation(); downloadOpenAPI('${escapeHtml(group.name)}')" title="Download ${escapeHtml(group.name)} as OpenAPI JSON">
|
|
360
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
361
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
362
|
+
<polyline points="7 10 12 15 17 10"></polyline>
|
|
363
|
+
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
364
|
+
</svg>
|
|
365
|
+
</button>
|
|
366
|
+
</td>
|
|
367
|
+
<td class="group-arrow-cell">
|
|
368
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
369
|
+
<path d="m9 18 6-6-6-6"></path>
|
|
370
|
+
</svg>
|
|
371
|
+
</td>
|
|
372
|
+
</tr>
|
|
373
|
+
`;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
html += '</tbody></table>';
|
|
377
|
+
groupsList.innerHTML = html;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function showGroupEndpoints(groupName) {
|
|
381
|
+
const data = getActiveData();
|
|
382
|
+
const group = data.groups.find(g => g.name === groupName);
|
|
383
|
+
if (!group) return;
|
|
384
|
+
|
|
385
|
+
currentGroup = groupName;
|
|
386
|
+
const color = getGroupColor(groupName);
|
|
387
|
+
|
|
388
|
+
// Update header
|
|
389
|
+
currentGroupEl.querySelector('.group-color-bar').style.backgroundColor = color;
|
|
390
|
+
currentGroupEl.querySelector('.group-name').textContent = groupName;
|
|
391
|
+
currentGroupEl.querySelector('.group-count').textContent = `${group.endpoints.length} endpoint${group.endpoints.length !== 1 ? 's' : ''}`;
|
|
392
|
+
|
|
393
|
+
// Update export button
|
|
394
|
+
const exportBtn = document.getElementById('group-export-btn');
|
|
395
|
+
if (exportBtn) {
|
|
396
|
+
exportBtn.setAttribute('onclick', `event.stopPropagation(); downloadOpenAPI('${escapeHtml(groupName)}')`);
|
|
397
|
+
exportBtn.setAttribute('title', `Download ${groupName} as OpenAPI JSON`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Render endpoints with striped header
|
|
401
|
+
let html = '<div class="endpoints-stripe"></div>';
|
|
402
|
+
group.endpoints.forEach(endpoint => {
|
|
403
|
+
html += renderEndpointItem(endpoint);
|
|
404
|
+
});
|
|
405
|
+
endpointsList.innerHTML = html;
|
|
406
|
+
|
|
407
|
+
// Switch views
|
|
408
|
+
groupsView.classList.add('hidden');
|
|
409
|
+
searchView.classList.add('hidden');
|
|
410
|
+
endpointsView.classList.remove('hidden');
|
|
411
|
+
|
|
412
|
+
// Update URL
|
|
413
|
+
window.history.pushState({}, '', `#group-${encodeURIComponent(groupName)}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function showGroupsView() {
|
|
417
|
+
currentGroup = null;
|
|
418
|
+
endpointsView.classList.add('hidden');
|
|
419
|
+
searchView.classList.add('hidden');
|
|
420
|
+
groupsView.classList.remove('hidden');
|
|
421
|
+
window.history.pushState({}, '', window.location.pathname);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderEndpointItem(endpoint) {
|
|
425
|
+
const methodClass = endpoint.method.toLowerCase();
|
|
426
|
+
const deprecatedBadge = endpoint.deprecated ? '<span class="deprecated-badge">Deprecated</span>' : '';
|
|
427
|
+
const expandedClass = settings.expandEndpoints ? 'expanded' : '';
|
|
428
|
+
const detailsHidden = settings.expandEndpoints ? '' : 'hidden';
|
|
429
|
+
|
|
430
|
+
return `
|
|
431
|
+
<div class="endpoint-item ${expandedClass}" data-id="${escapeHtml(endpoint.id)}">
|
|
432
|
+
<div class="endpoint-header">
|
|
433
|
+
<span class="endpoint-method ${methodClass}">${endpoint.method}</span>
|
|
434
|
+
<span class="endpoint-path">${escapeHtml(endpoint.path)}</span>
|
|
435
|
+
<span class="endpoint-title">${escapeHtml(endpoint.title || '')}${deprecatedBadge}</span>
|
|
436
|
+
<svg class="endpoint-toggle" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
437
|
+
<path d="m9 18 6-6-6-6"></path>
|
|
438
|
+
</svg>
|
|
439
|
+
</div>
|
|
440
|
+
<div class="endpoint-details ${detailsHidden}">
|
|
441
|
+
${renderEndpointDetails(endpoint)}
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function renderEndpointDetails(endpoint) {
|
|
448
|
+
let html = '';
|
|
449
|
+
|
|
450
|
+
// Badges
|
|
451
|
+
if (endpoint.version || endpoint.permission || endpoint.deprecated) {
|
|
452
|
+
html += '<div class="endpoint-badges">';
|
|
453
|
+
if (endpoint.version) {
|
|
454
|
+
html += `<span class="badge badge-version">v${escapeHtml(endpoint.version)}</span>`;
|
|
455
|
+
}
|
|
456
|
+
if (endpoint.permission) {
|
|
457
|
+
html += `<span class="badge badge-permission">${escapeHtml(endpoint.permission)}</span>`;
|
|
458
|
+
}
|
|
459
|
+
if (endpoint.deprecated) {
|
|
460
|
+
html += `<span class="badge badge-deprecated">Deprecated</span>`;
|
|
461
|
+
}
|
|
462
|
+
html += '</div>';
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Description
|
|
466
|
+
if (endpoint.description) {
|
|
467
|
+
html += `<div class="endpoint-description">${marked.parse(endpoint.description)}</div>`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Deprecation notice
|
|
471
|
+
if (endpoint.deprecated && endpoint.deprecatedText) {
|
|
472
|
+
html += `<div class="endpoint-description" style="color: var(--warning);"><strong>Deprecation notice:</strong> ${escapeHtml(endpoint.deprecatedText)}</div>`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Parameters sections
|
|
476
|
+
html += renderParamsSection('Headers', endpoint.headers);
|
|
477
|
+
html += renderParamsSection('Query Parameters', endpoint.query);
|
|
478
|
+
html += renderParamsSection('Body Parameters', endpoint.body);
|
|
479
|
+
html += renderParamsSection('Parameters', endpoint.parameters);
|
|
480
|
+
|
|
481
|
+
// Examples
|
|
482
|
+
html += renderExamples('Request Examples', endpoint.examples);
|
|
483
|
+
html += renderExamples('Header Examples', endpoint.headerExamples);
|
|
484
|
+
html += renderExamples('Parameter Examples', endpoint.paramExamples);
|
|
485
|
+
|
|
486
|
+
// Success/Error responses
|
|
487
|
+
html += renderResponseSection('Success Response', endpoint.success, 'success');
|
|
488
|
+
html += renderExamples('Success Examples', endpoint.successExamples, 'success');
|
|
489
|
+
html += renderResponseSection('Error Response', endpoint.error, 'error');
|
|
490
|
+
html += renderExamples('Error Examples', endpoint.errorExamples, 'error');
|
|
491
|
+
|
|
492
|
+
// Code samples
|
|
493
|
+
html += renderCodeSamples(endpoint);
|
|
494
|
+
|
|
495
|
+
// Try it out
|
|
496
|
+
html += renderTryItOut(endpoint);
|
|
497
|
+
|
|
498
|
+
return html;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function renderCodeSamples(endpoint) {
|
|
502
|
+
const baseUrl = getSelectedBaseUrl(endpoint);
|
|
503
|
+
if (!baseUrl) return '';
|
|
504
|
+
|
|
505
|
+
const eid = escapeHtml(endpoint.id);
|
|
506
|
+
const codeLangs = [
|
|
507
|
+
{ key: 'curl', label: 'cURL' },
|
|
508
|
+
{ key: 'javascript', label: 'JavaScript' },
|
|
509
|
+
{ key: 'axios', label: 'Axios' },
|
|
510
|
+
{ key: 'python', label: 'Python' },
|
|
511
|
+
{ key: 'php', label: 'PHP' }
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
let tabsHtml = '';
|
|
515
|
+
let panelsHtml = '';
|
|
516
|
+
|
|
517
|
+
for (let i = 0; i < codeLangs.length; i++) {
|
|
518
|
+
const lang = codeLangs[i];
|
|
519
|
+
const isActive = i === 0;
|
|
520
|
+
const sample = generateCodeSample(endpoint, lang.key, baseUrl);
|
|
521
|
+
const highlighted = highlightCode(sample, lang.key);
|
|
522
|
+
|
|
523
|
+
tabsHtml += '<button class="code-sample-tab' + (isActive ? ' active' : '') + '" data-lang="' + lang.key + '" onclick="switchCodeSample(\'' + eid + '\', \'' + lang.key + '\')">' + lang.label + '</button>';
|
|
524
|
+
|
|
525
|
+
panelsHtml += '<div class="code-sample-panel' + (isActive ? '' : ' hidden') + '" data-lang="' + lang.key + '">' +
|
|
526
|
+
'<div class="code-sample-header"><span>' + lang.label + '</span><button class="copy-btn" onclick="copyCodeSample(this)">Copy</button></div>' +
|
|
527
|
+
'<pre class="code-sample-code"><code>' + highlighted + '</code></pre></div>';
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return '<div class="code-samples-section">' +
|
|
531
|
+
'<div class="params-section-title">Code Samples</div>' +
|
|
532
|
+
'<div class="code-samples-container" data-endpoint-id="' + eid + '">' +
|
|
533
|
+
'<div class="code-samples-tabs">' + tabsHtml + '</div>' +
|
|
534
|
+
'<div class="code-samples-content">' + panelsHtml + '</div>' +
|
|
535
|
+
'</div></div>';
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function highlightCode(code, lang) {
|
|
539
|
+
// First escape HTML entities
|
|
540
|
+
let escaped = code
|
|
541
|
+
.replace(/&/g, '&')
|
|
542
|
+
.replace(/</g, '<')
|
|
543
|
+
.replace(/>/g, '>');
|
|
544
|
+
|
|
545
|
+
// Tokenize and highlight
|
|
546
|
+
// We'll use placeholders to avoid regex conflicts
|
|
547
|
+
const tokens = [];
|
|
548
|
+
let tokenId = 0;
|
|
549
|
+
|
|
550
|
+
function addToken(className, match) {
|
|
551
|
+
const placeholder = `__TOKEN_${tokenId}__`;
|
|
552
|
+
tokens.push({ placeholder, html: '<span class="' + className + '">' + match + '</span>' });
|
|
553
|
+
tokenId++;
|
|
554
|
+
return placeholder;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
let result = escaped;
|
|
558
|
+
|
|
559
|
+
// Strings (must be done first to avoid highlighting inside strings)
|
|
560
|
+
result = result.replace(/"(?:[^"\\]|\\.)*"/g, m => addToken('hl-string', m));
|
|
561
|
+
result = result.replace(/'(?:[^'\\]|\\.)*'/g, m => addToken('hl-string', m));
|
|
562
|
+
|
|
563
|
+
// Language-specific highlighting
|
|
564
|
+
if (lang === 'curl') {
|
|
565
|
+
result = result.replace(/\b(curl)\b/g, m => addToken('hl-keyword', m));
|
|
566
|
+
result = result.replace(/\s(-X|-H|-d|--data|--header)\b/g, (m, flag) => ' ' + addToken('hl-flag', flag));
|
|
567
|
+
result = result.replace(/\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g, m => addToken('hl-method', m));
|
|
568
|
+
} else if (lang === 'javascript' || lang === 'axios') {
|
|
569
|
+
result = result.replace(/\b(const|let|var|function|return|if|else|for|while|import|from|export|default|async|await|new|this|true|false|null|undefined)\b/g, m => addToken('hl-keyword', m));
|
|
570
|
+
result = result.replace(/\b(fetch|console|JSON|Promise|then|catch|stringify|parse|log|error|axios)\b/g, m => addToken('hl-builtin', m));
|
|
571
|
+
} else if (lang === 'python') {
|
|
572
|
+
result = result.replace(/\b(import|from|def|return|if|else|elif|for|while|in|not|and|or|True|False|None|print|class|try|except|with|as)\b/g, m => addToken('hl-keyword', m));
|
|
573
|
+
result = result.replace(/\b(requests|json|response)\b/g, m => addToken('hl-builtin', m));
|
|
574
|
+
} else if (lang === 'php') {
|
|
575
|
+
result = result.replace(/<\?php/g, m => addToken('hl-tag', m));
|
|
576
|
+
result = result.replace(/\?>/g, m => addToken('hl-tag', m));
|
|
577
|
+
result = result.replace(/\$[a-zA-Z_]\w*/g, m => addToken('hl-variable', m));
|
|
578
|
+
result = result.replace(/\b(function|return|if|else|elseif|for|foreach|while|echo|print|true|false|null|array|new|class)\b/g, m => addToken('hl-keyword', m));
|
|
579
|
+
result = result.replace(/\b(curl_init|curl_setopt_array|curl_exec|curl_error|curl_close|json_encode|json_decode|print_r)\b/g, m => addToken('hl-builtin', m));
|
|
580
|
+
result = result.replace(/\b(CURLOPT_[A-Z_]+)\b/g, m => addToken('hl-constant', m));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Numbers (but not inside tokens)
|
|
584
|
+
result = result.replace(/\b(\d+\.?\d*)\b/g, m => addToken('hl-number', m));
|
|
585
|
+
|
|
586
|
+
// Restore tokens
|
|
587
|
+
for (const token of tokens) {
|
|
588
|
+
result = result.replace(token.placeholder, token.html);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function generateCodeSample(endpoint, lang, baseUrl) {
|
|
595
|
+
const method = endpoint.method;
|
|
596
|
+
let path = endpoint.path;
|
|
597
|
+
const url = baseUrl + path;
|
|
598
|
+
|
|
599
|
+
// Get sample values for parameters
|
|
600
|
+
const pathParams = getAllParams(endpoint.parameters);
|
|
601
|
+
const queryParams = getAllParams(endpoint.query);
|
|
602
|
+
const headerParams = getAllParams(endpoint.headers);
|
|
603
|
+
const bodyParams = getAllParams(endpoint.body);
|
|
604
|
+
|
|
605
|
+
// Replace path params with sample values
|
|
606
|
+
pathParams.forEach(param => {
|
|
607
|
+
const sampleValue = getSampleValue(param);
|
|
608
|
+
path = path.replace(`:${param.field}`, sampleValue);
|
|
609
|
+
path = path.replace(`{${param.field}}`, sampleValue);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const finalUrl = baseUrl + path;
|
|
613
|
+
|
|
614
|
+
// Build query string
|
|
615
|
+
let queryString = '';
|
|
616
|
+
if (queryParams.length > 0) {
|
|
617
|
+
const params = queryParams.map(p => `${p.field}=${encodeURIComponent(getSampleValue(p))}`).join('&');
|
|
618
|
+
queryString = '?' + params;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Build body object
|
|
622
|
+
const bodyObj = {};
|
|
623
|
+
bodyParams.forEach(param => {
|
|
624
|
+
setNestedValue(bodyObj, param.field, getSampleValue(param));
|
|
625
|
+
});
|
|
626
|
+
const hasBody = Object.keys(bodyObj).length > 0 && ['POST', 'PUT', 'PATCH'].includes(method);
|
|
627
|
+
|
|
628
|
+
switch (lang) {
|
|
629
|
+
case 'curl':
|
|
630
|
+
return generateCurlSample(method, finalUrl + queryString, headerParams, bodyObj, hasBody);
|
|
631
|
+
case 'javascript':
|
|
632
|
+
return generateFetchSample(method, finalUrl + queryString, headerParams, bodyObj, hasBody);
|
|
633
|
+
case 'axios':
|
|
634
|
+
return generateAxiosSample(method, finalUrl + queryString, headerParams, bodyObj, hasBody);
|
|
635
|
+
case 'python':
|
|
636
|
+
return generatePythonSample(method, finalUrl + queryString, headerParams, bodyObj, hasBody);
|
|
637
|
+
case 'php':
|
|
638
|
+
return generatePhpSample(method, finalUrl + queryString, headerParams, bodyObj, hasBody);
|
|
639
|
+
default:
|
|
640
|
+
return '';
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function getSampleValue(param) {
|
|
645
|
+
if (param.defaultValue) return param.defaultValue;
|
|
646
|
+
if (param.type && param.type.allowedValues && param.type.allowedValues.length > 0) {
|
|
647
|
+
return param.type.allowedValues[0];
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const typeName = param.type ? param.type.name.toLowerCase() : 'string';
|
|
651
|
+
const fieldLower = param.field.toLowerCase();
|
|
652
|
+
|
|
653
|
+
// Smart defaults based on field name
|
|
654
|
+
if (fieldLower.includes('email')) return 'user@example.com';
|
|
655
|
+
if (fieldLower.includes('password')) return 'secretpassword';
|
|
656
|
+
if (fieldLower.includes('name') && fieldLower.includes('user')) return 'John Doe';
|
|
657
|
+
if (fieldLower.includes('name')) return 'Example Name';
|
|
658
|
+
if (fieldLower.includes('id')) return '123';
|
|
659
|
+
if (fieldLower.includes('page')) return '1';
|
|
660
|
+
if (fieldLower.includes('limit') || fieldLower.includes('per_page')) return '10';
|
|
661
|
+
if (fieldLower.includes('token')) return 'your_token_here';
|
|
662
|
+
if (fieldLower.includes('url')) return 'https://example.com';
|
|
663
|
+
|
|
664
|
+
// Defaults based on type
|
|
665
|
+
switch (typeName) {
|
|
666
|
+
case 'number':
|
|
667
|
+
case 'integer':
|
|
668
|
+
case 'int':
|
|
669
|
+
return '1';
|
|
670
|
+
case 'boolean':
|
|
671
|
+
case 'bool':
|
|
672
|
+
return 'true';
|
|
673
|
+
case 'array':
|
|
674
|
+
return '[]';
|
|
675
|
+
case 'object':
|
|
676
|
+
return '{}';
|
|
677
|
+
default:
|
|
678
|
+
return 'value';
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function generateCurlSample(method, url, headers, body, hasBody) {
|
|
683
|
+
let cmd = `curl -X ${method} "${url}"`;
|
|
684
|
+
|
|
685
|
+
// Add headers
|
|
686
|
+
headers.forEach(h => {
|
|
687
|
+
cmd += ` \\\n -H "${h.field}: ${getSampleValue(h)}"`;
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
if (hasBody) {
|
|
691
|
+
cmd += ` \\\n -H "Content-Type: application/json"`;
|
|
692
|
+
cmd += ` \\\n -d '${JSON.stringify(body, null, 2).replace(/'/g, "\\'")}'`;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return cmd;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function generateFetchSample(method, url, headers, body, hasBody) {
|
|
699
|
+
let code = `fetch("${url}", {\n`;
|
|
700
|
+
code += ` method: "${method}",\n`;
|
|
701
|
+
code += ` headers: {\n`;
|
|
702
|
+
|
|
703
|
+
if (hasBody) {
|
|
704
|
+
code += ` "Content-Type": "application/json",\n`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
headers.forEach(h => {
|
|
708
|
+
code += ` "${h.field}": "${getSampleValue(h)}",\n`;
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
code += ` },\n`;
|
|
712
|
+
|
|
713
|
+
if (hasBody) {
|
|
714
|
+
code += ` body: JSON.stringify(${JSON.stringify(body, null, 4).split('\n').join('\n ')}),\n`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
code += `})\n`;
|
|
718
|
+
code += `.then(response => response.json())\n`;
|
|
719
|
+
code += `.then(data => console.log(data))\n`;
|
|
720
|
+
code += `.catch(error => console.error("Error:", error));`;
|
|
721
|
+
|
|
722
|
+
return code;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function generateAxiosSample(method, url, headers, body, hasBody) {
|
|
726
|
+
const methodLower = method.toLowerCase();
|
|
727
|
+
let code = `import axios from 'axios';\n\n`;
|
|
728
|
+
|
|
729
|
+
if (hasBody) {
|
|
730
|
+
code += `axios.${methodLower}("${url}", ${JSON.stringify(body, null, 2)}, {\n`;
|
|
731
|
+
} else {
|
|
732
|
+
code += `axios.${methodLower}("${url}", {\n`;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
code += ` headers: {\n`;
|
|
736
|
+
headers.forEach(h => {
|
|
737
|
+
code += ` "${h.field}": "${getSampleValue(h)}",\n`;
|
|
738
|
+
});
|
|
739
|
+
code += ` },\n`;
|
|
740
|
+
code += `})\n`;
|
|
741
|
+
code += `.then(response => console.log(response.data))\n`;
|
|
742
|
+
code += `.catch(error => console.error(error));`;
|
|
743
|
+
|
|
744
|
+
return code;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function generatePythonSample(method, url, headers, body, hasBody) {
|
|
748
|
+
let code = `import requests\n\n`;
|
|
749
|
+
code += `url = "${url}"\n\n`;
|
|
750
|
+
|
|
751
|
+
code += `headers = {\n`;
|
|
752
|
+
if (hasBody) {
|
|
753
|
+
code += ` "Content-Type": "application/json",\n`;
|
|
754
|
+
}
|
|
755
|
+
headers.forEach(h => {
|
|
756
|
+
code += ` "${h.field}": "${getSampleValue(h)}",\n`;
|
|
757
|
+
});
|
|
758
|
+
code += `}\n\n`;
|
|
759
|
+
|
|
760
|
+
if (hasBody) {
|
|
761
|
+
code += `data = ${JSON.stringify(body, null, 4)}\n\n`;
|
|
762
|
+
code += `response = requests.${method.toLowerCase()}(url, headers=headers, json=data)\n`;
|
|
763
|
+
} else {
|
|
764
|
+
code += `response = requests.${method.toLowerCase()}(url, headers=headers)\n`;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
code += `print(response.json())`;
|
|
768
|
+
|
|
769
|
+
return code;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function generatePhpSample(method, url, headers, body, hasBody) {
|
|
773
|
+
let code = `<?php\n\n`;
|
|
774
|
+
code += `$curl = curl_init();\n\n`;
|
|
775
|
+
code += `curl_setopt_array($curl, [\n`;
|
|
776
|
+
code += ` CURLOPT_URL => "${url}",\n`;
|
|
777
|
+
code += ` CURLOPT_RETURNTRANSFER => true,\n`;
|
|
778
|
+
code += ` CURLOPT_CUSTOMREQUEST => "${method}",\n`;
|
|
779
|
+
|
|
780
|
+
// Build headers array
|
|
781
|
+
code += ` CURLOPT_HTTPHEADER => [\n`;
|
|
782
|
+
if (hasBody) {
|
|
783
|
+
code += ` "Content-Type: application/json",\n`;
|
|
784
|
+
}
|
|
785
|
+
headers.forEach(h => {
|
|
786
|
+
code += ` "${h.field}: ${getSampleValue(h)}",\n`;
|
|
787
|
+
});
|
|
788
|
+
code += ` ],\n`;
|
|
789
|
+
|
|
790
|
+
if (hasBody) {
|
|
791
|
+
code += ` CURLOPT_POSTFIELDS => json_encode(${phpArrayToString(body)}),\n`;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
code += `]);\n\n`;
|
|
795
|
+
code += `$response = curl_exec($curl);\n`;
|
|
796
|
+
code += `$error = curl_error($curl);\n`;
|
|
797
|
+
code += `curl_close($curl);\n\n`;
|
|
798
|
+
code += `if ($error) {\n`;
|
|
799
|
+
code += ` echo "Error: " . $error;\n`;
|
|
800
|
+
code += `} else {\n`;
|
|
801
|
+
code += ` print_r(json_decode($response, true));\n`;
|
|
802
|
+
code += `}\n`;
|
|
803
|
+
code += `?>`;
|
|
804
|
+
|
|
805
|
+
return code;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function phpArrayToString(obj, indent = 8) {
|
|
809
|
+
const spaces = ' '.repeat(indent);
|
|
810
|
+
let result = '[\n';
|
|
811
|
+
|
|
812
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
813
|
+
if (typeof value === 'object' && value !== null) {
|
|
814
|
+
result += `${spaces} "${key}" => ${phpArrayToString(value, indent + 4)},\n`;
|
|
815
|
+
} else if (typeof value === 'string') {
|
|
816
|
+
result += `${spaces} "${key}" => "${value}",\n`;
|
|
817
|
+
} else {
|
|
818
|
+
result += `${spaces} "${key}" => ${value},\n`;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
result += `${spaces}]`;
|
|
823
|
+
return result;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Global functions for code samples
|
|
827
|
+
window.switchCodeSample = function(endpointId, lang) {
|
|
828
|
+
const container = document.querySelector(`.code-samples-container[data-endpoint-id="${endpointId}"]`);
|
|
829
|
+
if (!container) return;
|
|
830
|
+
|
|
831
|
+
// Update tabs
|
|
832
|
+
container.querySelectorAll('.code-sample-tab').forEach(tab => {
|
|
833
|
+
tab.classList.toggle('active', tab.dataset.lang === lang);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Update panels
|
|
837
|
+
container.querySelectorAll('.code-sample-panel').forEach(panel => {
|
|
838
|
+
panel.classList.toggle('hidden', panel.dataset.lang !== lang);
|
|
839
|
+
});
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
window.copyCodeSample = function(btn) {
|
|
843
|
+
const panel = btn.closest('.code-sample-panel');
|
|
844
|
+
const code = panel.querySelector('.code-sample-code').textContent;
|
|
845
|
+
|
|
846
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
847
|
+
const originalText = btn.textContent;
|
|
848
|
+
btn.textContent = 'Copied!';
|
|
849
|
+
setTimeout(() => {
|
|
850
|
+
btn.textContent = originalText;
|
|
851
|
+
}, 2000);
|
|
852
|
+
});
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
function renderParamsSection(title, params) {
|
|
856
|
+
if (!params || Object.keys(params).length === 0) return '';
|
|
857
|
+
|
|
858
|
+
let html = `<div class="params-section"><div class="params-section-title">${escapeHtml(title)}</div>`;
|
|
859
|
+
|
|
860
|
+
for (const [groupName, groupParams] of Object.entries(params)) {
|
|
861
|
+
if (!groupParams || groupParams.length === 0) continue;
|
|
862
|
+
|
|
863
|
+
// Skip showing group title if it's a default/redundant name
|
|
864
|
+
const skipGroupTitle = ['Parameter', 'default', 'Header', 'Query', 'Body'].includes(groupName);
|
|
865
|
+
if (!skipGroupTitle) {
|
|
866
|
+
html += `<div class="params-group-title">${escapeHtml(groupName)}</div>`;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
html += `
|
|
870
|
+
<table class="params-table">
|
|
871
|
+
<thead>
|
|
872
|
+
<tr>
|
|
873
|
+
<th>Field</th>
|
|
874
|
+
<th>Type</th>
|
|
875
|
+
<th></th>
|
|
876
|
+
<th>Description</th>
|
|
877
|
+
</tr>
|
|
878
|
+
</thead>
|
|
879
|
+
<tbody>
|
|
880
|
+
`;
|
|
881
|
+
|
|
882
|
+
for (const param of groupParams) {
|
|
883
|
+
const typeStr = formatType(param.type);
|
|
884
|
+
const requiredBadge = param.optional
|
|
885
|
+
? '<span class="param-optional">optional</span>'
|
|
886
|
+
: '<span class="param-required">required</span>';
|
|
887
|
+
const defaultStr = param.defaultValue ? `<span class="param-default">= ${escapeHtml(param.defaultValue)}</span>` : '';
|
|
888
|
+
|
|
889
|
+
html += `
|
|
890
|
+
<tr>
|
|
891
|
+
<td><span class="param-name">${escapeHtml(param.field)}</span></td>
|
|
892
|
+
<td><span class="param-type">${escapeHtml(typeStr)}</span>${defaultStr}</td>
|
|
893
|
+
<td>${requiredBadge}</td>
|
|
894
|
+
<td>${escapeHtml(param.description || '')}</td>
|
|
895
|
+
</tr>
|
|
896
|
+
`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
html += '</tbody></table>';
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
html += '</div>';
|
|
903
|
+
return html;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function renderResponseSection(title, responses, type) {
|
|
907
|
+
if (!responses || Object.keys(responses).length === 0) return '';
|
|
908
|
+
|
|
909
|
+
let html = `<div class="params-section"><div class="params-section-title ${type}">${escapeHtml(title)}</div>`;
|
|
910
|
+
|
|
911
|
+
for (const [groupName, groupResponses] of Object.entries(responses)) {
|
|
912
|
+
if (!groupResponses || groupResponses.length === 0) continue;
|
|
913
|
+
|
|
914
|
+
if (groupName !== 'Success 200' && groupName !== 'Error 4xx') {
|
|
915
|
+
html += `<div class="params-group-title">${escapeHtml(groupName)}</div>`;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
html += `
|
|
919
|
+
<table class="params-table">
|
|
920
|
+
<thead>
|
|
921
|
+
<tr>
|
|
922
|
+
<th>Field</th>
|
|
923
|
+
<th>Type</th>
|
|
924
|
+
<th>Description</th>
|
|
925
|
+
</tr>
|
|
926
|
+
</thead>
|
|
927
|
+
<tbody>
|
|
928
|
+
`;
|
|
929
|
+
|
|
930
|
+
for (const response of groupResponses) {
|
|
931
|
+
const typeStr = formatType(response.type);
|
|
932
|
+
|
|
933
|
+
html += `
|
|
934
|
+
<tr>
|
|
935
|
+
<td><span class="param-name">${escapeHtml(response.field)}</span></td>
|
|
936
|
+
<td><span class="param-type">${escapeHtml(typeStr)}</span></td>
|
|
937
|
+
<td>${escapeHtml(response.description || '')}</td>
|
|
938
|
+
</tr>
|
|
939
|
+
`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
html += '</tbody></table>';
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
html += '</div>';
|
|
946
|
+
return html;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function renderExamples(title, examples, type) {
|
|
950
|
+
if (!examples || examples.length === 0) return '';
|
|
951
|
+
|
|
952
|
+
const titleClass = type === 'success' ? 'success' : (type === 'error' ? 'error' : '');
|
|
953
|
+
|
|
954
|
+
let html = `<div class="example-section"><div class="example-title ${titleClass}">${escapeHtml(title)}</div>`;
|
|
955
|
+
|
|
956
|
+
for (const example of examples) {
|
|
957
|
+
const exampleTitle = example.title || example.type || 'Example';
|
|
958
|
+
const formattedContent = formatExampleContent(example.content, example.type);
|
|
959
|
+
const collapsedClass = settings.expandExamples ? '' : 'collapsed';
|
|
960
|
+
|
|
961
|
+
html += `
|
|
962
|
+
<div class="example-block ${collapsedClass}">
|
|
963
|
+
<div class="example-header" onclick="toggleExample(this)">
|
|
964
|
+
<span>${escapeHtml(exampleTitle)}</span>
|
|
965
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
966
|
+
<button class="copy-btn" onclick="event.stopPropagation(); copyCode(this)">Copy</button>
|
|
967
|
+
<svg class="example-toggle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
968
|
+
<path d="m6 9 6 6 6-6"></path>
|
|
969
|
+
</svg>
|
|
970
|
+
</div>
|
|
971
|
+
</div>
|
|
972
|
+
<div class="example-content">
|
|
973
|
+
<pre>${formattedContent}</pre>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
`;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
html += '</div>';
|
|
980
|
+
return html;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function renderTryItOut(endpoint) {
|
|
984
|
+
if (!apiData.template || !apiData.template.withGenerator) return '';
|
|
985
|
+
if (endpoint.sampleRequest === 'off') return '';
|
|
986
|
+
|
|
987
|
+
const baseUrl = getSelectedBaseUrl(endpoint);
|
|
988
|
+
// If no URL available at all, don't show Try it out
|
|
989
|
+
if (!baseUrl && !apiData.project.sampleUrl && (!apiData.project.environments || apiData.project.environments.length === 0)) return '';
|
|
990
|
+
const eid = escapeHtml(endpoint.id);
|
|
991
|
+
|
|
992
|
+
let html = `
|
|
993
|
+
<div class="try-it-section">
|
|
994
|
+
<div class="params-section-title">Try it out</div>
|
|
995
|
+
<div class="sample-request" data-endpoint-id="${eid}" data-method="${endpoint.method}" data-base-url="${escapeHtml(baseUrl)}" data-path="${escapeHtml(endpoint.path)}">
|
|
996
|
+
`;
|
|
997
|
+
|
|
998
|
+
// Headers
|
|
999
|
+
const allHeaders = getAllParams(endpoint.headers);
|
|
1000
|
+
if (allHeaders.length > 0) {
|
|
1001
|
+
html += `<div class="sample-section"><div class="sample-section-title">Headers</div>`;
|
|
1002
|
+
for (const header of allHeaders) {
|
|
1003
|
+
html += renderSampleField(eid, 'header', header);
|
|
1004
|
+
}
|
|
1005
|
+
html += `</div>`;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// URL Parameters
|
|
1009
|
+
const urlParams = extractUrlParams(endpoint.path);
|
|
1010
|
+
const pathParams = getAllParams(endpoint.parameters);
|
|
1011
|
+
if (urlParams.length > 0 || pathParams.length > 0) {
|
|
1012
|
+
html += `<div class="sample-section"><div class="sample-section-title">URL Parameters</div>`;
|
|
1013
|
+
for (const param of urlParams) {
|
|
1014
|
+
const paramInfo = pathParams.find(p => p.field === param) || { field: param, type: { name: 'String' }, optional: false };
|
|
1015
|
+
html += renderSampleField(eid, 'path', paramInfo);
|
|
1016
|
+
}
|
|
1017
|
+
html += `</div>`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Query Parameters
|
|
1021
|
+
const queryParams = getAllParams(endpoint.query);
|
|
1022
|
+
if (queryParams.length > 0) {
|
|
1023
|
+
html += `<div class="sample-section"><div class="sample-section-title">Query Parameters</div>`;
|
|
1024
|
+
for (const param of queryParams) {
|
|
1025
|
+
html += renderSampleField(eid, 'query', param);
|
|
1026
|
+
}
|
|
1027
|
+
html += `</div>`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Body Parameters
|
|
1031
|
+
const bodyParams = getAllParams(endpoint.body);
|
|
1032
|
+
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && bodyParams.length > 0) {
|
|
1033
|
+
html += `<div class="sample-section"><div class="sample-section-title">Body Parameters</div>`;
|
|
1034
|
+
for (const param of bodyParams) {
|
|
1035
|
+
html += renderSampleField(eid, 'body', param);
|
|
1036
|
+
}
|
|
1037
|
+
html += `</div>`;
|
|
1038
|
+
} else if (['POST', 'PUT', 'PATCH'].includes(endpoint.method)) {
|
|
1039
|
+
html += `
|
|
1040
|
+
<div class="sample-section">
|
|
1041
|
+
<div class="sample-section-title">Request Body (JSON)</div>
|
|
1042
|
+
<textarea class="sample-body-raw" id="sample-body-raw-${eid}" rows="5" placeholder='{"key": "value"}'></textarea>
|
|
1043
|
+
</div>
|
|
1044
|
+
`;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
html += `
|
|
1048
|
+
<div class="sample-actions">
|
|
1049
|
+
<div class="sample-url-display">
|
|
1050
|
+
<span class="sample-method ${endpoint.method.toLowerCase()}">${endpoint.method}</span>
|
|
1051
|
+
<span class="sample-url-text" id="sample-url-preview-${eid}">${escapeHtml(baseUrl + endpoint.path)}</span>
|
|
1052
|
+
</div>
|
|
1053
|
+
<button class="sample-btn" onclick="sendRequest('${eid}')">Send Request</button>
|
|
1054
|
+
</div>
|
|
1055
|
+
<div class="sample-response-wrapper">
|
|
1056
|
+
<div class="sample-response-header">
|
|
1057
|
+
<span>Response</span>
|
|
1058
|
+
<span class="sample-status" id="sample-status-${eid}"></span>
|
|
1059
|
+
</div>
|
|
1060
|
+
<pre class="sample-response" id="sample-response-${eid}">Click "Send Request" to see the response...</pre>
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
`;
|
|
1065
|
+
|
|
1066
|
+
return html;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function getAllParams(paramsObj) {
|
|
1070
|
+
if (!paramsObj) return [];
|
|
1071
|
+
const all = [];
|
|
1072
|
+
for (const group of Object.values(paramsObj)) {
|
|
1073
|
+
all.push(...group);
|
|
1074
|
+
}
|
|
1075
|
+
return all;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function extractUrlParams(path) {
|
|
1079
|
+
const matches = path.match(/:(\w+)/g) || [];
|
|
1080
|
+
return matches.map(m => m.slice(1));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function renderSampleField(endpointId, type, param) {
|
|
1084
|
+
const fieldId = `sample-${type}-${endpointId}-${param.field}`;
|
|
1085
|
+
const isRequired = !param.optional;
|
|
1086
|
+
const typeStr = param.type ? param.type.name : 'String';
|
|
1087
|
+
const defaultVal = param.defaultValue || '';
|
|
1088
|
+
const typeLower = typeStr.toLowerCase();
|
|
1089
|
+
|
|
1090
|
+
let inputType = 'text';
|
|
1091
|
+
if (typeLower === 'number') inputType = 'number';
|
|
1092
|
+
if (typeLower === 'boolean') inputType = 'checkbox';
|
|
1093
|
+
if (param.field.toLowerCase().includes('password')) inputType = 'password';
|
|
1094
|
+
|
|
1095
|
+
// Check if it's a file/image type
|
|
1096
|
+
const isFileType = typeLower === 'file' || typeLower === 'image' ||
|
|
1097
|
+
param.field.toLowerCase().includes('image') ||
|
|
1098
|
+
param.field.toLowerCase().includes('file') ||
|
|
1099
|
+
param.field.toLowerCase().includes('avatar') ||
|
|
1100
|
+
param.field.toLowerCase().includes('photo');
|
|
1101
|
+
|
|
1102
|
+
let inputHtml;
|
|
1103
|
+
if (param.type && param.type.allowedValues) {
|
|
1104
|
+
inputHtml = `<select class="sample-input" id="${fieldId}" data-field="${escapeHtml(param.field)}" data-type="${type}">
|
|
1105
|
+
<option value="">-- Select --</option>
|
|
1106
|
+
${param.type.allowedValues.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('')}
|
|
1107
|
+
</select>`;
|
|
1108
|
+
} else if (isFileType) {
|
|
1109
|
+
inputHtml = `
|
|
1110
|
+
<div class="sample-file-wrapper">
|
|
1111
|
+
<input type="file" class="sample-file-input" id="${fieldId}" data-field="${escapeHtml(param.field)}" data-type="${type}" data-is-file="true" accept="image/*">
|
|
1112
|
+
<label for="${fieldId}" class="sample-file-label">
|
|
1113
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1114
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
1115
|
+
<polyline points="17 8 12 3 7 8"></polyline>
|
|
1116
|
+
<line x1="12" y1="3" x2="12" y2="15"></line>
|
|
1117
|
+
</svg>
|
|
1118
|
+
<span class="sample-file-text">Choose file...</span>
|
|
1119
|
+
</label>
|
|
1120
|
+
<span class="sample-file-name" id="${fieldId}-name"></span>
|
|
1121
|
+
</div>`;
|
|
1122
|
+
} else if (inputType === 'checkbox') {
|
|
1123
|
+
inputHtml = `<input type="checkbox" class="sample-checkbox" id="${fieldId}" data-field="${escapeHtml(param.field)}" data-type="${type}">`;
|
|
1124
|
+
} else {
|
|
1125
|
+
inputHtml = `<input type="${inputType}" class="sample-input" id="${fieldId}" data-field="${escapeHtml(param.field)}" data-type="${type}" value="${escapeHtml(defaultVal)}" placeholder="${escapeHtml(param.description || param.field)}">`;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return `
|
|
1129
|
+
<div class="sample-field">
|
|
1130
|
+
<label class="sample-label" for="${fieldId}">
|
|
1131
|
+
${escapeHtml(param.field)}
|
|
1132
|
+
${isRequired ? '<span class="required">*</span>' : ''}
|
|
1133
|
+
<span class="sample-type">${escapeHtml(typeStr)}</span>
|
|
1134
|
+
</label>
|
|
1135
|
+
${inputHtml}
|
|
1136
|
+
</div>
|
|
1137
|
+
`;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function formatType(type) {
|
|
1141
|
+
if (!type) return 'String';
|
|
1142
|
+
|
|
1143
|
+
let str = type.name;
|
|
1144
|
+
if (type.isArray) str += '[]';
|
|
1145
|
+
if (type.size) str += `{${type.size}}`;
|
|
1146
|
+
if (type.allowedValues) str += `=${type.allowedValues.join(',')}`;
|
|
1147
|
+
|
|
1148
|
+
return str;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function formatExampleContent(content, type) {
|
|
1152
|
+
if (!content) return '';
|
|
1153
|
+
|
|
1154
|
+
let formatted = content.trim();
|
|
1155
|
+
|
|
1156
|
+
if (type === 'json' || looksLikeJson(formatted)) {
|
|
1157
|
+
try {
|
|
1158
|
+
const parsed = JSON.parse(formatted);
|
|
1159
|
+
formatted = JSON.stringify(parsed, null, 2);
|
|
1160
|
+
return syntaxHighlightJson(formatted);
|
|
1161
|
+
} catch (e) {
|
|
1162
|
+
// Not valid JSON
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return escapeHtml(formatted);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function looksLikeJson(str) {
|
|
1170
|
+
const trimmed = str.trim();
|
|
1171
|
+
return (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
1172
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'));
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function syntaxHighlightJson(json) {
|
|
1176
|
+
const PH = {
|
|
1177
|
+
KEY_OPEN: '___KEY_OPEN___',
|
|
1178
|
+
KEY_CLOSE: '___KEY_CLOSE___',
|
|
1179
|
+
STR_OPEN: '___STR_OPEN___',
|
|
1180
|
+
STR_CLOSE: '___STR_CLOSE___',
|
|
1181
|
+
NUM_OPEN: '___NUM_OPEN___',
|
|
1182
|
+
NUM_CLOSE: '___NUM_CLOSE___',
|
|
1183
|
+
KW_OPEN: '___KW_OPEN___',
|
|
1184
|
+
KW_CLOSE: '___KW_CLOSE___'
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
let result = json
|
|
1188
|
+
.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/g, PH.KEY_OPEN + '"$1"' + PH.KEY_CLOSE + ':')
|
|
1189
|
+
.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"/g, PH.STR_OPEN + '"$1"' + PH.STR_CLOSE)
|
|
1190
|
+
.replace(/\b(\d+\.?\d*)\b/g, PH.NUM_OPEN + '$1' + PH.NUM_CLOSE)
|
|
1191
|
+
.replace(/\b(true|false|null)\b/g, PH.KW_OPEN + '$1' + PH.KW_CLOSE);
|
|
1192
|
+
|
|
1193
|
+
result = result
|
|
1194
|
+
.replace(/&/g, '&')
|
|
1195
|
+
.replace(/</g, '<')
|
|
1196
|
+
.replace(/>/g, '>');
|
|
1197
|
+
|
|
1198
|
+
result = result
|
|
1199
|
+
.replace(/___KEY_OPEN___/g, '<span class="json-key">')
|
|
1200
|
+
.replace(/___KEY_CLOSE___/g, '</span>')
|
|
1201
|
+
.replace(/___STR_OPEN___/g, '<span class="json-string">')
|
|
1202
|
+
.replace(/___STR_CLOSE___/g, '</span>')
|
|
1203
|
+
.replace(/___NUM_OPEN___/g, '<span class="json-number">')
|
|
1204
|
+
.replace(/___NUM_CLOSE___/g, '</span>')
|
|
1205
|
+
.replace(/___KW_OPEN___/g, '<span class="json-keyword">')
|
|
1206
|
+
.replace(/___KW_CLOSE___/g, '</span>');
|
|
1207
|
+
|
|
1208
|
+
return result;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Search functionality
|
|
1212
|
+
function performSearch(query) {
|
|
1213
|
+
if (!query) {
|
|
1214
|
+
clearSearch();
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const lowerQuery = query.toLowerCase();
|
|
1219
|
+
const results = {};
|
|
1220
|
+
const data = getActiveData();
|
|
1221
|
+
|
|
1222
|
+
data.groups.forEach(group => {
|
|
1223
|
+
const matchingEndpoints = group.endpoints.filter(ep => {
|
|
1224
|
+
return ep.title.toLowerCase().includes(lowerQuery) ||
|
|
1225
|
+
ep.path.toLowerCase().includes(lowerQuery) ||
|
|
1226
|
+
ep.method.toLowerCase().includes(lowerQuery) ||
|
|
1227
|
+
(ep.description && ep.description.toLowerCase().includes(lowerQuery));
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
if (matchingEndpoints.length > 0) {
|
|
1231
|
+
results[group.name] = matchingEndpoints;
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
renderSearchResults(query, results);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function renderSearchResults(query, results) {
|
|
1239
|
+
searchQuery.textContent = query;
|
|
1240
|
+
|
|
1241
|
+
if (Object.keys(results).length === 0) {
|
|
1242
|
+
searchResults.innerHTML = '<div style="padding: 2rem; text-align: center; color: var(--muted-foreground);">No results found</div>';
|
|
1243
|
+
} else {
|
|
1244
|
+
let html = '';
|
|
1245
|
+
|
|
1246
|
+
for (const [groupName, endpoints] of Object.entries(results)) {
|
|
1247
|
+
const color = getGroupColor(groupName);
|
|
1248
|
+
|
|
1249
|
+
html += `
|
|
1250
|
+
<div class="search-group">
|
|
1251
|
+
<div class="search-group-header">
|
|
1252
|
+
<div class="group-color-bar" style="background-color: ${color}"></div>
|
|
1253
|
+
<span class="group-name">${escapeHtml(groupName)}</span>
|
|
1254
|
+
</div>
|
|
1255
|
+
<div class="endpoints-list">
|
|
1256
|
+
`;
|
|
1257
|
+
|
|
1258
|
+
endpoints.forEach(endpoint => {
|
|
1259
|
+
html += renderEndpointItem(endpoint);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
html += '</div></div>';
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
searchResults.innerHTML = html;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Show search view
|
|
1269
|
+
groupsView.classList.add('hidden');
|
|
1270
|
+
endpointsView.classList.add('hidden');
|
|
1271
|
+
searchView.classList.remove('hidden');
|
|
1272
|
+
searchClear.classList.remove('hidden');
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function clearSearch() {
|
|
1276
|
+
searchInput.value = '';
|
|
1277
|
+
searchClear.classList.add('hidden');
|
|
1278
|
+
|
|
1279
|
+
if (currentGroup) {
|
|
1280
|
+
showGroupEndpoints(currentGroup);
|
|
1281
|
+
} else {
|
|
1282
|
+
showGroupsView();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Event listeners
|
|
1287
|
+
function setupEventListeners() {
|
|
1288
|
+
// Logo click - go home
|
|
1289
|
+
const logoLink = document.getElementById('logo-link');
|
|
1290
|
+
if (logoLink) {
|
|
1291
|
+
logoLink.addEventListener('click', (e) => {
|
|
1292
|
+
e.preventDefault();
|
|
1293
|
+
showGroupsView();
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Export dropdown
|
|
1298
|
+
const exportDropdownToggle = document.getElementById('export-dropdown-toggle');
|
|
1299
|
+
const exportDropdownMenu = document.getElementById('export-dropdown-menu');
|
|
1300
|
+
const exportDropdownWrapper = exportDropdownToggle?.closest('.export-dropdown-wrapper');
|
|
1301
|
+
|
|
1302
|
+
if (exportDropdownToggle && exportDropdownMenu) {
|
|
1303
|
+
exportDropdownToggle.addEventListener('click', (e) => {
|
|
1304
|
+
e.stopPropagation();
|
|
1305
|
+
exportDropdownMenu.classList.toggle('hidden');
|
|
1306
|
+
exportDropdownWrapper?.classList.toggle('open');
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
// Close dropdown on outside click
|
|
1310
|
+
document.addEventListener('click', (e) => {
|
|
1311
|
+
if (!exportDropdownMenu.classList.contains('hidden') &&
|
|
1312
|
+
!exportDropdownMenu.contains(e.target) &&
|
|
1313
|
+
!exportDropdownToggle.contains(e.target)) {
|
|
1314
|
+
exportDropdownMenu.classList.add('hidden');
|
|
1315
|
+
exportDropdownWrapper?.classList.remove('open');
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
// Close dropdown after clicking an item
|
|
1320
|
+
exportDropdownMenu.addEventListener('click', () => {
|
|
1321
|
+
exportDropdownMenu.classList.add('hidden');
|
|
1322
|
+
exportDropdownWrapper?.classList.remove('open');
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Settings panel
|
|
1327
|
+
const settingsBtn = document.getElementById('settings-btn');
|
|
1328
|
+
const settingsPanel = document.getElementById('settings-panel');
|
|
1329
|
+
const settingsClose = document.getElementById('settings-close');
|
|
1330
|
+
|
|
1331
|
+
if (settingsBtn && settingsPanel) {
|
|
1332
|
+
settingsBtn.addEventListener('click', () => {
|
|
1333
|
+
settingsPanel.classList.toggle('hidden');
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
settingsClose.addEventListener('click', () => {
|
|
1337
|
+
settingsPanel.classList.add('hidden');
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
// Close on outside click
|
|
1341
|
+
document.addEventListener('click', (e) => {
|
|
1342
|
+
if (!settingsPanel.classList.contains('hidden') &&
|
|
1343
|
+
!settingsPanel.contains(e.target) &&
|
|
1344
|
+
!settingsBtn.contains(e.target)) {
|
|
1345
|
+
settingsPanel.classList.add('hidden');
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// Settings toggles
|
|
1350
|
+
const settingMappings = {
|
|
1351
|
+
'setting-expand-examples': 'expandExamples',
|
|
1352
|
+
'setting-expand-endpoints': 'expandEndpoints',
|
|
1353
|
+
'setting-auto-collapse': 'autoCollapse',
|
|
1354
|
+
'setting-show-tryit': 'showTryit',
|
|
1355
|
+
'setting-show-codesamples': 'showCodeSamples',
|
|
1356
|
+
'setting-compact-mode': 'compactMode'
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
for (const [id, key] of Object.entries(settingMappings)) {
|
|
1360
|
+
const toggle = document.getElementById(id);
|
|
1361
|
+
if (toggle) {
|
|
1362
|
+
toggle.addEventListener('change', (e) => {
|
|
1363
|
+
settings[key] = e.target.checked;
|
|
1364
|
+
|
|
1365
|
+
// Mutually exclusive: expandEndpoints and autoCollapse
|
|
1366
|
+
if (key === 'expandEndpoints' && e.target.checked) {
|
|
1367
|
+
settings.autoCollapse = false;
|
|
1368
|
+
document.getElementById('setting-auto-collapse').checked = false;
|
|
1369
|
+
applySettingsToCurrentView('autoCollapse', false);
|
|
1370
|
+
} else if (key === 'autoCollapse' && e.target.checked) {
|
|
1371
|
+
settings.expandEndpoints = false;
|
|
1372
|
+
document.getElementById('setting-expand-endpoints').checked = false;
|
|
1373
|
+
applySettingsToCurrentView('expandEndpoints', false);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
saveSettings();
|
|
1377
|
+
applySettingsToCurrentView(key, e.target.checked);
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function applySettingsToCurrentView(key, value) {
|
|
1384
|
+
// Apply expand/collapse to examples
|
|
1385
|
+
if (key === 'expandExamples') {
|
|
1386
|
+
document.querySelectorAll('.example-block').forEach(block => {
|
|
1387
|
+
if (value) {
|
|
1388
|
+
block.classList.remove('collapsed');
|
|
1389
|
+
} else {
|
|
1390
|
+
block.classList.add('collapsed');
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Apply expand/collapse to endpoints
|
|
1396
|
+
if (key === 'expandEndpoints') {
|
|
1397
|
+
document.querySelectorAll('.endpoint-item').forEach(item => {
|
|
1398
|
+
const details = item.querySelector('.endpoint-details');
|
|
1399
|
+
if (value) {
|
|
1400
|
+
item.classList.add('expanded');
|
|
1401
|
+
details.classList.remove('hidden');
|
|
1402
|
+
} else {
|
|
1403
|
+
item.classList.remove('expanded');
|
|
1404
|
+
details.classList.add('hidden');
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Group click
|
|
1411
|
+
groupsList.addEventListener('click', (e) => {
|
|
1412
|
+
const groupRow = e.target.closest('.group-row');
|
|
1413
|
+
if (groupRow) {
|
|
1414
|
+
showGroupEndpoints(groupRow.dataset.group);
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// Back button
|
|
1419
|
+
backToGroups.addEventListener('click', showGroupsView);
|
|
1420
|
+
|
|
1421
|
+
// Endpoint expand/collapse (for endpoints view and search view)
|
|
1422
|
+
document.addEventListener('click', (e) => {
|
|
1423
|
+
const endpointHeader = e.target.closest('.endpoint-header');
|
|
1424
|
+
if (endpointHeader) {
|
|
1425
|
+
const endpointItem = endpointHeader.closest('.endpoint-item');
|
|
1426
|
+
const details = endpointItem.querySelector('.endpoint-details');
|
|
1427
|
+
|
|
1428
|
+
// Collapse other expanded endpoints in same list (if autoCollapse enabled)
|
|
1429
|
+
if (settings.autoCollapse) {
|
|
1430
|
+
const parentList = endpointItem.closest('.endpoints-list, .search-results');
|
|
1431
|
+
if (parentList) {
|
|
1432
|
+
parentList.querySelectorAll('.endpoint-item.expanded').forEach(item => {
|
|
1433
|
+
if (item !== endpointItem) {
|
|
1434
|
+
item.classList.remove('expanded');
|
|
1435
|
+
item.querySelector('.endpoint-details').classList.add('hidden');
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Toggle current
|
|
1442
|
+
endpointItem.classList.toggle('expanded');
|
|
1443
|
+
details.classList.toggle('hidden');
|
|
1444
|
+
|
|
1445
|
+
// Update URL hash
|
|
1446
|
+
if (endpointItem.classList.contains('expanded')) {
|
|
1447
|
+
window.history.pushState({}, '', `#${endpointItem.dataset.id}`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
// Search
|
|
1453
|
+
searchInput.addEventListener('input', debounce((e) => {
|
|
1454
|
+
performSearch(e.target.value.trim());
|
|
1455
|
+
}, 300));
|
|
1456
|
+
|
|
1457
|
+
searchClear.addEventListener('click', clearSearch);
|
|
1458
|
+
clearSearchBtn.addEventListener('click', clearSearch);
|
|
1459
|
+
|
|
1460
|
+
// File input change handler (delegated)
|
|
1461
|
+
document.addEventListener('change', (e) => {
|
|
1462
|
+
if (e.target.classList.contains('sample-file-input')) {
|
|
1463
|
+
const fileInput = e.target;
|
|
1464
|
+
const fileNameSpan = document.getElementById(fileInput.id + '-name');
|
|
1465
|
+
const fileLabelText = fileInput.parentElement.querySelector('.sample-file-text');
|
|
1466
|
+
|
|
1467
|
+
if (fileInput.files && fileInput.files.length > 0) {
|
|
1468
|
+
const fileName = fileInput.files[0].name;
|
|
1469
|
+
if (fileNameSpan) fileNameSpan.textContent = fileName;
|
|
1470
|
+
if (fileLabelText) fileLabelText.textContent = 'Change file...';
|
|
1471
|
+
} else {
|
|
1472
|
+
if (fileNameSpan) fileNameSpan.textContent = '';
|
|
1473
|
+
if (fileLabelText) fileLabelText.textContent = 'Choose file...';
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
// Handle browser back/forward
|
|
1479
|
+
window.addEventListener('popstate', handleInitialHash);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function handleInitialHash() {
|
|
1483
|
+
const hash = window.location.hash.slice(1);
|
|
1484
|
+
|
|
1485
|
+
if (!hash) {
|
|
1486
|
+
showGroupsView();
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Check if it's a group hash
|
|
1491
|
+
if (hash.startsWith('group-')) {
|
|
1492
|
+
const groupName = decodeURIComponent(hash.slice(6));
|
|
1493
|
+
showGroupEndpoints(groupName);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Otherwise it's an endpoint ID - find it
|
|
1498
|
+
const data = getActiveData();
|
|
1499
|
+
for (const group of data.groups) {
|
|
1500
|
+
const endpoint = group.endpoints.find(ep => ep.id === hash);
|
|
1501
|
+
if (endpoint) {
|
|
1502
|
+
showGroupEndpoints(group.name);
|
|
1503
|
+
// Expand the endpoint after view switch
|
|
1504
|
+
setTimeout(() => {
|
|
1505
|
+
const endpointItem = document.querySelector(`.endpoint-item[data-id="${hash}"]`);
|
|
1506
|
+
if (endpointItem) {
|
|
1507
|
+
endpointItem.classList.add('expanded');
|
|
1508
|
+
endpointItem.querySelector('.endpoint-details').classList.remove('hidden');
|
|
1509
|
+
endpointItem.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1510
|
+
}
|
|
1511
|
+
}, 100);
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function debounce(fn, delay) {
|
|
1518
|
+
let timer = null;
|
|
1519
|
+
return function(...args) {
|
|
1520
|
+
clearTimeout(timer);
|
|
1521
|
+
timer = setTimeout(() => fn.apply(this, args), delay);
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function escapeHtml(str) {
|
|
1526
|
+
if (str === null || str === undefined) return '';
|
|
1527
|
+
return String(str)
|
|
1528
|
+
.replace(/&/g, '&')
|
|
1529
|
+
.replace(/</g, '<')
|
|
1530
|
+
.replace(/>/g, '>')
|
|
1531
|
+
.replace(/"/g, '"')
|
|
1532
|
+
.replace(/'/g, ''');
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Markdown parser
|
|
1536
|
+
const marked = {
|
|
1537
|
+
parse: function(text) {
|
|
1538
|
+
if (!text) return '';
|
|
1539
|
+
|
|
1540
|
+
let html = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
1541
|
+
|
|
1542
|
+
const codeBlocks = [];
|
|
1543
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
|
|
1544
|
+
const index = codeBlocks.length;
|
|
1545
|
+
codeBlocks.push(`<pre><code class="language-${lang}">${this.escapeHtml(code)}</code></pre>`);
|
|
1546
|
+
return `\n%%CODEBLOCK${index}%%\n`;
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
1550
|
+
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
|
|
1551
|
+
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
1552
|
+
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
1553
|
+
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
1554
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
1555
|
+
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
1556
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
1557
|
+
html = html.replace(/^\- (.+)$/gm, '<li>$1</li>');
|
|
1558
|
+
html = html.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
|
|
1559
|
+
html = html.replace(/<\/ul>\s*<ul>/g, '');
|
|
1560
|
+
html = html.replace(/\n\n+/g, '</p><p>');
|
|
1561
|
+
html = html.replace(/([^>\n])\n([^<\n])/g, '$1<br>$2');
|
|
1562
|
+
|
|
1563
|
+
codeBlocks.forEach((block, index) => {
|
|
1564
|
+
html = html.replace(`%%CODEBLOCK${index}%%`, block);
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
html = '<p>' + html + '</p>';
|
|
1568
|
+
html = html.replace(/<p>\s*(<h[1-4]|<ul|<pre)/g, '$1');
|
|
1569
|
+
html = html.replace(/(<\/h[1-4]>|<\/ul>|<\/pre>)\s*<\/p>/g, '$1');
|
|
1570
|
+
html = html.replace(/<p>\s*<\/p>/g, '');
|
|
1571
|
+
|
|
1572
|
+
return html;
|
|
1573
|
+
},
|
|
1574
|
+
escapeHtml: function(str) {
|
|
1575
|
+
return String(str)
|
|
1576
|
+
.replace(/&/g, '&')
|
|
1577
|
+
.replace(/</g, '<')
|
|
1578
|
+
.replace(/>/g, '>');
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
// ========================================
|
|
1583
|
+
// OpenAPI/Swagger Export Functions
|
|
1584
|
+
// ========================================
|
|
1585
|
+
|
|
1586
|
+
function convertToOpenAPI(groups, options = {}) {
|
|
1587
|
+
const includeAllGroups = options.allGroups || false;
|
|
1588
|
+
const groupsToExport = includeAllGroups ? apiData.groups : groups;
|
|
1589
|
+
|
|
1590
|
+
const openapi = {
|
|
1591
|
+
openapi: '3.0.0',
|
|
1592
|
+
info: {
|
|
1593
|
+
title: apiData.project.name || apiData.project.title || 'API Documentation',
|
|
1594
|
+
version: apiData.project.version || '1.0.0',
|
|
1595
|
+
description: apiData.project.description || ''
|
|
1596
|
+
},
|
|
1597
|
+
servers: []
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
// Add server URL if available
|
|
1601
|
+
if (apiData.project.url) {
|
|
1602
|
+
openapi.servers.push({
|
|
1603
|
+
url: apiData.project.url,
|
|
1604
|
+
description: 'API Server'
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
if (apiData.project.sampleUrl && apiData.project.sampleUrl !== apiData.project.url) {
|
|
1608
|
+
openapi.servers.push({
|
|
1609
|
+
url: apiData.project.sampleUrl,
|
|
1610
|
+
description: 'Sample/Test Server'
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
openapi.paths = {};
|
|
1615
|
+
openapi.components = {
|
|
1616
|
+
schemas: {},
|
|
1617
|
+
securitySchemes: {}
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
const permissionsFound = new Set();
|
|
1621
|
+
|
|
1622
|
+
groupsToExport.forEach(group => {
|
|
1623
|
+
group.endpoints.forEach(endpoint => {
|
|
1624
|
+
const path = convertPathParams(endpoint.path);
|
|
1625
|
+
|
|
1626
|
+
if (!openapi.paths[path]) {
|
|
1627
|
+
openapi.paths[path] = {};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const method = endpoint.method.toLowerCase();
|
|
1631
|
+
const operation = {
|
|
1632
|
+
summary: endpoint.title || endpoint.name || '',
|
|
1633
|
+
description: endpoint.description || '',
|
|
1634
|
+
operationId: endpoint.id || `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
|
1635
|
+
tags: [group.name],
|
|
1636
|
+
parameters: [],
|
|
1637
|
+
responses: {}
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
// Deprecated flag
|
|
1641
|
+
if (endpoint.deprecated) {
|
|
1642
|
+
operation.deprecated = true;
|
|
1643
|
+
if (endpoint.deprecatedText) {
|
|
1644
|
+
operation.description += `\n\n**Deprecation Notice:** ${endpoint.deprecatedText}`;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// Version info
|
|
1649
|
+
if (endpoint.version) {
|
|
1650
|
+
operation.description += `\n\n**Version:** ${endpoint.version}`;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Permission/Security
|
|
1654
|
+
if (endpoint.permission) {
|
|
1655
|
+
permissionsFound.add(endpoint.permission);
|
|
1656
|
+
operation.security = [{ [endpoint.permission]: [] }];
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// Path parameters (from URL like :id or {id})
|
|
1660
|
+
const pathParams = extractPathParameters(endpoint.path);
|
|
1661
|
+
const allParams = getAllParams(endpoint.parameters);
|
|
1662
|
+
|
|
1663
|
+
pathParams.forEach(paramName => {
|
|
1664
|
+
const paramInfo = allParams.find(p => p.field === paramName);
|
|
1665
|
+
operation.parameters.push({
|
|
1666
|
+
name: paramName,
|
|
1667
|
+
in: 'path',
|
|
1668
|
+
required: true,
|
|
1669
|
+
schema: convertTypeToSchema(paramInfo?.type),
|
|
1670
|
+
description: paramInfo?.description || ''
|
|
1671
|
+
});
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
// Query parameters
|
|
1675
|
+
const queryParams = getAllParams(endpoint.query);
|
|
1676
|
+
queryParams.forEach(param => {
|
|
1677
|
+
operation.parameters.push({
|
|
1678
|
+
name: param.field,
|
|
1679
|
+
in: 'query',
|
|
1680
|
+
required: !param.optional,
|
|
1681
|
+
schema: convertTypeToSchema(param.type),
|
|
1682
|
+
description: param.description || '',
|
|
1683
|
+
...(param.defaultValue && { default: param.defaultValue })
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
// Header parameters
|
|
1688
|
+
const headerParams = getAllParams(endpoint.headers);
|
|
1689
|
+
headerParams.forEach(param => {
|
|
1690
|
+
operation.parameters.push({
|
|
1691
|
+
name: param.field,
|
|
1692
|
+
in: 'header',
|
|
1693
|
+
required: !param.optional,
|
|
1694
|
+
schema: convertTypeToSchema(param.type),
|
|
1695
|
+
description: param.description || '',
|
|
1696
|
+
...(param.defaultValue && { default: param.defaultValue })
|
|
1697
|
+
});
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
// Request body (for POST, PUT, PATCH)
|
|
1701
|
+
const bodyParams = getAllParams(endpoint.body);
|
|
1702
|
+
if (['post', 'put', 'patch'].includes(method) && bodyParams.length > 0) {
|
|
1703
|
+
const requiredFields = bodyParams.filter(p => !p.optional).map(p => p.field);
|
|
1704
|
+
const properties = {};
|
|
1705
|
+
|
|
1706
|
+
bodyParams.forEach(param => {
|
|
1707
|
+
properties[param.field] = {
|
|
1708
|
+
...convertTypeToSchema(param.type),
|
|
1709
|
+
description: param.description || '',
|
|
1710
|
+
...(param.defaultValue && { default: param.defaultValue })
|
|
1711
|
+
};
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
operation.requestBody = {
|
|
1715
|
+
required: requiredFields.length > 0,
|
|
1716
|
+
content: {
|
|
1717
|
+
'application/json': {
|
|
1718
|
+
schema: {
|
|
1719
|
+
type: 'object',
|
|
1720
|
+
properties: properties,
|
|
1721
|
+
...(requiredFields.length > 0 && { required: requiredFields })
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
// Add request examples
|
|
1728
|
+
if (endpoint.examples && endpoint.examples.length > 0) {
|
|
1729
|
+
operation.requestBody.content['application/json'].examples = {};
|
|
1730
|
+
endpoint.examples.forEach((ex, idx) => {
|
|
1731
|
+
const exampleName = ex.title || `example_${idx + 1}`;
|
|
1732
|
+
try {
|
|
1733
|
+
operation.requestBody.content['application/json'].examples[exampleName] = {
|
|
1734
|
+
summary: ex.title || '',
|
|
1735
|
+
value: JSON.parse(ex.content)
|
|
1736
|
+
};
|
|
1737
|
+
} catch (e) {
|
|
1738
|
+
operation.requestBody.content['application/json'].examples[exampleName] = {
|
|
1739
|
+
summary: ex.title || '',
|
|
1740
|
+
value: ex.content
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Success responses
|
|
1748
|
+
const successResponses = endpoint.success || {};
|
|
1749
|
+
let hasSuccessResponse = false;
|
|
1750
|
+
|
|
1751
|
+
for (const [groupName, responses] of Object.entries(successResponses)) {
|
|
1752
|
+
if (!responses || responses.length === 0) continue;
|
|
1753
|
+
hasSuccessResponse = true;
|
|
1754
|
+
|
|
1755
|
+
// Extract status code from group name (e.g., "Success 200" -> "200")
|
|
1756
|
+
const statusMatch = groupName.match(/(\d{3})/);
|
|
1757
|
+
const statusCode = statusMatch ? statusMatch[1] : '200';
|
|
1758
|
+
|
|
1759
|
+
const properties = {};
|
|
1760
|
+
responses.forEach(resp => {
|
|
1761
|
+
properties[resp.field] = {
|
|
1762
|
+
...convertTypeToSchema(resp.type),
|
|
1763
|
+
description: resp.description || ''
|
|
1764
|
+
};
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
operation.responses[statusCode] = {
|
|
1768
|
+
description: groupName,
|
|
1769
|
+
content: {
|
|
1770
|
+
'application/json': {
|
|
1771
|
+
schema: {
|
|
1772
|
+
type: 'object',
|
|
1773
|
+
properties: properties
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Add success examples
|
|
1781
|
+
if (endpoint.successExamples && endpoint.successExamples.length > 0) {
|
|
1782
|
+
const successStatus = Object.keys(operation.responses).find(s => s.startsWith('2')) || '200';
|
|
1783
|
+
if (!operation.responses[successStatus]) {
|
|
1784
|
+
operation.responses[successStatus] = {
|
|
1785
|
+
description: 'Successful response',
|
|
1786
|
+
content: { 'application/json': { schema: { type: 'object' } } }
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
operation.responses[successStatus].content['application/json'].examples = {};
|
|
1790
|
+
endpoint.successExamples.forEach((ex, idx) => {
|
|
1791
|
+
const exampleName = ex.title || `success_${idx + 1}`;
|
|
1792
|
+
try {
|
|
1793
|
+
operation.responses[successStatus].content['application/json'].examples[exampleName] = {
|
|
1794
|
+
summary: ex.title || '',
|
|
1795
|
+
value: JSON.parse(ex.content)
|
|
1796
|
+
};
|
|
1797
|
+
} catch (e) {
|
|
1798
|
+
operation.responses[successStatus].content['application/json'].examples[exampleName] = {
|
|
1799
|
+
summary: ex.title || '',
|
|
1800
|
+
value: ex.content
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Default success response if none defined
|
|
1807
|
+
if (!hasSuccessResponse) {
|
|
1808
|
+
operation.responses['200'] = {
|
|
1809
|
+
description: 'Successful response'
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// Error responses
|
|
1814
|
+
const errorResponses = endpoint.error || {};
|
|
1815
|
+
for (const [groupName, responses] of Object.entries(errorResponses)) {
|
|
1816
|
+
if (!responses || responses.length === 0) continue;
|
|
1817
|
+
|
|
1818
|
+
const statusMatch = groupName.match(/(\d{3})/);
|
|
1819
|
+
const statusCode = statusMatch ? statusMatch[1] : '400';
|
|
1820
|
+
|
|
1821
|
+
const properties = {};
|
|
1822
|
+
responses.forEach(resp => {
|
|
1823
|
+
properties[resp.field] = {
|
|
1824
|
+
...convertTypeToSchema(resp.type),
|
|
1825
|
+
description: resp.description || ''
|
|
1826
|
+
};
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
operation.responses[statusCode] = {
|
|
1830
|
+
description: groupName,
|
|
1831
|
+
content: {
|
|
1832
|
+
'application/json': {
|
|
1833
|
+
schema: {
|
|
1834
|
+
type: 'object',
|
|
1835
|
+
properties: properties
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// Add error examples
|
|
1843
|
+
if (endpoint.errorExamples && endpoint.errorExamples.length > 0) {
|
|
1844
|
+
const errorStatus = Object.keys(operation.responses).find(s => s.startsWith('4') || s.startsWith('5')) || '400';
|
|
1845
|
+
if (!operation.responses[errorStatus]) {
|
|
1846
|
+
operation.responses[errorStatus] = {
|
|
1847
|
+
description: 'Error response',
|
|
1848
|
+
content: { 'application/json': { schema: { type: 'object' } } }
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
operation.responses[errorStatus].content['application/json'].examples = {};
|
|
1852
|
+
endpoint.errorExamples.forEach((ex, idx) => {
|
|
1853
|
+
const exampleName = ex.title || `error_${idx + 1}`;
|
|
1854
|
+
try {
|
|
1855
|
+
operation.responses[errorStatus].content['application/json'].examples[exampleName] = {
|
|
1856
|
+
summary: ex.title || '',
|
|
1857
|
+
value: JSON.parse(ex.content)
|
|
1858
|
+
};
|
|
1859
|
+
} catch (e) {
|
|
1860
|
+
operation.responses[errorStatus].content['application/json'].examples[exampleName] = {
|
|
1861
|
+
summary: ex.title || '',
|
|
1862
|
+
value: ex.content
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
openapi.paths[path][method] = operation;
|
|
1869
|
+
});
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
// Add security schemes for found permissions
|
|
1873
|
+
permissionsFound.forEach(perm => {
|
|
1874
|
+
openapi.components.securitySchemes[perm] = {
|
|
1875
|
+
type: 'apiKey',
|
|
1876
|
+
in: 'header',
|
|
1877
|
+
name: 'Authorization',
|
|
1878
|
+
description: `Permission: ${perm}`
|
|
1879
|
+
};
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
// Clean up empty components
|
|
1883
|
+
if (Object.keys(openapi.components.schemas).length === 0) {
|
|
1884
|
+
delete openapi.components.schemas;
|
|
1885
|
+
}
|
|
1886
|
+
if (Object.keys(openapi.components.securitySchemes).length === 0) {
|
|
1887
|
+
delete openapi.components.securitySchemes;
|
|
1888
|
+
}
|
|
1889
|
+
if (Object.keys(openapi.components).length === 0) {
|
|
1890
|
+
delete openapi.components;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
return openapi;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function convertPathParams(path) {
|
|
1897
|
+
// Convert :paramName to {paramName} for OpenAPI format
|
|
1898
|
+
return path.replace(/:(\w+)/g, '{$1}');
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function extractPathParameters(path) {
|
|
1902
|
+
const matches = path.match(/[:{](\w+)[}]?/g) || [];
|
|
1903
|
+
return matches.map(m => m.replace(/[:{}\s]/g, ''));
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function convertTypeToSchema(type) {
|
|
1907
|
+
if (!type) return { type: 'string' };
|
|
1908
|
+
|
|
1909
|
+
const typeName = (type.name || 'string').toLowerCase();
|
|
1910
|
+
let schema = {};
|
|
1911
|
+
|
|
1912
|
+
// Map common types to OpenAPI types
|
|
1913
|
+
switch (typeName) {
|
|
1914
|
+
case 'string':
|
|
1915
|
+
case 'str':
|
|
1916
|
+
schema.type = 'string';
|
|
1917
|
+
break;
|
|
1918
|
+
case 'number':
|
|
1919
|
+
case 'int':
|
|
1920
|
+
case 'integer':
|
|
1921
|
+
schema.type = 'integer';
|
|
1922
|
+
break;
|
|
1923
|
+
case 'float':
|
|
1924
|
+
case 'double':
|
|
1925
|
+
case 'decimal':
|
|
1926
|
+
schema.type = 'number';
|
|
1927
|
+
break;
|
|
1928
|
+
case 'boolean':
|
|
1929
|
+
case 'bool':
|
|
1930
|
+
schema.type = 'boolean';
|
|
1931
|
+
break;
|
|
1932
|
+
case 'object':
|
|
1933
|
+
case 'json':
|
|
1934
|
+
schema.type = 'object';
|
|
1935
|
+
break;
|
|
1936
|
+
case 'array':
|
|
1937
|
+
schema.type = 'array';
|
|
1938
|
+
schema.items = { type: 'string' };
|
|
1939
|
+
break;
|
|
1940
|
+
case 'date':
|
|
1941
|
+
schema.type = 'string';
|
|
1942
|
+
schema.format = 'date';
|
|
1943
|
+
break;
|
|
1944
|
+
case 'datetime':
|
|
1945
|
+
schema.type = 'string';
|
|
1946
|
+
schema.format = 'date-time';
|
|
1947
|
+
break;
|
|
1948
|
+
case 'email':
|
|
1949
|
+
schema.type = 'string';
|
|
1950
|
+
schema.format = 'email';
|
|
1951
|
+
break;
|
|
1952
|
+
case 'url':
|
|
1953
|
+
case 'uri':
|
|
1954
|
+
schema.type = 'string';
|
|
1955
|
+
schema.format = 'uri';
|
|
1956
|
+
break;
|
|
1957
|
+
case 'file':
|
|
1958
|
+
schema.type = 'string';
|
|
1959
|
+
schema.format = 'binary';
|
|
1960
|
+
break;
|
|
1961
|
+
default:
|
|
1962
|
+
schema.type = 'string';
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// Handle arrays
|
|
1966
|
+
if (type.isArray) {
|
|
1967
|
+
schema = {
|
|
1968
|
+
type: 'array',
|
|
1969
|
+
items: schema
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// Handle enums (allowed values)
|
|
1974
|
+
if (type.allowedValues && type.allowedValues.length > 0) {
|
|
1975
|
+
if (schema.type === 'array') {
|
|
1976
|
+
schema.items.enum = type.allowedValues;
|
|
1977
|
+
} else {
|
|
1978
|
+
schema.enum = type.allowedValues;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// Handle size constraints
|
|
1983
|
+
if (type.size) {
|
|
1984
|
+
const sizeMatch = type.size.match(/(\d+)?\.\.(\d+)?/);
|
|
1985
|
+
if (sizeMatch) {
|
|
1986
|
+
const [, min, max] = sizeMatch;
|
|
1987
|
+
if (schema.type === 'string') {
|
|
1988
|
+
if (min) schema.minLength = parseInt(min);
|
|
1989
|
+
if (max) schema.maxLength = parseInt(max);
|
|
1990
|
+
} else if (schema.type === 'integer' || schema.type === 'number') {
|
|
1991
|
+
if (min) schema.minimum = parseInt(min);
|
|
1992
|
+
if (max) schema.maximum = parseInt(max);
|
|
1993
|
+
} else if (schema.type === 'array') {
|
|
1994
|
+
if (min) schema.minItems = parseInt(min);
|
|
1995
|
+
if (max) schema.maxItems = parseInt(max);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
return schema;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
function downloadOpenAPI(groupName) {
|
|
2004
|
+
let openapi;
|
|
2005
|
+
let filename;
|
|
2006
|
+
|
|
2007
|
+
if (groupName === '__all__') {
|
|
2008
|
+
openapi = convertToOpenAPI(apiData.groups, { allGroups: true });
|
|
2009
|
+
filename = `${(apiData.project.name || 'api').toLowerCase().replace(/\s+/g, '-')}-openapi.json`;
|
|
2010
|
+
} else {
|
|
2011
|
+
const group = apiData.groups.find(g => g.name === groupName);
|
|
2012
|
+
if (!group) return;
|
|
2013
|
+
openapi = convertToOpenAPI([group]);
|
|
2014
|
+
filename = `${groupName.toLowerCase().replace(/\s+/g, '-')}-openapi.json`;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const json = JSON.stringify(openapi, null, 2);
|
|
2018
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
2019
|
+
const url = URL.createObjectURL(blob);
|
|
2020
|
+
|
|
2021
|
+
const a = document.createElement('a');
|
|
2022
|
+
a.href = url;
|
|
2023
|
+
a.download = filename;
|
|
2024
|
+
document.body.appendChild(a);
|
|
2025
|
+
a.click();
|
|
2026
|
+
document.body.removeChild(a);
|
|
2027
|
+
URL.revokeObjectURL(url);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Global functions
|
|
2031
|
+
window.toggleExample = function(header) {
|
|
2032
|
+
const block = header.closest('.example-block');
|
|
2033
|
+
block.classList.toggle('collapsed');
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2036
|
+
window.copyCode = function(btn) {
|
|
2037
|
+
const block = btn.closest('.example-block');
|
|
2038
|
+
const code = block.querySelector('pre').textContent;
|
|
2039
|
+
|
|
2040
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
2041
|
+
const originalText = btn.textContent;
|
|
2042
|
+
btn.textContent = 'Copied!';
|
|
2043
|
+
setTimeout(() => {
|
|
2044
|
+
btn.textContent = originalText;
|
|
2045
|
+
}, 2000);
|
|
2046
|
+
});
|
|
2047
|
+
};
|
|
2048
|
+
|
|
2049
|
+
window.downloadOpenAPI = function(groupName) {
|
|
2050
|
+
downloadOpenAPI(groupName);
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
window.sendRequest = async function(endpointId) {
|
|
2054
|
+
const container = document.querySelector(`.sample-request[data-endpoint-id="${endpointId}"]`);
|
|
2055
|
+
const responseDiv = document.getElementById(`sample-response-${endpointId}`);
|
|
2056
|
+
const statusDiv = document.getElementById(`sample-status-${endpointId}`);
|
|
2057
|
+
const urlPreview = document.getElementById(`sample-url-preview-${endpointId}`);
|
|
2058
|
+
|
|
2059
|
+
if (!container) return;
|
|
2060
|
+
|
|
2061
|
+
const method = container.dataset.method;
|
|
2062
|
+
const baseUrl = container.dataset.baseUrl;
|
|
2063
|
+
let path = container.dataset.path;
|
|
2064
|
+
|
|
2065
|
+
const headers = { 'Accept': 'application/json' };
|
|
2066
|
+
container.querySelectorAll('[data-type="header"]').forEach(input => {
|
|
2067
|
+
const value = input.type === 'checkbox' ? input.checked : input.value;
|
|
2068
|
+
if (value) headers[input.dataset.field] = String(value);
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
container.querySelectorAll('[data-type="path"]').forEach(input => {
|
|
2072
|
+
const value = input.value;
|
|
2073
|
+
if (value) {
|
|
2074
|
+
path = path.replace(`:${input.dataset.field}`, encodeURIComponent(value));
|
|
2075
|
+
path = path.replace(`{${input.dataset.field}}`, encodeURIComponent(value));
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
const queryParams = new URLSearchParams();
|
|
2080
|
+
container.querySelectorAll('[data-type="query"]').forEach(input => {
|
|
2081
|
+
const value = input.type === 'checkbox' ? input.checked : input.value;
|
|
2082
|
+
if (value) queryParams.append(input.dataset.field, String(value));
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
let url = baseUrl + path;
|
|
2086
|
+
const queryString = queryParams.toString();
|
|
2087
|
+
if (queryString) url += '?' + queryString;
|
|
2088
|
+
|
|
2089
|
+
urlPreview.textContent = url;
|
|
2090
|
+
|
|
2091
|
+
let body = null;
|
|
2092
|
+
let hasFileInput = false;
|
|
2093
|
+
|
|
2094
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
2095
|
+
const bodyInputs = container.querySelectorAll('[data-type="body"]');
|
|
2096
|
+
const fileInputs = container.querySelectorAll('[data-type="body"][data-is-file="true"]');
|
|
2097
|
+
const rawBody = document.getElementById(`sample-body-raw-${endpointId}`);
|
|
2098
|
+
|
|
2099
|
+
// Check if there are file inputs with files selected
|
|
2100
|
+
fileInputs.forEach(input => {
|
|
2101
|
+
if (input.files && input.files.length > 0) {
|
|
2102
|
+
hasFileInput = true;
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
if (hasFileInput) {
|
|
2107
|
+
// Use FormData for file uploads
|
|
2108
|
+
body = new FormData();
|
|
2109
|
+
bodyInputs.forEach(input => {
|
|
2110
|
+
if (input.dataset.isFile === 'true') {
|
|
2111
|
+
if (input.files && input.files.length > 0) {
|
|
2112
|
+
body.append(input.dataset.field, input.files[0]);
|
|
2113
|
+
}
|
|
2114
|
+
} else {
|
|
2115
|
+
let value = input.type === 'checkbox' ? input.checked : input.value;
|
|
2116
|
+
if (value !== '' && value !== null && value !== false) {
|
|
2117
|
+
body.append(input.dataset.field, value);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
// Don't set Content-Type for FormData - browser will set it with boundary
|
|
2122
|
+
} else if (bodyInputs.length > 0) {
|
|
2123
|
+
const bodyObj = {};
|
|
2124
|
+
bodyInputs.forEach(input => {
|
|
2125
|
+
if (input.dataset.isFile !== 'true') {
|
|
2126
|
+
let value = input.type === 'checkbox' ? input.checked : input.value;
|
|
2127
|
+
if (input.type === 'number' && value) value = Number(value);
|
|
2128
|
+
if (value !== '' && value !== null) {
|
|
2129
|
+
setNestedValue(bodyObj, input.dataset.field, value);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
if (Object.keys(bodyObj).length > 0) {
|
|
2134
|
+
body = JSON.stringify(bodyObj);
|
|
2135
|
+
headers['Content-Type'] = 'application/json';
|
|
2136
|
+
}
|
|
2137
|
+
} else if (rawBody && rawBody.value.trim()) {
|
|
2138
|
+
body = rawBody.value.trim();
|
|
2139
|
+
headers['Content-Type'] = 'application/json';
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
responseDiv.textContent = 'Loading...';
|
|
2144
|
+
statusDiv.textContent = '';
|
|
2145
|
+
statusDiv.className = 'sample-status';
|
|
2146
|
+
|
|
2147
|
+
try {
|
|
2148
|
+
const fetchOptions = {
|
|
2149
|
+
method: method,
|
|
2150
|
+
body: body
|
|
2151
|
+
};
|
|
2152
|
+
|
|
2153
|
+
// Only add headers if not using FormData (FormData needs browser to set Content-Type)
|
|
2154
|
+
if (!hasFileInput) {
|
|
2155
|
+
fetchOptions.headers = headers;
|
|
2156
|
+
} else {
|
|
2157
|
+
// For FormData, only include non-Content-Type headers
|
|
2158
|
+
const formDataHeaders = { ...headers };
|
|
2159
|
+
delete formDataHeaders['Content-Type'];
|
|
2160
|
+
if (Object.keys(formDataHeaders).length > 0) {
|
|
2161
|
+
fetchOptions.headers = formDataHeaders;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
const response = await fetch(url, fetchOptions);
|
|
2166
|
+
|
|
2167
|
+
statusDiv.textContent = `${response.status} ${response.statusText}`;
|
|
2168
|
+
statusDiv.classList.add(response.ok ? 'success' : 'error');
|
|
2169
|
+
|
|
2170
|
+
const contentType = response.headers.get('content-type');
|
|
2171
|
+
let data;
|
|
2172
|
+
|
|
2173
|
+
if (contentType && contentType.includes('application/json')) {
|
|
2174
|
+
data = await response.json();
|
|
2175
|
+
responseDiv.textContent = JSON.stringify(data, null, 2);
|
|
2176
|
+
} else {
|
|
2177
|
+
data = await response.text();
|
|
2178
|
+
responseDiv.textContent = data || '(empty response)';
|
|
2179
|
+
}
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
statusDiv.textContent = 'Error';
|
|
2182
|
+
statusDiv.classList.add('error');
|
|
2183
|
+
responseDiv.textContent = `Error: ${error.message}`;
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
|
|
2187
|
+
function setNestedValue(obj, path, value) {
|
|
2188
|
+
const keys = path.split('.');
|
|
2189
|
+
let current = obj;
|
|
2190
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
2191
|
+
if (!current[keys[i]]) current[keys[i]] = {};
|
|
2192
|
+
current = current[keys[i]];
|
|
2193
|
+
}
|
|
2194
|
+
current[keys[keys.length - 1]] = value;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// ========================================
|
|
2198
|
+
// Postman/Insomnia Export Functions
|
|
2199
|
+
// ========================================
|
|
2200
|
+
|
|
2201
|
+
window.downloadPostmanCollection = function(groupName) {
|
|
2202
|
+
const collection = generatePostmanCollection(groupName);
|
|
2203
|
+
const blob = new Blob([JSON.stringify(collection, null, 2)], { type: 'application/json' });
|
|
2204
|
+
const filename = groupName === '__all__'
|
|
2205
|
+
? `${apiData.project.name || 'api'}_postman_collection.json`
|
|
2206
|
+
: `${groupName}_postman_collection.json`;
|
|
2207
|
+
downloadFileBlob(blob, filename);
|
|
2208
|
+
};
|
|
2209
|
+
|
|
2210
|
+
window.downloadInsomniaCollection = function(groupName) {
|
|
2211
|
+
const collection = generateInsomniaCollection(groupName);
|
|
2212
|
+
const blob = new Blob([JSON.stringify(collection, null, 2)], { type: 'application/json' });
|
|
2213
|
+
const filename = groupName === '__all__'
|
|
2214
|
+
? `${apiData.project.name || 'api'}_insomnia.json`
|
|
2215
|
+
: `${groupName}_insomnia.json`;
|
|
2216
|
+
downloadFileBlob(blob, filename);
|
|
2217
|
+
};
|
|
2218
|
+
|
|
2219
|
+
function downloadFileBlob(blob, filename) {
|
|
2220
|
+
const url = URL.createObjectURL(blob);
|
|
2221
|
+
const a = document.createElement('a');
|
|
2222
|
+
a.href = url;
|
|
2223
|
+
a.download = filename;
|
|
2224
|
+
document.body.appendChild(a);
|
|
2225
|
+
a.click();
|
|
2226
|
+
document.body.removeChild(a);
|
|
2227
|
+
URL.revokeObjectURL(url);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
function generatePostmanCollection(groupName) {
|
|
2231
|
+
const baseUrl = apiData.project.url || 'https://api.example.com';
|
|
2232
|
+
|
|
2233
|
+
const collection = {
|
|
2234
|
+
info: {
|
|
2235
|
+
name: groupName === '__all__'
|
|
2236
|
+
? (apiData.project.name || 'API')
|
|
2237
|
+
: groupName,
|
|
2238
|
+
description: apiData.project.description || '',
|
|
2239
|
+
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
|
2240
|
+
_exporter_id: 'apidocly'
|
|
2241
|
+
},
|
|
2242
|
+
variable: [
|
|
2243
|
+
{
|
|
2244
|
+
key: 'baseUrl',
|
|
2245
|
+
value: baseUrl,
|
|
2246
|
+
type: 'string'
|
|
2247
|
+
}
|
|
2248
|
+
],
|
|
2249
|
+
item: []
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
// Add environment variables if defined
|
|
2253
|
+
if (apiData.project.environments && apiData.project.environments.length > 0) {
|
|
2254
|
+
apiData.project.environments.forEach(env => {
|
|
2255
|
+
collection.variable.push({
|
|
2256
|
+
key: `baseUrl_${env.name.replace(/\s+/g, '_').toLowerCase()}`,
|
|
2257
|
+
value: env.url,
|
|
2258
|
+
type: 'string'
|
|
2259
|
+
});
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// Filter groups
|
|
2264
|
+
const groups = groupName === '__all__'
|
|
2265
|
+
? apiData.groups
|
|
2266
|
+
: apiData.groups.filter(g => g.name === groupName);
|
|
2267
|
+
|
|
2268
|
+
// Build items
|
|
2269
|
+
groups.forEach(group => {
|
|
2270
|
+
const groupItem = {
|
|
2271
|
+
name: group.name,
|
|
2272
|
+
item: []
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2275
|
+
group.endpoints.forEach(endpoint => {
|
|
2276
|
+
const request = buildPostmanRequest(endpoint, baseUrl);
|
|
2277
|
+
groupItem.item.push({
|
|
2278
|
+
name: endpoint.title || `${endpoint.method} ${endpoint.path}`,
|
|
2279
|
+
request: request,
|
|
2280
|
+
response: buildPostmanExampleResponses(endpoint)
|
|
2281
|
+
});
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
collection.item.push(groupItem);
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
return collection;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
function buildPostmanRequest(endpoint, baseUrl) {
|
|
2291
|
+
const pathParams = getAllParams(endpoint.parameters);
|
|
2292
|
+
const queryParams = getAllParams(endpoint.query);
|
|
2293
|
+
const headerParams = getAllParams(endpoint.headers);
|
|
2294
|
+
const bodyParams = getAllParams(endpoint.body);
|
|
2295
|
+
|
|
2296
|
+
// Build URL with Postman variable syntax
|
|
2297
|
+
let path = endpoint.path;
|
|
2298
|
+
pathParams.forEach(param => {
|
|
2299
|
+
path = path.replace(`:${param.field}`, `:${param.field}`);
|
|
2300
|
+
path = path.replace(`{${param.field}}`, `:${param.field}`);
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2303
|
+
const request = {
|
|
2304
|
+
method: endpoint.method,
|
|
2305
|
+
header: [],
|
|
2306
|
+
url: {
|
|
2307
|
+
raw: `{{baseUrl}}${path}`,
|
|
2308
|
+
host: ['{{baseUrl}}'],
|
|
2309
|
+
path: path.split('/').filter(p => p),
|
|
2310
|
+
variable: [],
|
|
2311
|
+
query: []
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
|
|
2315
|
+
// Add path variables
|
|
2316
|
+
pathParams.forEach(param => {
|
|
2317
|
+
request.url.variable.push({
|
|
2318
|
+
key: param.field,
|
|
2319
|
+
value: getSampleValue(param),
|
|
2320
|
+
description: param.description || ''
|
|
2321
|
+
});
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2324
|
+
// Add query parameters
|
|
2325
|
+
queryParams.forEach(param => {
|
|
2326
|
+
request.url.query.push({
|
|
2327
|
+
key: param.field,
|
|
2328
|
+
value: getSampleValue(param),
|
|
2329
|
+
description: param.description || '',
|
|
2330
|
+
disabled: !param.required
|
|
2331
|
+
});
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
// Add headers
|
|
2335
|
+
headerParams.forEach(param => {
|
|
2336
|
+
request.header.push({
|
|
2337
|
+
key: param.field,
|
|
2338
|
+
value: getSampleValue(param),
|
|
2339
|
+
description: param.description || ''
|
|
2340
|
+
});
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
// Add body
|
|
2344
|
+
if (bodyParams.length > 0 && ['POST', 'PUT', 'PATCH'].includes(endpoint.method)) {
|
|
2345
|
+
const bodyObj = {};
|
|
2346
|
+
bodyParams.forEach(param => {
|
|
2347
|
+
setNestedValue(bodyObj, param.field, getSampleValue(param));
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
request.header.push({
|
|
2351
|
+
key: 'Content-Type',
|
|
2352
|
+
value: 'application/json'
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
request.body = {
|
|
2356
|
+
mode: 'raw',
|
|
2357
|
+
raw: JSON.stringify(bodyObj, null, 2),
|
|
2358
|
+
options: {
|
|
2359
|
+
raw: {
|
|
2360
|
+
language: 'json'
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
return request;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
function buildPostmanExampleResponses(endpoint) {
|
|
2370
|
+
const responses = [];
|
|
2371
|
+
|
|
2372
|
+
if (endpoint.successExamples && endpoint.successExamples.length > 0) {
|
|
2373
|
+
endpoint.successExamples.forEach(example => {
|
|
2374
|
+
responses.push({
|
|
2375
|
+
name: example.title || 'Success Response',
|
|
2376
|
+
originalRequest: {},
|
|
2377
|
+
status: 'OK',
|
|
2378
|
+
code: 200,
|
|
2379
|
+
body: example.content || ''
|
|
2380
|
+
});
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
return responses;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
function generateInsomniaCollection(groupName) {
|
|
2388
|
+
const baseUrl = apiData.project.url || 'https://api.example.com';
|
|
2389
|
+
const workspaceId = 'wrk_' + generateUniqueId();
|
|
2390
|
+
|
|
2391
|
+
const collection = {
|
|
2392
|
+
_type: 'export',
|
|
2393
|
+
__export_format: 4,
|
|
2394
|
+
__export_date: new Date().toISOString(),
|
|
2395
|
+
__export_source: 'apidocly',
|
|
2396
|
+
resources: []
|
|
2397
|
+
};
|
|
2398
|
+
|
|
2399
|
+
// Add workspace
|
|
2400
|
+
collection.resources.push({
|
|
2401
|
+
_id: workspaceId,
|
|
2402
|
+
_type: 'workspace',
|
|
2403
|
+
name: groupName === '__all__'
|
|
2404
|
+
? (apiData.project.name || 'API')
|
|
2405
|
+
: groupName,
|
|
2406
|
+
description: apiData.project.description || ''
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2409
|
+
// Add base environment
|
|
2410
|
+
const envId = 'env_' + generateUniqueId();
|
|
2411
|
+
collection.resources.push({
|
|
2412
|
+
_id: envId,
|
|
2413
|
+
_type: 'environment',
|
|
2414
|
+
parentId: workspaceId,
|
|
2415
|
+
name: 'Base Environment',
|
|
2416
|
+
data: {
|
|
2417
|
+
base_url: baseUrl
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
|
|
2421
|
+
// Add additional environments
|
|
2422
|
+
if (apiData.project.environments && apiData.project.environments.length > 0) {
|
|
2423
|
+
apiData.project.environments.forEach(env => {
|
|
2424
|
+
collection.resources.push({
|
|
2425
|
+
_id: 'env_' + generateUniqueId(),
|
|
2426
|
+
_type: 'environment',
|
|
2427
|
+
parentId: envId,
|
|
2428
|
+
name: env.name,
|
|
2429
|
+
data: {
|
|
2430
|
+
base_url: env.url
|
|
2431
|
+
}
|
|
2432
|
+
});
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// Filter groups
|
|
2437
|
+
const groups = groupName === '__all__'
|
|
2438
|
+
? apiData.groups
|
|
2439
|
+
: apiData.groups.filter(g => g.name === groupName);
|
|
2440
|
+
|
|
2441
|
+
// Build request groups and requests
|
|
2442
|
+
groups.forEach(group => {
|
|
2443
|
+
const groupId = 'fld_' + generateUniqueId();
|
|
2444
|
+
|
|
2445
|
+
collection.resources.push({
|
|
2446
|
+
_id: groupId,
|
|
2447
|
+
_type: 'request_group',
|
|
2448
|
+
parentId: workspaceId,
|
|
2449
|
+
name: group.name
|
|
2450
|
+
});
|
|
2451
|
+
|
|
2452
|
+
group.endpoints.forEach(endpoint => {
|
|
2453
|
+
const request = buildInsomniaRequest(endpoint, groupId);
|
|
2454
|
+
collection.resources.push(request);
|
|
2455
|
+
});
|
|
2456
|
+
});
|
|
2457
|
+
|
|
2458
|
+
return collection;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function buildInsomniaRequest(endpoint, parentId) {
|
|
2462
|
+
const pathParams = getAllParams(endpoint.parameters);
|
|
2463
|
+
const queryParams = getAllParams(endpoint.query);
|
|
2464
|
+
const headerParams = getAllParams(endpoint.headers);
|
|
2465
|
+
const bodyParams = getAllParams(endpoint.body);
|
|
2466
|
+
|
|
2467
|
+
// Build URL with Insomnia variable syntax
|
|
2468
|
+
let path = endpoint.path;
|
|
2469
|
+
pathParams.forEach(param => {
|
|
2470
|
+
path = path.replace(`:${param.field}`, `{{ _.${param.field} }}`);
|
|
2471
|
+
path = path.replace(`{${param.field}}`, `{{ _.${param.field} }}`);
|
|
2472
|
+
});
|
|
2473
|
+
|
|
2474
|
+
const request = {
|
|
2475
|
+
_id: 'req_' + generateUniqueId(),
|
|
2476
|
+
_type: 'request',
|
|
2477
|
+
parentId: parentId,
|
|
2478
|
+
name: endpoint.title || `${endpoint.method} ${endpoint.path}`,
|
|
2479
|
+
description: endpoint.description || '',
|
|
2480
|
+
method: endpoint.method,
|
|
2481
|
+
url: `{{ _.base_url }}${path}`,
|
|
2482
|
+
headers: [],
|
|
2483
|
+
parameters: [],
|
|
2484
|
+
body: {}
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2487
|
+
// Add query parameters
|
|
2488
|
+
queryParams.forEach(param => {
|
|
2489
|
+
request.parameters.push({
|
|
2490
|
+
name: param.field,
|
|
2491
|
+
value: getSampleValue(param),
|
|
2492
|
+
disabled: !param.required
|
|
2493
|
+
});
|
|
2494
|
+
});
|
|
2495
|
+
|
|
2496
|
+
// Add headers
|
|
2497
|
+
headerParams.forEach(param => {
|
|
2498
|
+
request.headers.push({
|
|
2499
|
+
name: param.field,
|
|
2500
|
+
value: getSampleValue(param)
|
|
2501
|
+
});
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
// Add body
|
|
2505
|
+
if (bodyParams.length > 0 && ['POST', 'PUT', 'PATCH'].includes(endpoint.method)) {
|
|
2506
|
+
const bodyObj = {};
|
|
2507
|
+
bodyParams.forEach(param => {
|
|
2508
|
+
setNestedValue(bodyObj, param.field, getSampleValue(param));
|
|
2509
|
+
});
|
|
2510
|
+
|
|
2511
|
+
request.headers.push({
|
|
2512
|
+
name: 'Content-Type',
|
|
2513
|
+
value: 'application/json'
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
request.body = {
|
|
2517
|
+
mimeType: 'application/json',
|
|
2518
|
+
text: JSON.stringify(bodyObj, null, 2)
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
return request;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
function generateUniqueId() {
|
|
2526
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// ========================================
|
|
2530
|
+
// Version Selection Functions
|
|
2531
|
+
// ========================================
|
|
2532
|
+
|
|
2533
|
+
let availableEndpointVersions = [];
|
|
2534
|
+
|
|
2535
|
+
function initVersionComparison() {
|
|
2536
|
+
availableEndpointVersions = getEndpointVersions();
|
|
2537
|
+
const versionConfig = typeof apiVersionsConfig !== 'undefined' ? apiVersionsConfig : { withCompare: false };
|
|
2538
|
+
const wrapper = document.getElementById('version-selector-wrapper');
|
|
2539
|
+
const selector = document.getElementById('version-selector');
|
|
2540
|
+
const panel = document.getElementById('version-compare-panel');
|
|
2541
|
+
const toggleBtn = document.getElementById('version-compare-toggle');
|
|
2542
|
+
const closeBtn = document.getElementById('version-compare-close');
|
|
2543
|
+
const compareBtn = document.getElementById('version-compare-btn');
|
|
2544
|
+
const versionFrom = document.getElementById('version-from');
|
|
2545
|
+
const versionTo = document.getElementById('version-to');
|
|
2546
|
+
|
|
2547
|
+
if (!wrapper || availableEndpointVersions.length < 2) {
|
|
2548
|
+
// Need at least 2 endpoint versions to show selector
|
|
2549
|
+
if (wrapper) wrapper.classList.add('hidden');
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
// Show the version selector
|
|
2554
|
+
wrapper.classList.remove('hidden');
|
|
2555
|
+
|
|
2556
|
+
// Only show compare button if withCompare is enabled
|
|
2557
|
+
if (!versionConfig.withCompare && toggleBtn) {
|
|
2558
|
+
toggleBtn.classList.add('hidden');
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// Populate main version selector from endpoint versions
|
|
2562
|
+
// First option is "Latest" (shows latest version of each endpoint)
|
|
2563
|
+
let selectorHtml = '<option value="" selected>Latest</option>';
|
|
2564
|
+
availableEndpointVersions.forEach(function(version) {
|
|
2565
|
+
selectorHtml += '<option value="' + escapeHtml(version) + '">v' + escapeHtml(version) + '</option>';
|
|
2566
|
+
});
|
|
2567
|
+
selector.innerHTML = selectorHtml;
|
|
2568
|
+
|
|
2569
|
+
// Populate comparison selectors
|
|
2570
|
+
let compareOptionsHtml = '';
|
|
2571
|
+
availableEndpointVersions.forEach(function(version) {
|
|
2572
|
+
compareOptionsHtml += '<option value="' + escapeHtml(version) + '">v' + escapeHtml(version) + '</option>';
|
|
2573
|
+
});
|
|
2574
|
+
|
|
2575
|
+
if (versionFrom) versionFrom.innerHTML = compareOptionsHtml;
|
|
2576
|
+
if (versionTo) versionTo.innerHTML = compareOptionsHtml;
|
|
2577
|
+
|
|
2578
|
+
// Default comparison: second newest to newest
|
|
2579
|
+
if (availableEndpointVersions.length >= 2 && versionFrom && versionTo) {
|
|
2580
|
+
versionFrom.value = availableEndpointVersions[1]; // Second newest (older)
|
|
2581
|
+
versionTo.value = availableEndpointVersions[0]; // Newest
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// Version selector change - filter endpoints by version
|
|
2585
|
+
selector.addEventListener('change', function(e) {
|
|
2586
|
+
const version = e.target.value;
|
|
2587
|
+
selectedEndpointVersion = version || null; // null means "Latest"
|
|
2588
|
+
reRenderWithData();
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
// Toggle comparison panel
|
|
2592
|
+
if (toggleBtn) {
|
|
2593
|
+
toggleBtn.addEventListener('click', function() {
|
|
2594
|
+
panel.classList.toggle('hidden');
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
if (closeBtn) {
|
|
2599
|
+
closeBtn.addEventListener('click', function() {
|
|
2600
|
+
panel.classList.add('hidden');
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// Compare button
|
|
2605
|
+
if (compareBtn) {
|
|
2606
|
+
compareBtn.addEventListener('click', function() {
|
|
2607
|
+
const fromVersion = versionFrom.value;
|
|
2608
|
+
const toVersion = versionTo.value;
|
|
2609
|
+
|
|
2610
|
+
if (fromVersion === toVersion) {
|
|
2611
|
+
alert('Please select two different versions to compare.');
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
compareBtn.disabled = true;
|
|
2616
|
+
compareBtn.textContent = 'Comparing...';
|
|
2617
|
+
|
|
2618
|
+
try {
|
|
2619
|
+
const fromData = filterToVersion(apiData, fromVersion);
|
|
2620
|
+
const toData = filterToVersion(apiData, toVersion);
|
|
2621
|
+
|
|
2622
|
+
const diff = compareVersionData(fromData, toData);
|
|
2623
|
+
renderDiffResults(diff, { version: fromVersion }, { version: toVersion });
|
|
2624
|
+
|
|
2625
|
+
document.getElementById('version-compare-results').classList.remove('hidden');
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
console.error('Failed to compare versions:', error);
|
|
2628
|
+
alert('Failed to compare versions: ' + error.message);
|
|
2629
|
+
} finally {
|
|
2630
|
+
compareBtn.disabled = false;
|
|
2631
|
+
compareBtn.textContent = 'Compare';
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
function reRenderWithData() {
|
|
2638
|
+
// Re-generate group colors for the filtered data
|
|
2639
|
+
generateGroupColors();
|
|
2640
|
+
|
|
2641
|
+
// Update stats for the filtered data
|
|
2642
|
+
updateStats();
|
|
2643
|
+
|
|
2644
|
+
// Re-render groups (uses getActiveData() which applies version filter)
|
|
2645
|
+
renderGroups();
|
|
2646
|
+
|
|
2647
|
+
// Reset to groups view
|
|
2648
|
+
currentGroup = null;
|
|
2649
|
+
if (groupsView) groupsView.classList.remove('hidden');
|
|
2650
|
+
if (endpointsView) endpointsView.classList.add('hidden');
|
|
2651
|
+
if (searchView) searchView.classList.add('hidden');
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
async function loadVersionData(filename) {
|
|
2655
|
+
if (versionCache[filename]) {
|
|
2656
|
+
return versionCache[filename];
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// Check if this is the current version - use apiData directly
|
|
2660
|
+
if (apiData && apiData.project) {
|
|
2661
|
+
const currentVersionFilename = 'v' + apiData.project.version.replace(/\./g, '_') + '.json';
|
|
2662
|
+
if (filename === currentVersionFilename) {
|
|
2663
|
+
versionCache[filename] = apiData;
|
|
2664
|
+
return apiData;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// Check if version data is embedded in versions.js (for file:// protocol support)
|
|
2669
|
+
if (typeof apiVersionsData !== 'undefined' && apiVersionsData[filename]) {
|
|
2670
|
+
versionCache[filename] = apiVersionsData[filename];
|
|
2671
|
+
return apiVersionsData[filename];
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// Fall back to fetch (for http:// protocol)
|
|
2675
|
+
try {
|
|
2676
|
+
const response = await fetch('versions/' + filename);
|
|
2677
|
+
if (!response.ok) {
|
|
2678
|
+
console.error('Version fetch failed:', response.status, response.statusText, 'for', filename);
|
|
2679
|
+
throw new Error('Failed to load version: ' + filename + ' (HTTP ' + response.status + ')');
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
const data = await response.json();
|
|
2683
|
+
versionCache[filename] = data;
|
|
2684
|
+
return data;
|
|
2685
|
+
} catch (error) {
|
|
2686
|
+
console.error('Version load error:', error);
|
|
2687
|
+
throw error;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
function compareVersionData(oldData, newData) {
|
|
2692
|
+
const diff = {
|
|
2693
|
+
added: [],
|
|
2694
|
+
removed: [],
|
|
2695
|
+
modified: []
|
|
2696
|
+
};
|
|
2697
|
+
|
|
2698
|
+
// Build endpoint maps
|
|
2699
|
+
const oldEndpoints = new Map();
|
|
2700
|
+
const newEndpoints = new Map();
|
|
2701
|
+
|
|
2702
|
+
for (const group of oldData.groups) {
|
|
2703
|
+
for (const endpoint of group.endpoints) {
|
|
2704
|
+
const key = `${endpoint.method}:${endpoint.path}`;
|
|
2705
|
+
oldEndpoints.set(key, { ...endpoint, group: group.name });
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
for (const group of newData.groups) {
|
|
2710
|
+
for (const endpoint of group.endpoints) {
|
|
2711
|
+
const key = `${endpoint.method}:${endpoint.path}`;
|
|
2712
|
+
newEndpoints.set(key, { ...endpoint, group: group.name });
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// Find added endpoints
|
|
2717
|
+
for (const [key, endpoint] of newEndpoints) {
|
|
2718
|
+
if (!oldEndpoints.has(key)) {
|
|
2719
|
+
diff.added.push(endpoint);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// Find removed endpoints
|
|
2724
|
+
for (const [key, endpoint] of oldEndpoints) {
|
|
2725
|
+
if (!newEndpoints.has(key)) {
|
|
2726
|
+
diff.removed.push(endpoint);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// Find modified endpoints
|
|
2731
|
+
for (const [key, newEndpoint] of newEndpoints) {
|
|
2732
|
+
if (oldEndpoints.has(key)) {
|
|
2733
|
+
const oldEndpoint = oldEndpoints.get(key);
|
|
2734
|
+
const changes = findEndpointChanges(oldEndpoint, newEndpoint);
|
|
2735
|
+
if (changes.length > 0) {
|
|
2736
|
+
diff.modified.push({
|
|
2737
|
+
endpoint: newEndpoint,
|
|
2738
|
+
changes: changes
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
return diff;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
function findEndpointChanges(oldEp, newEp) {
|
|
2748
|
+
const changes = [];
|
|
2749
|
+
|
|
2750
|
+
// Check basic fields
|
|
2751
|
+
if (oldEp.title !== newEp.title) {
|
|
2752
|
+
changes.push({ field: 'title', old: oldEp.title, new: newEp.title });
|
|
2753
|
+
}
|
|
2754
|
+
if (oldEp.description !== newEp.description) {
|
|
2755
|
+
changes.push({ field: 'description', old: 'changed', new: 'changed' });
|
|
2756
|
+
}
|
|
2757
|
+
if (oldEp.deprecated !== newEp.deprecated) {
|
|
2758
|
+
changes.push({ field: 'deprecated', old: oldEp.deprecated, new: newEp.deprecated });
|
|
2759
|
+
}
|
|
2760
|
+
if (oldEp.permission !== newEp.permission) {
|
|
2761
|
+
changes.push({ field: 'permission', old: oldEp.permission, new: newEp.permission });
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
// Check parameters
|
|
2765
|
+
const paramChanges = compareParams(oldEp.parameters, newEp.parameters, 'parameters');
|
|
2766
|
+
const queryChanges = compareParams(oldEp.query, newEp.query, 'query');
|
|
2767
|
+
const bodyChanges = compareParams(oldEp.body, newEp.body, 'body');
|
|
2768
|
+
const headerChanges = compareParams(oldEp.headers, newEp.headers, 'headers');
|
|
2769
|
+
|
|
2770
|
+
changes.push(...paramChanges, ...queryChanges, ...bodyChanges, ...headerChanges);
|
|
2771
|
+
|
|
2772
|
+
// Check responses
|
|
2773
|
+
const successChanges = compareParams(oldEp.success, newEp.success, 'success');
|
|
2774
|
+
const errorChanges = compareParams(oldEp.error, newEp.error, 'error');
|
|
2775
|
+
|
|
2776
|
+
changes.push(...successChanges, ...errorChanges);
|
|
2777
|
+
|
|
2778
|
+
return changes;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
function compareParams(oldParams, newParams, fieldName) {
|
|
2782
|
+
const changes = [];
|
|
2783
|
+
|
|
2784
|
+
const oldFields = new Set();
|
|
2785
|
+
const newFields = new Set();
|
|
2786
|
+
|
|
2787
|
+
// Flatten grouped params
|
|
2788
|
+
if (oldParams) {
|
|
2789
|
+
for (const group of Object.values(oldParams)) {
|
|
2790
|
+
for (const param of group) {
|
|
2791
|
+
oldFields.add(param.field);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
if (newParams) {
|
|
2797
|
+
for (const group of Object.values(newParams)) {
|
|
2798
|
+
for (const param of group) {
|
|
2799
|
+
newFields.add(param.field);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// Find added fields
|
|
2805
|
+
for (const field of newFields) {
|
|
2806
|
+
if (!oldFields.has(field)) {
|
|
2807
|
+
changes.push({ field: `${fieldName}.${field}`, type: 'added' });
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// Find removed fields
|
|
2812
|
+
for (const field of oldFields) {
|
|
2813
|
+
if (!newFields.has(field)) {
|
|
2814
|
+
changes.push({ field: `${fieldName}.${field}`, type: 'removed' });
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
return changes;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function renderDiffResults(diff, fromVersion, toVersion) {
|
|
2822
|
+
const summaryEl = document.getElementById('version-diff-summary');
|
|
2823
|
+
const detailsEl = document.getElementById('version-diff-details');
|
|
2824
|
+
|
|
2825
|
+
// Render summary
|
|
2826
|
+
summaryEl.innerHTML = `
|
|
2827
|
+
<div class="diff-stat added">
|
|
2828
|
+
<span class="diff-stat-count">${diff.added.length}</span>
|
|
2829
|
+
<span>Added</span>
|
|
2830
|
+
</div>
|
|
2831
|
+
<div class="diff-stat removed">
|
|
2832
|
+
<span class="diff-stat-count">${diff.removed.length}</span>
|
|
2833
|
+
<span>Removed</span>
|
|
2834
|
+
</div>
|
|
2835
|
+
<div class="diff-stat modified">
|
|
2836
|
+
<span class="diff-stat-count">${diff.modified.length}</span>
|
|
2837
|
+
<span>Modified</span>
|
|
2838
|
+
</div>
|
|
2839
|
+
`;
|
|
2840
|
+
|
|
2841
|
+
// Render details
|
|
2842
|
+
let detailsHtml = '';
|
|
2843
|
+
|
|
2844
|
+
if (diff.added.length === 0 && diff.removed.length === 0 && diff.modified.length === 0) {
|
|
2845
|
+
detailsHtml = '<div class="no-changes-message">No changes between these versions.</div>';
|
|
2846
|
+
} else {
|
|
2847
|
+
if (diff.added.length > 0) {
|
|
2848
|
+
detailsHtml += `
|
|
2849
|
+
<div class="diff-section">
|
|
2850
|
+
<div class="diff-section-header added">
|
|
2851
|
+
<span class="diff-section-icon">+</span>
|
|
2852
|
+
<span>Added Endpoints (${diff.added.length})</span>
|
|
2853
|
+
</div>
|
|
2854
|
+
<div class="diff-section-content">
|
|
2855
|
+
${diff.added.map(ep => renderDiffEndpoint(ep, 'added')).join('')}
|
|
2856
|
+
</div>
|
|
2857
|
+
</div>
|
|
2858
|
+
`;
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
if (diff.removed.length > 0) {
|
|
2862
|
+
detailsHtml += `
|
|
2863
|
+
<div class="diff-section">
|
|
2864
|
+
<div class="diff-section-header removed">
|
|
2865
|
+
<span class="diff-section-icon">−</span>
|
|
2866
|
+
<span>Removed Endpoints (${diff.removed.length})</span>
|
|
2867
|
+
</div>
|
|
2868
|
+
<div class="diff-section-content">
|
|
2869
|
+
${diff.removed.map(ep => renderDiffEndpoint(ep, 'removed')).join('')}
|
|
2870
|
+
</div>
|
|
2871
|
+
</div>
|
|
2872
|
+
`;
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
if (diff.modified.length > 0) {
|
|
2876
|
+
detailsHtml += `
|
|
2877
|
+
<div class="diff-section">
|
|
2878
|
+
<div class="diff-section-header modified">
|
|
2879
|
+
<span class="diff-section-icon">~</span>
|
|
2880
|
+
<span>Modified Endpoints (${diff.modified.length})</span>
|
|
2881
|
+
</div>
|
|
2882
|
+
<div class="diff-section-content">
|
|
2883
|
+
${diff.modified.map(item => renderDiffEndpoint(item.endpoint, 'modified', item.changes)).join('')}
|
|
2884
|
+
</div>
|
|
2885
|
+
</div>
|
|
2886
|
+
`;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
detailsEl.innerHTML = detailsHtml;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
function renderDiffEndpoint(endpoint, type, changes = null) {
|
|
2894
|
+
let changesHtml = '';
|
|
2895
|
+
if (changes && changes.length > 0) {
|
|
2896
|
+
changesHtml = `
|
|
2897
|
+
<div class="diff-changes-list">
|
|
2898
|
+
${changes.map(c => `<div class="diff-change-item">• <span class="field">${escapeHtml(c.field)}</span> ${c.type || 'changed'}</div>`).join('')}
|
|
2899
|
+
</div>
|
|
2900
|
+
`;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
return `
|
|
2904
|
+
<div class="diff-endpoint ${type}">
|
|
2905
|
+
<span class="diff-endpoint-method ${endpoint.method.toLowerCase()}">${endpoint.method}</span>
|
|
2906
|
+
<span class="diff-endpoint-path">${escapeHtml(endpoint.path)}</span>
|
|
2907
|
+
<span class="diff-endpoint-title">${escapeHtml(endpoint.title || '')}</span>
|
|
2908
|
+
</div>
|
|
2909
|
+
${changesHtml}
|
|
2910
|
+
`;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// Initialize
|
|
2914
|
+
function startup() {
|
|
2915
|
+
if (typeof window.apiDoclyAuth !== 'undefined' && window.apiDoclyAuth.isReady) {
|
|
2916
|
+
app.classList.remove('hidden');
|
|
2917
|
+
init();
|
|
2918
|
+
} else if (typeof window.apiDoclyAuth !== 'undefined') {
|
|
2919
|
+
window.apiDoclyAuth.onAuthenticated(function() {
|
|
2920
|
+
init();
|
|
2921
|
+
});
|
|
2922
|
+
} else {
|
|
2923
|
+
app.classList.remove('hidden');
|
|
2924
|
+
init();
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
if (document.readyState === 'loading') {
|
|
2929
|
+
document.addEventListener('DOMContentLoaded', startup);
|
|
2930
|
+
} else {
|
|
2931
|
+
startup();
|
|
2932
|
+
}
|
|
2933
|
+
})();
|