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.
@@ -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, '&amp;')
542
+ .replace(/</g, '&lt;')
543
+ .replace(/>/g, '&gt;');
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(/&lt;\?php/g, m => addToken('hl-tag', m));
576
+ result = result.replace(/\?&gt;/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, '&amp;')
1195
+ .replace(/</g, '&lt;')
1196
+ .replace(/>/g, '&gt;');
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, '&amp;')
1529
+ .replace(/</g, '&lt;')
1530
+ .replace(/>/g, '&gt;')
1531
+ .replace(/"/g, '&quot;')
1532
+ .replace(/'/g, '&#39;');
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, '&amp;')
1577
+ .replace(/</g, '&lt;')
1578
+ .replace(/>/g, '&gt;');
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
+ })();