drf-to-mkdoc 0.1.9__py3-none-any.whl → 0.2.1__py3-none-any.whl

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.

Potentially problematic release.


This version of drf-to-mkdoc might be problematic. Click here for more details.

Files changed (35) hide show
  1. drf_to_mkdoc/conf/defaults.py +5 -0
  2. drf_to_mkdoc/conf/settings.py +123 -9
  3. drf_to_mkdoc/management/commands/build_docs.py +8 -7
  4. drf_to_mkdoc/management/commands/build_endpoint_docs.py +69 -0
  5. drf_to_mkdoc/management/commands/build_model_docs.py +50 -0
  6. drf_to_mkdoc/management/commands/{generate_model_docs.py → extract_model_data.py} +18 -24
  7. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out-sidebar.js +879 -0
  8. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out-sidebar.css +728 -0
  9. drf_to_mkdoc/utils/ai_tools/__init__.py +0 -0
  10. drf_to_mkdoc/utils/ai_tools/enums.py +13 -0
  11. drf_to_mkdoc/utils/ai_tools/exceptions.py +19 -0
  12. drf_to_mkdoc/utils/ai_tools/providers/__init__.py +0 -0
  13. drf_to_mkdoc/utils/ai_tools/providers/base_provider.py +123 -0
  14. drf_to_mkdoc/utils/ai_tools/providers/gemini_provider.py +80 -0
  15. drf_to_mkdoc/utils/ai_tools/types.py +81 -0
  16. drf_to_mkdoc/utils/commons/__init__.py +0 -0
  17. drf_to_mkdoc/utils/commons/code_extractor.py +22 -0
  18. drf_to_mkdoc/utils/commons/file_utils.py +35 -0
  19. drf_to_mkdoc/utils/commons/model_utils.py +83 -0
  20. drf_to_mkdoc/utils/commons/operation_utils.py +83 -0
  21. drf_to_mkdoc/utils/commons/path_utils.py +78 -0
  22. drf_to_mkdoc/utils/commons/schema_utils.py +230 -0
  23. drf_to_mkdoc/utils/endpoint_detail_generator.py +16 -35
  24. drf_to_mkdoc/utils/endpoint_list_generator.py +1 -1
  25. drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +33 -30
  26. drf_to_mkdoc/utils/model_detail_generator.py +44 -40
  27. drf_to_mkdoc/utils/model_list_generator.py +25 -15
  28. drf_to_mkdoc/utils/schema.py +259 -0
  29. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/METADATA +16 -5
  30. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/RECORD +33 -16
  31. drf_to_mkdoc/management/commands/generate_docs.py +0 -138
  32. drf_to_mkdoc/utils/common.py +0 -353
  33. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/WHEEL +0 -0
  34. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/licenses/LICENSE +0 -0
  35. {drf_to_mkdoc-0.1.9.dist-info → drf_to_mkdoc-0.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,879 @@
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ // Get endpoint information from the page
3
+ const getEndpointInfo = () => {
4
+ let { path = '', method = 'GET' } = (window.__getTryOutEndpointInfo && window.__getTryOutEndpointInfo()) || {};
5
+
6
+ // First, try to find the method badge and code element in the same paragraph
7
+ const methodBadge = document.querySelector('.method-badge');
8
+ if (methodBadge) {
9
+ method = methodBadge.textContent.trim();
10
+
11
+ // Look for a code element in the same parent or nearby
12
+ const parent = methodBadge.closest('p, div, section');
13
+ if (parent) {
14
+ const codeElement = parent.querySelector('code');
15
+ if (codeElement) {
16
+ path = codeElement.textContent.trim();
17
+ }
18
+ }
19
+ }
20
+
21
+ // Fallback: try to find in title
22
+ if (!path) {
23
+ const title = document.querySelector('h1');
24
+ if (title) {
25
+ const titleText = title.textContent.trim();
26
+ const pathMatch = titleText.match(/[A-Z]+\s+(.+)/);
27
+ if (pathMatch) {
28
+ path = pathMatch[1];
29
+ }
30
+ }
31
+ }
32
+
33
+ // Additional fallback - look for code blocks with paths
34
+ if (!path) {
35
+ const codeBlocks = document.querySelectorAll('code');
36
+ for (const code of codeBlocks) {
37
+ const text = code.textContent.trim();
38
+ if (text.startsWith('/') && !text.includes('http')) {
39
+ path = text;
40
+ break;
41
+ }
42
+ }
43
+ }
44
+
45
+ // Clean up the path to handle HTML entities and special characters
46
+ if (path) {
47
+ // Use DOMParser to safely decode HTML entities
48
+ const parser = new DOMParser();
49
+ const doc = parser.parseFromString(`<div>${path}</div>`, 'text/html');
50
+ const tempDiv = doc.querySelector('div');
51
+ path = tempDiv ? (tempDiv.textContent || tempDiv.innerText || path) : path;
52
+
53
+ // Remove any non-printable characters or replace problematic ones
54
+ path = path.replace(/[^\x20-\x7E]/g, ''); // Remove non-ASCII printable characters
55
+ path = path.replace(/¶/g, ''); // Specifically remove paragraph symbols
56
+ path = path.trim();
57
+ }
58
+
59
+ const info = {
60
+ method: method,
61
+ path: path || '/api/endpoint',
62
+ pathParams: path ? extractPathParams(path) : []
63
+ };
64
+ // expose for global functions
65
+ window.__getTryOutEndpointInfo = () => info;
66
+ return info;
67
+ };
68
+
69
+ // Extract path parameters from URL
70
+ const extractPathParams = (path) => {
71
+ const matches = path.match(/\{([^}]+)\}/g) || [];
72
+ return matches.map(param => param.slice(1, -1)); // Remove { }
73
+ };
74
+
75
+ // Standard header suggestions
76
+ const standardHeaders = [
77
+ 'Accept', 'Accept-Encoding', 'Accept-Language', 'Authorization',
78
+ 'Cache-Control', 'Content-Type', 'Cookie', 'User-Agent',
79
+ 'X-API-Key', 'X-Requested-With', 'X-CSRF-Token'
80
+ ];
81
+
82
+ // HTML escaping function to prevent XSS
83
+ const escapeHtml = (s='') => s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
84
+
85
+ // Create the try-out panel HTML
86
+ const createTryOutPanel = (endpointInfo) => {
87
+ // Create path parameters HTML
88
+ let pathParamsHtml = '';
89
+ if (endpointInfo.pathParams.length > 0) {
90
+ pathParamsHtml = `
91
+ <div class="form-group">
92
+ <label class="form-label">Path Parameters</label>
93
+ <div class="kv-container" id="pathParams">
94
+ ${endpointInfo.pathParams.map(p => {
95
+ const param = escapeHtml(p);
96
+ return `
97
+ <div class="kv-item">
98
+ <label class="param-label">${param}</label>
99
+ <input type="text" placeholder="Enter ${param} value" data-param="${param}" required>
100
+ </div>
101
+ `;
102
+ }).join('')}
103
+ </div>
104
+ </div>
105
+ `;
106
+ }
107
+
108
+ // Create the panel HTML
109
+ return `
110
+ <div class="try-out-sidebar">
111
+ <div class="form-group">
112
+ <label class="form-label">Base URL</label>
113
+ <input type="text" class="form-input" id="baseUrl" value="${window.location.origin}" placeholder="https://api.example.com">
114
+ </div>
115
+
116
+ <div class="tabs">
117
+ <button class="tab active" data-tab="parameters">Parameters</button>
118
+ <button class="tab" data-tab="headers">Headers</button>
119
+ <button class="tab" data-tab="body" style="${['POST', 'PUT', 'PATCH'].includes(endpointInfo.method) ? '' : 'display: none;'}">Body</button>
120
+ </div>
121
+
122
+ <div class="tab-content active" id="parametersTab">
123
+ ${pathParamsHtml}
124
+
125
+ <div class="form-group">
126
+ <label class="form-label">Query Parameters</label>
127
+ <div class="kv-container" id="queryParams">
128
+ <div class="kv-item">
129
+ <input type="text" placeholder="Parameter name" list="queryParamSuggestions">
130
+ <input type="text" placeholder="Parameter value">
131
+ <button class="remove-btn" onclick="removeKvItem(this)">✕</button>
132
+ </div>
133
+ </div>
134
+ <datalist id="queryParamSuggestions">
135
+ ${(() => {
136
+ const suggestions = new Set();
137
+
138
+ // Find query parameters section by looking for h2 with id="query-parameters"
139
+ const queryParamsHeading = document.querySelector('h2[id="query-parameters"]');
140
+ if (queryParamsHeading) {
141
+ // Look for the next ul element after this heading
142
+ let nextElement = queryParamsHeading.nextElementSibling;
143
+ while (nextElement && nextElement.tagName !== 'UL') {
144
+ nextElement = nextElement.nextElementSibling;
145
+ }
146
+ if (nextElement) {
147
+ const queryParamElements = nextElement.querySelectorAll('li code');
148
+ queryParamElements.forEach(code => {
149
+ const name = (code.textContent || '').trim();
150
+ if (name) {
151
+ suggestions.add(name);
152
+ }
153
+ });
154
+ }
155
+ }
156
+
157
+ // Find search parameters section by looking for h3 with id="search-parameters"
158
+ const searchParamsHeading = document.querySelector('h3[id="search-parameters"]');
159
+ if (searchParamsHeading) {
160
+ // Look for the next ul element after this heading
161
+ let nextElement = searchParamsHeading.nextElementSibling;
162
+ while (nextElement && nextElement.tagName !== 'UL') {
163
+ nextElement = nextElement.nextElementSibling;
164
+ }
165
+ if (nextElement) {
166
+ const searchParamItems = nextElement.querySelectorAll('li code');
167
+ if (searchParamItems.length > 0) {
168
+ suggestions.add('search');
169
+ }
170
+ // Also add the actual search parameter names
171
+ searchParamItems.forEach(code => {
172
+ const name = (code.textContent || '').trim();
173
+ if (name) {
174
+ suggestions.add(name);
175
+ }
176
+ });
177
+ }
178
+ }
179
+
180
+ // Fallback: if no specific sections found, look for any ul li code elements
181
+ if (suggestions.size === 0) {
182
+ const fallbackElements = document.querySelectorAll('ul li code');
183
+ fallbackElements.forEach(code => {
184
+ const name = (code.textContent || '').trim();
185
+ if (name) {
186
+ suggestions.add(name);
187
+ }
188
+ });
189
+ }
190
+
191
+ return Array.from(suggestions).map(name => `<option value="${escapeHtml(name)}">`).join('');
192
+ })()}
193
+ </datalist>
194
+ <button class="add-btn" onclick="addQueryParam()">
195
+ <span>+</span> Add Parameter
196
+ </button>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="tab-content" id="headersTab">
201
+ <div class="form-group">
202
+ <label class="form-label">Request Headers</label>
203
+ <div class="kv-container" id="requestHeaders">
204
+ <div class="kv-item">
205
+ <input type="text" value="Content-Type" list="headerSuggestions">
206
+ <input type="text" value="application/json">
207
+ <button class="remove-btn" onclick="removeKvItem(this)">✕</button>
208
+ </div>
209
+ <div class="kv-item">
210
+ <input type="text" value="Authorization" list="headerSuggestions">
211
+ <input type="text" placeholder="Bearer your-token">
212
+ <button class="remove-btn" onclick="removeKvItem(this)">✕</button>
213
+ </div>
214
+ </div>
215
+ <datalist id="headerSuggestions">
216
+ ${standardHeaders.map(header => `<option value="${header}">`).join('')}
217
+ </datalist>
218
+ <button class="add-btn" onclick="addHeader()">
219
+ <span>+</span> Add Header
220
+ </button>
221
+ </div>
222
+ </div>
223
+
224
+ <div class="tab-content" id="bodyTab">
225
+ <div class="form-group">
226
+ <label class="form-label">Request Body</label>
227
+ <textarea class="form-input textarea" id="requestBody" placeholder="Enter JSON payload here..."></textarea>
228
+ </div>
229
+ </div>
230
+
231
+ <button class="execute-btn" id="executeBtn" onclick="executeRequest()">
232
+ <span>▶</span> Execute Request
233
+ </button>
234
+ </div>
235
+ `;
236
+ };
237
+
238
+ // Check if mobile/tablet view
239
+ const isMobile = () => window.innerWidth <= 480;
240
+ const isTablet = () => window.innerWidth > 480 && window.innerWidth <= 1220; // Increased tablet breakpoint to match MkDocs
241
+
242
+ // Add the try-out panel to the sidebar or create mobile version
243
+ const addTryOutToSidebar = () => {
244
+ const endpointInfo = getEndpointInfo();
245
+ const tryOutPanel = createTryOutPanel(endpointInfo);
246
+
247
+ if (isMobile()) {
248
+ // Create mobile floating button and modal
249
+ createMobileTryOut(tryOutPanel);
250
+ } else if (isTablet()) {
251
+ // Tablet: Create mobile interface but don't interfere with hamburger
252
+ createMobileTryOut(tryOutPanel);
253
+ } else {
254
+ // Desktop: Add to sidebar
255
+ const leftSidebar = document.querySelector('.md-sidebar--primary');
256
+ if (leftSidebar) {
257
+ const panelContainer = document.createElement('div');
258
+ panelContainer.className = 'try-out-container';
259
+
260
+ const parser = new DOMParser();
261
+ const doc = parser.parseFromString(tryOutPanel, 'text/html');
262
+ const elements = doc.body.children;
263
+ while (elements.length > 0) {
264
+ panelContainer.appendChild(elements[0]);
265
+ }
266
+ leftSidebar.prepend(panelContainer);
267
+
268
+ // Add response modal to body
269
+ const modal = document.createElement('div');
270
+ modal.id = 'responseModal';
271
+ modal.style.display = 'none';
272
+ modal.setAttribute('role', 'dialog');
273
+ modal.setAttribute('aria-modal', 'true');
274
+ modal.setAttribute('aria-label', 'API Response');
275
+
276
+ const modalOverlay = document.createElement('div');
277
+ modalOverlay.className = 'modal-overlay';
278
+ modalOverlay.addEventListener('click', () => TryOutSidebar.closeResponseModal());
279
+
280
+ const modalContent = document.createElement('div');
281
+ modalContent.className = 'modal-content';
282
+
283
+ const modalHeader = document.createElement('div');
284
+ modalHeader.className = 'modal-header';
285
+
286
+ const modalTitle = document.createElement('h3');
287
+ modalTitle.textContent = 'API Response';
288
+
289
+ const modalClose = document.createElement('button');
290
+ modalClose.className = 'modal-close';
291
+ modalClose.setAttribute('aria-label', 'Close');
292
+ modalClose.textContent = '✕';
293
+ modalClose.addEventListener('click', () => TryOutSidebar.closeResponseModal());
294
+
295
+ modalHeader.appendChild(modalTitle);
296
+ modalHeader.appendChild(modalClose);
297
+
298
+ const modalBody = document.createElement('div');
299
+ modalBody.className = 'modal-body';
300
+
301
+ const responseHeader = document.createElement('div');
302
+ responseHeader.className = 'response-header';
303
+
304
+ const statusLabel = document.createElement('span');
305
+ statusLabel.textContent = 'Status: ';
306
+
307
+ const statusBadge = document.createElement('span');
308
+ statusBadge.className = 'status-badge';
309
+ statusBadge.id = 'modalStatusBadge';
310
+
311
+ responseHeader.appendChild(statusLabel);
312
+ responseHeader.appendChild(statusBadge);
313
+
314
+ const responseBody = document.createElement('div');
315
+ responseBody.className = 'response-body';
316
+ responseBody.id = 'modalResponseBody';
317
+
318
+ modalBody.appendChild(responseHeader);
319
+ modalBody.appendChild(responseBody);
320
+
321
+ modalContent.appendChild(modalHeader);
322
+ modalContent.appendChild(modalBody);
323
+
324
+ modal.appendChild(modalOverlay);
325
+ modal.appendChild(modalContent);
326
+
327
+ document.body.appendChild(modal);
328
+
329
+ // Initialize tabs
330
+ initTabs();
331
+ }
332
+ }
333
+ };
334
+
335
+ // Create mobile try-out interface
336
+ const createMobileTryOut = (tryOutPanel) => {
337
+ // Create floating action button
338
+ const fab = document.createElement('div');
339
+ fab.className = 'mobile-try-out-fab';
340
+ fab.setAttribute('role', 'button');
341
+ fab.setAttribute('tabindex', '0');
342
+ fab.setAttribute('aria-label', 'Open Try It Out');
343
+ fab.addEventListener('click', () => TryOutSidebar.openMobileTryOut());
344
+
345
+ const fabIcon = document.createElement('span');
346
+ fabIcon.textContent = '🚀';
347
+ fab.appendChild(fabIcon);
348
+
349
+ document.body.appendChild(fab);
350
+
351
+ // Create mobile modal
352
+ const mobileModal = document.createElement('div');
353
+ mobileModal.className = 'mobile-try-out-modal';
354
+ mobileModal.id = 'mobileTryOutModal';
355
+ mobileModal.style.display = 'none';
356
+
357
+ const mobileOverlay = document.createElement('div');
358
+ mobileOverlay.className = 'mobile-modal-overlay';
359
+ mobileOverlay.addEventListener('click', () => TryOutSidebar.closeMobileTryOut());
360
+
361
+ const mobileContent = document.createElement('div');
362
+ mobileContent.className = 'mobile-modal-content';
363
+
364
+ const mobileHeader = document.createElement('div');
365
+ mobileHeader.className = 'mobile-modal-header';
366
+
367
+ const mobileTitle = document.createElement('h3');
368
+ mobileTitle.textContent = '🚀 Try It Out';
369
+
370
+ const mobileClose = document.createElement('button');
371
+ mobileClose.className = 'mobile-modal-close';
372
+ mobileClose.setAttribute('aria-label', 'Close');
373
+ mobileClose.textContent = '✕';
374
+ mobileClose.addEventListener('click', () => TryOutSidebar.closeMobileTryOut());
375
+
376
+ mobileHeader.appendChild(mobileTitle);
377
+ mobileHeader.appendChild(mobileClose);
378
+
379
+ const mobileBody = document.createElement('div');
380
+ mobileBody.className = 'mobile-modal-body';
381
+ mobileBody.id = 'mobileTryOutBody';
382
+
383
+ mobileContent.appendChild(mobileHeader);
384
+ mobileContent.appendChild(mobileBody);
385
+
386
+ mobileModal.appendChild(mobileOverlay);
387
+ mobileModal.appendChild(mobileContent);
388
+
389
+ document.body.appendChild(mobileModal);
390
+
391
+ // Mount panel content into mobile body
392
+ const parser = new DOMParser();
393
+ const doc = parser.parseFromString(tryOutPanel, 'text/html');
394
+ const panelEl = doc.querySelector('.try-out-sidebar');
395
+ if (panelEl) {
396
+ panelEl.classList.add('mobile-try-out');
397
+ document.getElementById('mobileTryOutBody').appendChild(panelEl);
398
+ }
399
+
400
+ // Add response modal for mobile
401
+ let responseModal = document.getElementById('responseModal');
402
+ if (!responseModal) {
403
+ responseModal = document.createElement('div');
404
+ responseModal.id = 'responseModal';
405
+ responseModal.style.display = 'none';
406
+ responseModal.setAttribute('role', 'dialog');
407
+ responseModal.setAttribute('aria-modal', 'true');
408
+ responseModal.setAttribute('aria-label', 'API Response');
409
+
410
+ const modalOverlay = document.createElement('div');
411
+ modalOverlay.className = 'modal-overlay';
412
+ modalOverlay.addEventListener('click', () => TryOutSidebar.closeResponseModal());
413
+
414
+ const modalContent = document.createElement('div');
415
+ modalContent.className = 'modal-content';
416
+
417
+ const modalHeader = document.createElement('div');
418
+ modalHeader.className = 'modal-header';
419
+
420
+ const modalTitle = document.createElement('h3');
421
+ modalTitle.textContent = 'API Response';
422
+
423
+ const modalClose = document.createElement('button');
424
+ modalClose.className = 'modal-close';
425
+ modalClose.setAttribute('aria-label', 'Close');
426
+ modalClose.textContent = '✕';
427
+ modalClose.addEventListener('click', () => TryOutSidebar.closeResponseModal());
428
+
429
+ modalHeader.appendChild(modalTitle);
430
+ modalHeader.appendChild(modalClose);
431
+
432
+ const modalBody = document.createElement('div');
433
+ modalBody.className = 'modal-body';
434
+
435
+ const responseHeader = document.createElement('div');
436
+ responseHeader.className = 'response-header';
437
+
438
+ const statusLabel = document.createElement('span');
439
+ statusLabel.textContent = 'Status: ';
440
+
441
+ const statusBadge = document.createElement('span');
442
+ statusBadge.className = 'status-badge';
443
+ statusBadge.id = 'modalStatusBadge';
444
+
445
+ responseHeader.appendChild(statusLabel);
446
+ responseHeader.appendChild(statusBadge);
447
+
448
+ const responseBody = document.createElement('div');
449
+ responseBody.className = 'response-body';
450
+ responseBody.id = 'modalResponseBody';
451
+
452
+ modalBody.appendChild(responseHeader);
453
+ modalBody.appendChild(responseBody);
454
+
455
+ modalContent.appendChild(modalHeader);
456
+ modalContent.appendChild(modalBody);
457
+
458
+ responseModal.appendChild(modalOverlay);
459
+ responseModal.appendChild(modalContent);
460
+
461
+ document.body.appendChild(responseModal);
462
+ }
463
+
464
+ // Initialize tabs for mobile
465
+ setTimeout(() => initTabs(), 100);
466
+ };
467
+
468
+ // Initialize tabs
469
+ const initTabs = () => {
470
+ document.querySelectorAll('.try-out-sidebar .tab').forEach(tab => {
471
+ tab.addEventListener('click', () => {
472
+ // Remove active class from all tabs and contents
473
+ document.querySelectorAll('.try-out-sidebar .tab').forEach(t => t.classList.remove('active'));
474
+ document.querySelectorAll('.try-out-sidebar .tab-content').forEach(c => c.classList.remove('active'));
475
+
476
+ // Add active class to clicked tab and corresponding content
477
+ tab.classList.add('active');
478
+ const tabName = tab.getAttribute('data-tab');
479
+ document.getElementById(tabName + 'Tab').classList.add('active');
480
+ });
481
+ });
482
+ };
483
+
484
+ // Add try-out panel to sidebar
485
+ addTryOutToSidebar();
486
+
487
+ // Handle window resize
488
+ let resizeTimeout;
489
+ window.addEventListener('resize', () => {
490
+ clearTimeout(resizeTimeout);
491
+ resizeTimeout = setTimeout(() => {
492
+ // Re-initialize on resize to handle mobile/desktop transitions
493
+ const currentMobile = window.innerWidth <= 480;
494
+ const currentTablet = window.innerWidth > 480 && window.innerWidth <= 1220;
495
+ const fab = document.querySelector('.mobile-try-out-fab');
496
+ const sidebar = document.querySelector('.md-sidebar--primary .try-out-sidebar');
497
+
498
+ if ((currentMobile || currentTablet) && sidebar && !fab) {
499
+ // Switched to mobile/tablet, need to create mobile interface
500
+ location.reload(); // Simple solution to reinitialize
501
+ } else if (!currentMobile && !currentTablet && fab && !sidebar) {
502
+ // Switched to desktop, need to create sidebar interface
503
+ location.reload(); // Simple solution to reinitialize
504
+ }
505
+ }, 250);
506
+ });
507
+ });
508
+
509
+ // Create namespace to avoid global pollution
510
+ window.TryOutSidebar = {
511
+ openMobileTryOut: function() {
512
+ const modal = document.getElementById('mobileTryOutModal');
513
+ if (modal) {
514
+ modal.style.display = 'block';
515
+ document.body.style.overflow = 'hidden';
516
+ }
517
+ },
518
+
519
+ closeMobileTryOut: function() {
520
+ const modal = document.getElementById('mobileTryOutModal');
521
+ if (modal) {
522
+ modal.style.display = 'none';
523
+ document.body.style.overflow = '';
524
+ }
525
+ },
526
+
527
+ addQueryParam: function() {
528
+ const container = document.getElementById('queryParams');
529
+ if (!container) return;
530
+
531
+ const kvItem = document.createElement('div');
532
+ kvItem.className = 'kv-item';
533
+
534
+ const nameInput = document.createElement('input');
535
+ nameInput.type = 'text';
536
+ nameInput.placeholder = 'Parameter name';
537
+ nameInput.setAttribute('list', 'queryParamSuggestions');
538
+
539
+ const valueInput = document.createElement('input');
540
+ valueInput.type = 'text';
541
+ valueInput.placeholder = 'Parameter value';
542
+
543
+ const removeBtn = document.createElement('button');
544
+ removeBtn.className = 'remove-btn';
545
+ removeBtn.textContent = '✕';
546
+ removeBtn.addEventListener('click', () => TryOutSidebar.removeKvItem(removeBtn));
547
+
548
+ kvItem.appendChild(nameInput);
549
+ kvItem.appendChild(valueInput);
550
+ kvItem.appendChild(removeBtn);
551
+ container.appendChild(kvItem);
552
+ },
553
+
554
+ addHeader: function() {
555
+ const container = document.getElementById('requestHeaders');
556
+ if (!container) return;
557
+
558
+ const kvItem = document.createElement('div');
559
+ kvItem.className = 'kv-item';
560
+
561
+ const nameInput = document.createElement('input');
562
+ nameInput.type = 'text';
563
+ nameInput.placeholder = 'Header name';
564
+ nameInput.setAttribute('list', 'headerSuggestions');
565
+
566
+ const valueInput = document.createElement('input');
567
+ valueInput.type = 'text';
568
+ valueInput.placeholder = 'Header value';
569
+
570
+ const removeBtn = document.createElement('button');
571
+ removeBtn.className = 'remove-btn';
572
+ removeBtn.textContent = '✕';
573
+ removeBtn.addEventListener('click', () => TryOutSidebar.removeKvItem(removeBtn));
574
+
575
+ kvItem.appendChild(nameInput);
576
+ kvItem.appendChild(valueInput);
577
+ kvItem.appendChild(removeBtn);
578
+ container.appendChild(kvItem);
579
+ },
580
+
581
+ removeKvItem: function(button) {
582
+ if (button && button.parentElement) {
583
+ button.parentElement.remove();
584
+ TryOutSidebar.updateUrlFromParams();
585
+ }
586
+ },
587
+
588
+ updateUrlFromParams: function() {
589
+ // This function is no longer needed since we don't show the full URL
590
+ },
591
+
592
+ closeResponseModal: function() {
593
+ const modal = document.getElementById('responseModal');
594
+ if (modal) {
595
+ modal.style.display = 'none';
596
+ }
597
+ },
598
+
599
+ showResponseModal: function(status, responseText) {
600
+ const modal = document.getElementById('responseModal');
601
+ const statusBadge = document.getElementById('modalStatusBadge');
602
+ const responseBody = document.getElementById('modalResponseBody');
603
+
604
+ if (modal && statusBadge && responseBody) {
605
+ statusBadge.textContent = String(status);
606
+ const code = Number(status);
607
+ statusBadge.className = 'status-badge' + (Number.isFinite(code) ? ` status-${Math.floor(code/100)*100}` : '');
608
+
609
+ try {
610
+ const jsonResponse = JSON.parse(responseText);
611
+ responseBody.textContent = JSON.stringify(jsonResponse, null, 2);
612
+ } catch (e) {
613
+ responseBody.textContent = responseText;
614
+ }
615
+
616
+ modal.style.display = 'block';
617
+ }
618
+ },
619
+
620
+ validateRequiredParams: function() {
621
+ const requiredInputs = document.querySelectorAll('#pathParams input[required]');
622
+ const emptyParams = [];
623
+
624
+ requiredInputs.forEach(input => {
625
+ if (!input.value.trim()) {
626
+ const paramName = input.getAttribute('data-param');
627
+ emptyParams.push(paramName);
628
+ input.classList.add('error');
629
+ input.addEventListener('input', () => input.classList.remove('error'), { once: true });
630
+ }
631
+ });
632
+
633
+ return emptyParams;
634
+ }
635
+ };
636
+
637
+ // Legacy global functions for backward compatibility (deprecated)
638
+ function openMobileTryOut() { TryOutSidebar.openMobileTryOut(); }
639
+ function closeMobileTryOut() { TryOutSidebar.closeMobileTryOut(); }
640
+ function addQueryParam() { TryOutSidebar.addQueryParam(); }
641
+ function addHeader() { TryOutSidebar.addHeader(); }
642
+ function removeKvItem(button) { TryOutSidebar.removeKvItem(button); }
643
+ function updateUrlFromParams() { TryOutSidebar.updateUrlFromParams(); }
644
+ function closeResponseModal() { TryOutSidebar.closeResponseModal(); }
645
+ function showResponseModal(status, responseText) { TryOutSidebar.showResponseModal(status, responseText); }
646
+ function validateRequiredParams() { return TryOutSidebar.validateRequiredParams(); }
647
+
648
+ async function executeRequest() {
649
+ const executeBtn = document.getElementById('executeBtn');
650
+ if (!executeBtn) return;
651
+
652
+ // Validate required parameters
653
+ const emptyParams = TryOutSidebar.validateRequiredParams();
654
+ if (emptyParams.length > 0) {
655
+ alert(`Please fill in the required parameters: ${emptyParams.join(', ')}`);
656
+ return;
657
+ }
658
+
659
+ // Update button state
660
+ executeBtn.disabled = true;
661
+ executeBtn.textContent = '';
662
+ const spinner = document.createElement('div');
663
+ spinner.className = 'spinner';
664
+ const text = document.createTextNode(' Sending...');
665
+ executeBtn.appendChild(spinner);
666
+ executeBtn.appendChild(text);
667
+
668
+ try {
669
+ // Get base URL and construct full URL
670
+ // Validate and normalize the Base URL, restricting scheme to http/https
671
+ const baseInput = (document.getElementById('baseUrl').value || '').trim() || window.location.origin;
672
+ let base;
673
+ try {
674
+ base = new URL(baseInput, window.location.origin);
675
+ } catch (_) {
676
+ throw new Error('Invalid Base URL');
677
+ }
678
+ if (!/^https?:$/.test(base.protocol)) {
679
+ throw new Error('Base URL must use http or https');
680
+ }
681
+ const baseUrl = base.href;
682
+ // Get endpoint info from the current page
683
+ let path = '';
684
+ let method = 'GET';
685
+
686
+ // First, try to find the method badge and code element in the same paragraph
687
+ const methodBadge = document.querySelector('.method-badge');
688
+ if (methodBadge) {
689
+ method = methodBadge.textContent.trim();
690
+
691
+ // Look for a code element in the same parent or nearby
692
+ const parent = methodBadge.closest('p, div, section');
693
+ if (parent) {
694
+ const codeElement = parent.querySelector('code');
695
+ if (codeElement) {
696
+ path = codeElement.textContent.trim();
697
+ }
698
+ }
699
+ }
700
+
701
+ // Fallback: try to find in title
702
+ if (!path) {
703
+ const title = document.querySelector('h1');
704
+ if (title) {
705
+ const titleText = title.textContent.trim();
706
+ const pathMatch = titleText.match(/([A-Z]+)\s+(.+)/);
707
+ if (pathMatch) {
708
+ method = pathMatch[1];
709
+ path = pathMatch[2];
710
+ }
711
+ }
712
+ }
713
+
714
+ // Additional fallback - look for code blocks with paths
715
+ if (!path) {
716
+ const codeBlocks = document.querySelectorAll('code');
717
+ for (const code of codeBlocks) {
718
+ const text = code.textContent.trim();
719
+ if (text.startsWith('/') && !text.includes('http')) {
720
+ path = text;
721
+ break;
722
+ }
723
+ }
724
+ }
725
+
726
+ // Clean up the path to handle HTML entities and special characters
727
+ if (path) {
728
+ // Use DOMParser to safely decode HTML entities
729
+ const parser = new DOMParser();
730
+ const doc = parser.parseFromString(`<div>${path}</div>`, 'text/html');
731
+ const tempDiv = doc.querySelector('div');
732
+ path = tempDiv ? (tempDiv.textContent || tempDiv.innerText || path) : path;
733
+
734
+ // Remove any non-printable characters or replace problematic ones
735
+ path = path.replace(/[^\x20-\x7E]/g, ''); // Remove non-ASCII printable characters
736
+ path = path.replace(/¶/g, ''); // Specifically remove paragraph symbols
737
+ path = path.trim();
738
+ }
739
+
740
+ console.log('Extracted path:', path);
741
+ console.log('Extracted method:', method);
742
+
743
+ // Ensure baseUrl doesn't end with slash and path starts with slash
744
+ let cleanBaseUrl = baseUrl.replace(/\/$/, '');
745
+ let cleanPath = path || '/api/endpoint';
746
+ if (!cleanPath.startsWith('/')) {
747
+ cleanPath = '/' + cleanPath;
748
+ }
749
+
750
+ let url = cleanBaseUrl + cleanPath;
751
+ console.log('Initial URL:', url);
752
+
753
+ // Replace path parameters
754
+ const pathParams = document.querySelectorAll('#pathParams .kv-item');
755
+ console.log('Found path params:', pathParams.length);
756
+
757
+ pathParams.forEach((item, index) => {
758
+ const label = item.querySelector('.param-label');
759
+ const input = item.querySelector('input');
760
+ if (label && input) {
761
+ const paramName = label.textContent.trim();
762
+ const paramValue = input.value.trim();
763
+ console.log(`Param ${index}: ${paramName} = ${paramValue}`);
764
+
765
+ if (paramName && paramValue) {
766
+ const beforeReplace = url;
767
+ const escaped = paramName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
768
+ url = url.replace(new RegExp(`\\{${escaped}\\}`, 'g'), paramValue);
769
+ console.log(`URL after replacing {${paramName}}: ${beforeReplace} -> ${url}`);
770
+ } else if (paramName && !paramValue) {
771
+ // This should not happen as validation should catch empty required params
772
+ console.warn(`Empty value for required parameter: ${paramName}`);
773
+ throw new Error(`Required parameter '${paramName}' has no value`);
774
+ }
775
+ }
776
+ });
777
+
778
+ // Clean up any remaining unreplaced parameters or malformed URLs
779
+ const beforeCleanup = url;
780
+
781
+ // Remove double slashes but preserve protocol slashes
782
+ url = url.replace(/([^:])\/+/g, '$1/'); // Remove double slashes except after protocol
783
+ url = url.replace(/\{[^}]*\}/g, ''); // Remove any remaining parameter placeholders
784
+
785
+ // Don't remove trailing slash as it might be part of the endpoint
786
+
787
+ console.log('Final URL:', beforeCleanup, '->', url);
788
+
789
+ // Add query parameters
790
+ const queryParams = {};
791
+ document.querySelectorAll('#queryParams .kv-item').forEach(item => {
792
+ const inputs = item.querySelectorAll('input');
793
+ if (inputs.length >= 2) {
794
+ const key = inputs[0].value.trim();
795
+ const value = inputs[1].value.trim();
796
+ if (key && value) {
797
+ queryParams[key] = value;
798
+ }
799
+ }
800
+ });
801
+
802
+ if (Object.keys(queryParams).length > 0) {
803
+ const queryString = new URLSearchParams(queryParams).toString();
804
+ url += (url.includes('?') ? '&' : '?') + queryString;
805
+ }
806
+
807
+ // Get headers
808
+ const headers = {};
809
+ document.querySelectorAll('#requestHeaders .kv-item').forEach(item => {
810
+ const inputs = item.querySelectorAll('input');
811
+ if (inputs.length >= 2) {
812
+ const key = inputs[0].value.trim();
813
+ const value = inputs[1].value.trim();
814
+ if (key && value) {
815
+ headers[key] = value;
816
+ }
817
+ }
818
+ });
819
+
820
+ // Prepare request options
821
+ const requestOptions = {
822
+ method: method,
823
+ headers: headers
824
+ };
825
+
826
+ // Add request body for POST, PUT, PATCH
827
+ if (['POST', 'PUT', 'PATCH'].includes(method)) {
828
+ const bodyInput = document.getElementById('requestBody');
829
+ if (bodyInput && bodyInput.value.trim()) {
830
+ try {
831
+ // Validate JSON
832
+ JSON.parse(bodyInput.value);
833
+ requestOptions.body = bodyInput.value;
834
+ if (!headers['Content-Type']) {
835
+ requestOptions.headers['Content-Type'] = 'application/json';
836
+ }
837
+ } catch (e) {
838
+ throw new Error('Invalid JSON in request body');
839
+ }
840
+ }
841
+ }
842
+
843
+ // Send the request
844
+ const response = await fetch(url, requestOptions);
845
+ const responseText = await response.text();
846
+
847
+ // Show response in modal
848
+ TryOutSidebar.showResponseModal(response.status, responseText);
849
+
850
+ } catch (error) {
851
+ // Enhanced error handling with specific error types
852
+ let errorMessage = 'Unknown error occurred';
853
+ let errorType = 'Error';
854
+
855
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
856
+ errorType = 'Network Error';
857
+ errorMessage = 'Failed to connect to the server. Please check your internet connection and try again.';
858
+ } else if (error.name === 'SyntaxError' && error.message.includes('JSON')) {
859
+ errorType = 'JSON Parse Error';
860
+ errorMessage = 'Invalid JSON in request body. Please check your input and try again.';
861
+ } else if (error.message.includes('CORS')) {
862
+ errorType = 'CORS Error';
863
+ errorMessage = 'Cross-origin request blocked. The server may not allow requests from this domain.';
864
+ } else if (error.message) {
865
+ errorMessage = error.message;
866
+ }
867
+
868
+ TryOutSidebar.showResponseModal(errorType, errorMessage);
869
+ } finally {
870
+ // Reset button
871
+ executeBtn.disabled = false;
872
+ executeBtn.textContent = '';
873
+ const playIcon = document.createElement('span');
874
+ playIcon.textContent = '▶';
875
+ const text = document.createTextNode(' Execute Request');
876
+ executeBtn.appendChild(playIcon);
877
+ executeBtn.appendChild(text);
878
+ }
879
+ }