shokupan 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/analyzer-BqIe1p0R.js +35 -0
  2. package/dist/analyzer-BqIe1p0R.js.map +1 -0
  3. package/dist/analyzer-CKLGLFtx.cjs +35 -0
  4. package/dist/analyzer-CKLGLFtx.cjs.map +1 -0
  5. package/dist/{analyzer-Ce_7JxZh.js → analyzer.impl-CV6W1Eq7.js} +238 -21
  6. package/dist/analyzer.impl-CV6W1Eq7.js.map +1 -0
  7. package/dist/{analyzer-Bei1sVWp.cjs → analyzer.impl-D9Yi1Hax.cjs} +237 -20
  8. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +1 -0
  9. package/dist/cli.cjs +1 -1
  10. package/dist/cli.js +1 -1
  11. package/dist/context.d.ts +19 -7
  12. package/dist/http-server-BEMPIs33.cjs.map +1 -1
  13. package/dist/http-server-CCeagTyU.js.map +1 -1
  14. package/dist/index.cjs +1500 -275
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.js +1482 -256
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/application/api-explorer/plugin.d.ts +9 -0
  20. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +880 -0
  21. package/dist/plugins/application/api-explorer/static/style.css +767 -0
  22. package/dist/plugins/application/api-explorer/static/theme.css +128 -0
  23. package/dist/plugins/application/asyncapi/generator.d.ts +3 -0
  24. package/dist/plugins/application/asyncapi/plugin.d.ts +15 -0
  25. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +748 -0
  26. package/dist/plugins/application/asyncapi/static/style.css +565 -0
  27. package/dist/plugins/application/asyncapi/static/theme.css +128 -0
  28. package/dist/plugins/application/auth.d.ts +3 -1
  29. package/dist/plugins/application/dashboard/metrics-collector.d.ts +3 -1
  30. package/dist/plugins/application/dashboard/plugin.d.ts +13 -3
  31. package/dist/plugins/application/dashboard/static/registry.css +0 -53
  32. package/dist/plugins/application/dashboard/static/styles.css +29 -20
  33. package/dist/plugins/application/dashboard/static/tabulator.css +83 -31
  34. package/dist/plugins/application/dashboard/static/theme.css +128 -0
  35. package/dist/plugins/application/graphql-apollo.d.ts +33 -0
  36. package/dist/plugins/application/graphql-yoga.d.ts +25 -0
  37. package/dist/plugins/application/openapi/analyzer.d.ts +12 -119
  38. package/dist/plugins/application/openapi/analyzer.impl.d.ts +167 -0
  39. package/dist/plugins/application/scalar.d.ts +9 -2
  40. package/dist/router.d.ts +80 -51
  41. package/dist/shokupan.d.ts +14 -8
  42. package/dist/util/datastore.d.ts +71 -7
  43. package/dist/util/decorators.d.ts +2 -2
  44. package/dist/util/types.d.ts +96 -3
  45. package/package.json +32 -12
  46. package/dist/analyzer-Bei1sVWp.cjs.map +0 -1
  47. package/dist/analyzer-Ce_7JxZh.js.map +0 -1
  48. package/dist/plugins/application/dashboard/static/scrollbar.css +0 -24
  49. package/dist/plugins/application/dashboard/template.eta +0 -246
@@ -0,0 +1,880 @@
1
+ // Client-side JavaScript for API Explorer
2
+
3
+ // Global State
4
+ let explorerData = { routes: [], config: {}, info: {} };
5
+ let currentRoute = null;
6
+ let currentEditors = { request: null, response: null, source: null };
7
+
8
+ document.addEventListener('DOMContentLoaded', () => {
9
+ loadData();
10
+ setupSidebar();
11
+ handleHashNavigation();
12
+
13
+ // Listen for hash changes
14
+ window.addEventListener('hashchange', handleHashNavigation);
15
+ });
16
+
17
+ function loadData() {
18
+ const script = document.getElementById('explorer-data');
19
+ if (script) {
20
+ try {
21
+ explorerData = JSON.parse(script.textContent);
22
+ } catch (e) {
23
+ console.error('Failed to parse explorer data', e);
24
+ }
25
+ }
26
+ }
27
+
28
+ function handleHashNavigation() {
29
+ const hash = window.location.hash.slice(1);
30
+ const container = document.getElementById('ide-container');
31
+
32
+ // If no hash, show info/empty state
33
+ if (!hash) {
34
+ // Show info section if available, otherwise empty state
35
+ if (explorerData.info) {
36
+ container.innerHTML = renderInfoSection(explorerData.info);
37
+ } else {
38
+ container.innerHTML = '<div class="empty-state">Select a request to view details</div>';
39
+ }
40
+ return;
41
+ }
42
+
43
+ // Find route
44
+ const route = explorerData.routes.find(r => r.op.operationId === hash);
45
+ if (route) {
46
+ currentRoute = route;
47
+ renderRequestView(route, container);
48
+ } else {
49
+ container.innerHTML = `<div class="empty-state">Request not found: ${hash}</div>`;
50
+ }
51
+ }
52
+
53
+ function renderInfoSection(info) {
54
+ const { title, description } = info;
55
+ return `
56
+ <div class="info-section">
57
+ <h1>${title || 'API Explorer'}</h1>
58
+ ${description ? `<div class="markdown-content" data-markdown="true">${parseMarkdown(description)}</div>` : ''}
59
+ </div>
60
+ `;
61
+ }
62
+
63
+ // Helper to recursively render schema properties
64
+ function renderSchema(schema, depth = 0) {
65
+ if (!schema) return '';
66
+
67
+ const indent = depth * 16;
68
+ const type = schema.type || 'any';
69
+ const required = schema.required || [];
70
+
71
+ if (type === 'object' && schema.properties) {
72
+ const props = Object.entries(schema.properties).map(([key, prop]) => {
73
+ const isRequired = required.includes(key);
74
+ const propType = prop.type || 'any';
75
+ const hasNested = (propType === 'object' && prop.properties) || (propType === 'array' && prop.items);
76
+
77
+ return `
78
+ <div style="margin-left: ${indent}px;">
79
+ <div class="property-heading" style="display: flex; align-items: center; gap: 8px; padding: 6px 0;">
80
+ <div class="property-name" style="font-family: monospace; font-weight: 500; color: var(--text-primary);">${key}</div>
81
+ <span class="property-detail" style="color: var(--text-secondary); font-size: 0.85rem;">
82
+ <span class="property-detail-value">${propType}</span>
83
+ </span>
84
+ ${isRequired ? '<div class="property-required" style="margin-left: auto; font-size: 0.75rem; color: #f44336; text-transform: uppercase;">required</div>' : ''}
85
+ </div>
86
+ ${prop.description ? `<div style="color: var(--text-secondary); font-size: 0.85rem; margin-left: 0; margin-top: -4px; margin-bottom: 4px;">${prop.description}</div>` : ''}
87
+ ${hasNested ? renderSchema(propType === 'array' ? prop.items : prop, depth + 1) : ''}
88
+ </div>
89
+ `;
90
+ }).join('');
91
+ return props;
92
+ } else if (type === 'array' && schema.items) {
93
+ return `
94
+ <div style="margin-left: ${indent}px; margin-top: 4px;">
95
+ <div style="font-family: monospace; font-size: 0.85rem; color: var(--text-secondary);">
96
+ [array items]
97
+ </div>
98
+ ${renderSchema(schema.items, depth + 1)}
99
+ </div>
100
+ `;
101
+ }
102
+
103
+ return '';
104
+ }
105
+
106
+ // Helper to highlight path operators
107
+ function highlightPath(path) {
108
+ if (!path) return '';
109
+
110
+ return path
111
+ // Highlight {{substitution}} patterns
112
+ .replace(/\{\{([^}]+)\}\}/g, '<span style="color: #4caf50;">{{$1}}</span>')
113
+ // Highlight :parameter patterns
114
+ .replace(/:([a-zA-Z0-9_]+)/g, '<span style="color: #2196f3;">:$1</span>')
115
+ // Highlight * wildcards
116
+ .replace(/\*/g, '<span style="color: #ff9800;">*</span>');
117
+ }
118
+
119
+ // --- IDE View Implementation ---
120
+
121
+ function renderRequestView(route, container) {
122
+ const { method, path, op } = route;
123
+
124
+ // Extract metadata
125
+ const source = op['x-shokupan-source'];
126
+ const middlewares = op['x-shokupan-middleware'] || [];
127
+ const summary = op.summary || highlightPath(route.path);
128
+
129
+ // Build tabs for Request Body, Params, Auth, etc.
130
+ const uniqueParams = getUniqueParams(op);
131
+ const hasBody = op.requestBody || (method !== 'get' && method !== 'delete');
132
+
133
+ const html = `
134
+ <div class="ide-view">
135
+ <!-- Request Panel -->
136
+ <div class="ide-panel request-panel">
137
+ <div class="request-panel-header">
138
+ <div class="request-header-main">
139
+ <div class="request-url-bar">
140
+ <span class="url-method badge-${method}">${method.toUpperCase()}</span>
141
+ <div class="url-input" style="display: flex; align-items: center; font-family: monospace; white-space: nowrap; overflow-x: auto;">${highlightPath(path)}</div>
142
+ </div>
143
+ <button class="send-btn" id="btn-send">Send</button>
144
+ </div>
145
+ <div class="request-actions">
146
+ <button class="btn icon-btn copy-curl" title="Copy cURL">
147
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
148
+ Copy cURL
149
+ </button>
150
+ <button class="btn icon-btn copy-fetch" title="Copy Fetch">
151
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
152
+ Copy Fetch
153
+ </button>
154
+ </div>
155
+ </div>
156
+
157
+ <div class="panel-tabs">
158
+ <div class="panel-tab active" data-tab="info">Info</div>
159
+ <div class="panel-tab" data-tab="params">Params</div>
160
+ <div class="panel-tab" data-tab="headers">Headers</div>
161
+ ${hasBody ? '<div class="panel-tab" data-tab="body">Body</div>' : ''}
162
+ <div class="panel-tab" data-tab="auth">Auth</div>
163
+ </div>
164
+
165
+ <div class="panel-content">
166
+ <div class="panel-section active" id="tab-info">
167
+ <div class="info-content" style="padding:16px; overflow-y:auto; flex:1;">
168
+ <div class="info-header">
169
+ <h2 class="info-title">${summary}</h2>
170
+ <div class="info-meta">
171
+ ${op.tags ? `<div class="meta-row"><strong>Tags:</strong> ${op.tags.map(t => `<span class="badge">${t}</span>`).join('')}</div>` : ''}
172
+ </div>
173
+ </div>
174
+
175
+ ${op.description ? `<div class="markdown-content" style="margin:16px 0;">${parseMarkdown(op.description)}</div>` : ''}
176
+
177
+ ${op.tags && op.tags.length > 0 ? `
178
+ <div class="hierarchy-section" style="margin:16px 0;">
179
+ <div style="display: flex; align-items: center; gap: 6px; font-size: 0.9rem; color: var(--text-secondary);">
180
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
181
+ <path d="M3 3v18h18"></path>
182
+ <path d="M7 12h4"></path>
183
+ <path d="M11 8v8"></path>
184
+ <path d="M15 16h4"></path>
185
+ <path d="M19 12v8"></path>
186
+ </svg>
187
+ ${op.tags.map((tag, idx) => `<span>${tag}</span>${idx < op.tags.length - 1 ? '<span style="opacity: 0.5;">›</span>' : ''}`).join('')}
188
+ </div>
189
+ </div>
190
+ ` : ''}
191
+
192
+ ${middlewares.length > 0 ? `
193
+ <div class="middleware-section">
194
+ <h3>Middleware Pipeline</h3>
195
+ <div class="middleware-list" style="display: flex; flex-direction: column; gap: 4px;">
196
+ ${middlewares.map((mw, idx) => `<div style="display: flex; align-items: center; gap: 8px;">
197
+ <span style="font-family: monospace; color: var(--text-secondary); min-width: 20px;">${idx + 1}.</span>
198
+ <span class="middleware-badge" title="${mw.metadata ? JSON.stringify(mw.metadata).replace(/"/g, '&quot;') : ''}">${mw.name}</span>
199
+ </div>`).join('')}
200
+ </div>
201
+ </div>
202
+ ` : ''}
203
+
204
+ <div class="request-overview" style="margin:16px 0; background: var(--bg-secondary); border-radius: 8px; padding: 16px;">
205
+ <h3 style="margin-top: 0; margin-bottom: 12px; font-size: 1rem;">Request</h3>
206
+ <div style="display: grid; gap: 8px; font-size: 0.9rem;">
207
+ <div style="display: flex; gap: 8px;">
208
+ <span class="badge badge-${method.toUpperCase()}">${method.toUpperCase()}</span>
209
+ <code style="background: var(--bg-primary); padding: 2px 6px; border-radius: 4px;">${path}</code>
210
+ </div>
211
+ ${op.parameters && op.parameters.length > 0 ? `
212
+ <div style="display: grid; grid-template-columns: 120px 1fr; gap: 8px;">
213
+ <span style="color: var(--text-secondary);">Parameters:</span>
214
+ <div style="display: flex; flex-wrap: wrap; gap: 4px;">
215
+ ${op.parameters.filter(p => p.in === 'query').map(p =>
216
+ `<span style="background: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 0.85rem;">
217
+ <code>${p.name}</code>${p.required ? '*' : ''}
218
+ </span>`
219
+ ).join('')}
220
+ ${op.parameters.filter(p => p.in === 'path').map(p =>
221
+ `<span style="background: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 0.85rem;">
222
+ <code>{${p.name}}</code>
223
+ </span>`
224
+ ).join('')}
225
+ </div>
226
+ </div>
227
+ ` : ''}
228
+ ${op.requestBody ? `
229
+ <div style="display: grid; grid-template-columns: 120px 1fr; gap: 8px;">
230
+ <span style="color: var(--text-secondary);">Body:</span>
231
+ <div style="display: flex; flex-wrap: wrap; gap: 4px;">
232
+ ${Object.keys(op.requestBody.content || {}).map(ct =>
233
+ `<code style="background: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 0.85rem;">${ct}</code>`
234
+ ).join('')}
235
+ </div>
236
+ </div>
237
+ ` : ''}
238
+ </div>
239
+
240
+ <h3 style="margin-top: 16px; margin-bottom: 12px; font-size: 1rem;">Response</h3>
241
+ <div style="display: grid; gap: 12px; font-size: 0.9rem;">
242
+ ${Object.entries(op.responses || {}).map(([code, resp]) => {
243
+ const contentTypes = resp.content ? Object.keys(resp.content) : [];
244
+ const firstContentType = contentTypes[0];
245
+ const schema = firstContentType && resp.content[firstContentType]?.schema;
246
+
247
+ return `
248
+ <div style="border-left: 2px solid var(--text-secondary); padding-left: 12px;">
249
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
250
+ <code style="background: var(--bg-primary); padding: 2px 8px; border-radius: 4px; font-weight: bold;">${code}</code>
251
+ <span style="color: var(--text-secondary);">${resp.description || 'Response'}</span>
252
+ </div>
253
+ ${contentTypes.length > 0 ? `
254
+ <div style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px;">
255
+ ${contentTypes.map(ct =>
256
+ `<code style="background: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 0.8rem;">${ct}</code>`
257
+ ).join('')}
258
+ </div>
259
+ ` : ''}
260
+ ${schema ? `
261
+ <div style="margin-top: 8px; background: var(--bg-primary); padding: 8px; border-radius: 4px;">
262
+ ${renderSchema(schema)}
263
+ </div>
264
+ ` : ''}
265
+ </div>
266
+ `;
267
+ }).join('')}
268
+ </div>
269
+ </div>
270
+
271
+
272
+ ${source ? `
273
+ <div class="source-section">
274
+ <h3 style="margin-bottom:8px; font-size:1.1rem; color:var(--text-primary);">Source Code</h3>
275
+ <div class="source-header" style="justify-content: flex-start; margin-bottom: 8px;">
276
+ <a href="vscode://file/${source.file}:${source.line}" class="doc-source-link" title="${source.file}:${source.line}">
277
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px">
278
+ <polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline>
279
+ </svg>
280
+ ${source.file.split('/').pop()}:${source.line}
281
+ </a>
282
+ </div>
283
+ <div id="monaco-source-viewer" class="monaco-container"></div>
284
+ </div>
285
+ ` : ''}
286
+ </div>
287
+ </div>
288
+
289
+ <div class="panel-section" id="tab-params">
290
+ ${uniqueParams.length > 0 ? renderParamsTable(uniqueParams) : '<div style="padding:16px; color:var(--text-secondary)">No parameters</div>'}
291
+ </div>
292
+ <div class="panel-section" id="tab-headers">
293
+ <div class="headers-editor" style="padding:16px;">
294
+ <div class="headers-table" id="req-headers-table">
295
+ <!-- Default headers -->
296
+ <div class="header-row">
297
+ <div style="flex:1"><input type="text" class="header-name" value="Accept" placeholder="Header Name" /></div>
298
+ <div style="flex:2"><input type="text" class="header-value" value="*/*" placeholder="Value" /></div>
299
+ <button class="btn icon-btn remove-header" title="Remove Header">
300
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
301
+ </button>
302
+ </div>
303
+ <div class="header-row">
304
+ <div style="flex:1"><input type="text" class="header-name" value="Content-Type" placeholder="Header Name" /></div>
305
+ <div style="flex:2"><input type="text" class="header-value" value="application/json" placeholder="Value" /></div>
306
+ <button class="btn icon-btn remove-header" title="Remove Header">
307
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
308
+ </button>
309
+ </div>
310
+ </div>
311
+ <button class="btn" id="btn-add-header" style="margin-top:12px; font-size:0.8rem">+ Add Header</button>
312
+ </div>
313
+ </div>
314
+ ${hasBody ? `
315
+ <div class="panel-section" id="tab-body" style="height:100%; display:none; flex-direction:column;">
316
+ <div id="monaco-request-body" class="monaco-container"></div>
317
+ </div>
318
+ ` : ''}
319
+ <div class="panel-section" id="tab-auth">
320
+ <div style="padding:16px; color:var(--text-secondary)">
321
+ No authentication configured
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </div>
326
+
327
+ <!-- Resizer -->
328
+ <div class="panel-resizer"></div>
329
+
330
+ <!-- Response Panel -->
331
+ <div class="ide-panel response-panel">
332
+ <div class="response-status-bar">
333
+ <span style="margin-right:8px;">Response</span>
334
+ <span id="response-meta"></span>
335
+ <span style="flex: 1"></span>
336
+ <button class="btn" id="btn-download-response" style="display:none; margin-right:8px;">Download</button>
337
+ <button class="btn" id="btn-copy-response" style="display:none;">Copy</button>
338
+ </div>
339
+ <div class="monaco-container" id="monaco-response-body">
340
+ <div class="response-loader" style="display:none">Sending...</div>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ `;
345
+
346
+ container.innerHTML = html;
347
+
348
+ // Setup tabs
349
+ container.querySelectorAll('.panel-tab').forEach(tab => {
350
+ tab.addEventListener('click', () => {
351
+ container.querySelectorAll('.panel-tab').forEach(t => t.classList.remove('active'));
352
+ container.querySelectorAll('.panel-section').forEach(s => {
353
+ s.style.display = 'none';
354
+ s.classList.remove('active');
355
+ });
356
+
357
+ tab.classList.add('active');
358
+ const target = container.querySelector(`#tab-${tab.dataset.tab}`);
359
+ if (target) {
360
+ target.style.display = 'flex'; // Use flex for monaco containers to fill
361
+ target.classList.add('active');
362
+
363
+ // Layout monaco
364
+ if (tab.dataset.tab === 'body' && currentEditors.request) {
365
+ currentEditors.request.layout();
366
+ }
367
+ if (tab.dataset.tab === 'info' && currentEditors.source) {
368
+ currentEditors.source.layout();
369
+ }
370
+ }
371
+ });
372
+ });
373
+
374
+ // Initialize Monaco
375
+ initMonaco();
376
+
377
+ // Event Listeners
378
+ document.getElementById('btn-send').addEventListener('click', () => doSendRequest(route));
379
+
380
+ // Copy buttons
381
+ container.querySelector('.copy-curl').addEventListener('click', () => {
382
+ const text = buildCurl(route);
383
+ copyToClipboard(text);
384
+ });
385
+ container.querySelector('.copy-fetch').addEventListener('click', () => {
386
+ const text = buildFetch(route);
387
+ copyToClipboard(text);
388
+ });
389
+
390
+ document.getElementById('btn-copy-response').addEventListener('click', () => {
391
+ if (currentEditors.response) {
392
+ copyToClipboard(currentEditors.response.getValue());
393
+ }
394
+ });
395
+
396
+ // Headers Management
397
+ const headersTable = container.querySelector('#req-headers-table');
398
+
399
+ // Add Header
400
+ container.querySelector('#btn-add-header').addEventListener('click', () => {
401
+ const row = document.createElement('div');
402
+ row.className = 'header-row';
403
+ row.innerHTML = `
404
+ <div style="flex:1"><input type="text" class="header-name" placeholder="Header Name" /></div>
405
+ <div style="flex:2"><input type="text" class="header-value" placeholder="Value" /></div>
406
+ <button class="btn icon-btn remove-header" title="Remove Header">
407
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
408
+ </button>
409
+ `;
410
+ headersTable.appendChild(row);
411
+ setupRemoveHeaderBtn(row.querySelector('.remove-header'));
412
+ });
413
+
414
+ // Remove Header Delegate
415
+ function setupRemoveHeaderBtn(btn) {
416
+ btn.addEventListener('click', (e) => {
417
+ e.currentTarget.closest('.header-row').remove();
418
+ });
419
+ }
420
+
421
+ // Setup initial remove buttons
422
+ container.querySelectorAll('.remove-header').forEach(setupRemoveHeaderBtn);
423
+
424
+ // Populate Request Body if example exists
425
+ if (hasBody && currentEditors.request) {
426
+ currentEditors.request.setValue('{\n \n}');
427
+ }
428
+
429
+ setupPanelResizer(container);
430
+ }
431
+
432
+ function setupPanelResizer(container) {
433
+ const resizer = container.querySelector('.panel-resizer');
434
+ const topPanel = container.querySelector('.request-panel');
435
+ const bottomPanel = container.querySelector('.response-panel');
436
+
437
+ if (!resizer || !topPanel || !bottomPanel) return;
438
+
439
+ let isResizing = false;
440
+
441
+ resizer.addEventListener('mousedown', (e) => {
442
+ isResizing = true;
443
+ resizer.classList.add('active');
444
+ document.body.style.cursor = 'row-resize';
445
+ document.body.style.userSelect = 'none'; // Prevent selection
446
+ e.preventDefault();
447
+ });
448
+
449
+ // Use document for move/up to catch fast movements outside the element
450
+ document.addEventListener('mousemove', (e) => {
451
+ if (!isResizing) return;
452
+
453
+ const containerRect = container.getBoundingClientRect();
454
+ // Calculate percentage relative to container height
455
+ // Offset by top of container
456
+ const relativeY = e.clientY - containerRect.top;
457
+ const percentage = (relativeY / containerRect.height) * 100;
458
+
459
+ // Clamp between 20% and 80% to prevent full collapse
460
+ const clamped = Math.max(20, Math.min(80, percentage));
461
+
462
+ topPanel.style.flex = `0 0 ${clamped}%`;
463
+ // bottomPanel is flex: 1, so it takes the rest
464
+ });
465
+
466
+ document.addEventListener('mouseup', () => {
467
+ if (isResizing) {
468
+ isResizing = false;
469
+ resizer.classList.remove('active');
470
+ document.body.style.cursor = '';
471
+ document.body.style.userSelect = '';
472
+
473
+ // Trigger monaco layout as size changed
474
+ if (currentEditors.request) currentEditors.request.layout();
475
+ if (currentEditors.response) currentEditors.response.layout();
476
+ if (currentEditors.source) currentEditors.source.layout();
477
+ }
478
+ });
479
+ }
480
+
481
+ function renderParamsTable(params) {
482
+ return `
483
+ <div class="params-table">
484
+ ${params.map(p => `
485
+ <div class="param-row">
486
+ <div class="param-key">${p.name}${p.required ? '*' : ''}</div>
487
+ <div class="param-value">
488
+ <input type="text" name="param-${p.name}" data-in="${p.in}" placeholder="${p.description || ''}" />
489
+ </div>
490
+ </div>
491
+ `).join('')}
492
+ </div>
493
+ `;
494
+ }
495
+
496
+ function getUniqueParams(op) {
497
+ const uniqueParams = [];
498
+ const seen = new Set();
499
+ (op.parameters || []).forEach((p) => {
500
+ const key = `${p.name}-${p.in}`;
501
+ if (!seen.has(key)) {
502
+ seen.add(key);
503
+ uniqueParams.push(p);
504
+ }
505
+ });
506
+ return uniqueParams;
507
+ }
508
+
509
+ // --- Monaco Integration ---
510
+
511
+ function initMonaco() {
512
+ if (typeof require === 'undefined') return;
513
+
514
+ require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
515
+
516
+ require(['vs/editor/editor.main'], function () {
517
+ // Source Editor (In Info Tab)
518
+ const sourceContainer = document.getElementById('monaco-source-viewer');
519
+ if (sourceContainer && currentRoute && currentRoute.op['x-shokupan-source']) {
520
+ const source = currentRoute.op['x-shokupan-source'];
521
+ if (currentEditors.source) currentEditors.source.dispose();
522
+
523
+ // Initial placeholder
524
+ currentEditors.source = monaco.editor.create(sourceContainer, {
525
+ value: '// Loading source...',
526
+ language: 'typescript',
527
+ theme: 'vs-dark',
528
+ minimap: { enabled: false },
529
+ lineNumbers: 'on',
530
+ readOnly: true,
531
+ scrollBeyondLastLine: false,
532
+ automaticLayout: true,
533
+ scrollBeyondLastLine: false,
534
+ automaticLayout: true,
535
+ glyphMargin: false, // Removed per request
536
+ folding: false, // Removed per request (extra gutter room)
537
+ lineNumbersMinChars: 3,
538
+ fontSize: 13,
539
+ fontFamily: 'JetBrains Mono',
540
+ renderLineHighlight: 'none'
541
+ });
542
+
543
+ // Lazy load source
544
+ fetch(`_source?file=${encodeURIComponent(source.file)}`)
545
+ .then(res => {
546
+ if (!res.ok) throw new Error(res.statusText);
547
+ return res.text();
548
+ })
549
+ .then(text => {
550
+ if (currentEditors.source) {
551
+ currentEditors.source.setValue(text);
552
+
553
+ const op = currentRoute.op;
554
+ const sourceInfo = op['x-source-info'] || {};
555
+ const highlights = sourceInfo.highlightLines || (source.line ? [source.line, source.line] : null);
556
+
557
+ const decorations = [];
558
+
559
+ // 1. Highlight the main range (Closure)
560
+ if (highlights) {
561
+ const startLine = highlights[0];
562
+ const endLine = highlights[1] || startLine;
563
+
564
+ if (startLine > 0) {
565
+ decorations.push({
566
+ range: new monaco.Range(startLine, 1, endLine, 1),
567
+ options: {
568
+ isWholeLine: true,
569
+ className: 'closure-highlight'
570
+ }
571
+ });
572
+ currentEditors.source.revealLineInCenter(startLine);
573
+ }
574
+ }
575
+
576
+ // 2. Highlight specific statements (returns, emits)
577
+ if (sourceInfo.highlights) {
578
+ sourceInfo.highlights.forEach(h => {
579
+ if (h.startLine > 0) {
580
+ let className = 'warning-line-highlight'; // verification default
581
+ if (h.type === 'emit') className = 'emit-highlight';
582
+ else if (h.type === 'return-success') className = 'success-line-highlight';
583
+ else if (h.type === 'return-warning') className = 'warning-line-highlight';
584
+ // Fallback for older 'return' type if any mixed
585
+ else if (h.type === 'return') className = 'warning-line-highlight';
586
+
587
+ decorations.push({
588
+ range: new monaco.Range(h.startLine, 1, h.endLine, 1),
589
+ options: {
590
+ isWholeLine: true,
591
+ className: className
592
+ // glyphMarginClassName removed as glyphMargin is false
593
+ }
594
+ });
595
+ }
596
+ });
597
+ }
598
+
599
+ currentEditors.source.deltaDecorations([], decorations);
600
+ }
601
+ })
602
+ .catch(err => {
603
+ if (currentEditors.source) {
604
+ currentEditors.source.setValue(`// Failed to load source: ${err.message}`);
605
+ }
606
+ });
607
+ }
608
+
609
+ // Request Editor
610
+ const reqContainer = document.getElementById('monaco-request-body');
611
+ if (reqContainer) {
612
+ if (currentEditors.request) currentEditors.request.dispose();
613
+
614
+ currentEditors.request = monaco.editor.create(reqContainer, {
615
+ value: '',
616
+ language: 'json',
617
+ theme: 'vs-dark',
618
+ minimap: { enabled: false },
619
+ lineNumbers: 'on',
620
+ scrollBeyondLastLine: false,
621
+ automaticLayout: true,
622
+ glyphMargin: false,
623
+ folding: true,
624
+ fontSize: 13,
625
+ fontFamily: 'JetBrains Mono',
626
+ renderLineHighlight: 'none'
627
+ });
628
+ }
629
+
630
+ // Response Editor
631
+ const resContainer = document.getElementById('monaco-response-body');
632
+ if (resContainer) {
633
+ if (currentEditors.response) currentEditors.response.dispose();
634
+
635
+ currentEditors.response = monaco.editor.create(resContainer, {
636
+ value: '// Response will appear here',
637
+ language: 'json',
638
+ theme: 'vs-dark',
639
+ minimap: { enabled: false },
640
+ lineNumbers: 'on',
641
+ readOnly: true,
642
+ scrollBeyondLastLine: false,
643
+ automaticLayout: true,
644
+ glyphMargin: false,
645
+ fontSize: 13,
646
+ fontFamily: 'JetBrains Mono'
647
+ });
648
+ }
649
+ });
650
+ }
651
+
652
+ // --- Request Construction Helper ---
653
+
654
+ function getRequestData(route) {
655
+ const { method, path } = route;
656
+ const urlObj = new URL(path, window.location.origin);
657
+ const headers = {};
658
+
659
+ // Collect Params
660
+ document.querySelectorAll(`input[name^="param-"]`).forEach(input => {
661
+ const name = input.name.replace('param-', '');
662
+ const val = input.value;
663
+ const place = input.dataset.in; // query, path, header
664
+
665
+ if (!val) return;
666
+
667
+ if (place === 'query') urlObj.searchParams.set(name, val);
668
+ else if (place === 'header') headers[name] = val;
669
+ else if (place === 'path') urlObj.pathname = urlObj.pathname.replace(`{${name}}`, encodeURIComponent(val));
670
+ });
671
+
672
+ // Collect Headers
673
+ document.querySelectorAll('.header-row').forEach(row => {
674
+ const nameInput = row.querySelector('.header-name');
675
+ const valueInput = row.querySelector('.header-value');
676
+ if (nameInput && valueInput && nameInput.value) {
677
+ headers[nameInput.value] = valueInput.value;
678
+ }
679
+ });
680
+
681
+ // Body
682
+ let body = null;
683
+ if (currentEditors.request) {
684
+ try {
685
+ const bodyVal = currentEditors.request.getValue();
686
+ if (bodyVal && bodyVal.trim()) {
687
+ body = bodyVal;
688
+ }
689
+ } catch (e) { }
690
+ }
691
+
692
+ return { url: urlObj.toString(), method: method.toUpperCase(), headers, body };
693
+ }
694
+
695
+ // --- Request Execution ---
696
+
697
+ async function doSendRequest(route) {
698
+ const { url, method, headers, body } = getRequestData(route);
699
+ const options = { method, headers };
700
+
701
+ // Only include body for methods that support it (not GET/HEAD)
702
+ if (body && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
703
+ options.body = body;
704
+ }
705
+
706
+ // UI Updates
707
+ const responseMeta = document.getElementById('response-meta');
708
+ const loader = document.querySelector('.response-loader');
709
+ if (loader) loader.style.display = 'flex';
710
+ if (currentEditors.response) currentEditors.response.setValue('// Loading...');
711
+
712
+ const startTime = Date.now();
713
+ try {
714
+ const res = await fetch(url, options);
715
+ const duration = Date.now() - startTime;
716
+
717
+ // Handle content
718
+ const contentType = res.headers.get('content-type') || '';
719
+ let bodyContent = '';
720
+ let isBinary = false;
721
+
722
+ if (contentType.includes('application/json')) {
723
+ const json = await res.json();
724
+ bodyContent = JSON.stringify(json, null, 2);
725
+ if (currentEditors.response) monaco.editor.setModelLanguage(currentEditors.response.getModel(), 'json');
726
+ } else if (contentType.includes('text/') || contentType.includes('xml') || contentType.includes('javascript') || contentType.includes('html')) {
727
+ bodyContent = await res.text();
728
+ if (currentEditors.response) monaco.editor.setModelLanguage(currentEditors.response.getModel(), 'html'); // or text
729
+ } else {
730
+ // Binary / other
731
+ isBinary = true;
732
+ bodyContent = `[Binary Content: ${contentType}]`;
733
+ const blob = await res.blob();
734
+ setupDownloadButton(blob, 'response');
735
+ }
736
+
737
+ // Update Editor
738
+ if (currentEditors.response) currentEditors.response.setValue(bodyContent);
739
+
740
+ // Update Buttons
741
+ const copyBtn = document.getElementById('btn-copy-response');
742
+ const dlBtn = document.getElementById('btn-download-response');
743
+
744
+ if (!isBinary) {
745
+ if (copyBtn) copyBtn.style.display = 'block';
746
+ if (dlBtn) dlBtn.style.display = 'block';
747
+
748
+ // Create blob for download text
749
+ const blob = new Blob([bodyContent], { type: contentType || 'text/plain' });
750
+ setupDownloadButton(blob, 'response.' + (contentType.includes('json') ? 'json' : 'txt'));
751
+ } else {
752
+ if (copyBtn) copyBtn.style.display = 'none';
753
+ if (dlBtn) dlBtn.style.display = 'block';
754
+ }
755
+
756
+ // Status Bar
757
+ if (responseMeta) {
758
+ responseMeta.innerHTML = `
759
+ <span class="${res.ok ? 'success' : 'error'}" style="${res.ok ? 'color:#4caf50' : 'color:#f44336'}">${res.status} ${res.statusText}</span>
760
+ <span style="margin-left:12px; opacity:0.7">${duration}ms</span>
761
+ <span style="margin-left:12px; opacity:0.7">${formatSize(bodyContent.length)}</span>
762
+ `;
763
+ }
764
+
765
+ } catch (err) {
766
+ if (currentEditors.response) currentEditors.response.setValue(`Error: ${err.message}`);
767
+ if (responseMeta) responseMeta.innerHTML = `<span style="color:#f44336">Error</span>`;
768
+ } finally {
769
+ if (loader) loader.style.display = 'none';
770
+ }
771
+ }
772
+
773
+ function setupDownloadButton(blob, filename) {
774
+ const btn = document.getElementById('btn-download-response');
775
+ if (!btn) return;
776
+
777
+ // Clone to remove old listener
778
+ const newBtn = btn.cloneNode(true);
779
+ btn.parentNode.replaceChild(newBtn, btn);
780
+
781
+ newBtn.onclick = () => {
782
+ const url = URL.createObjectURL(blob);
783
+ const a = document.createElement('a');
784
+ a.href = url;
785
+ a.download = filename;
786
+ document.body.appendChild(a);
787
+ a.click();
788
+ document.body.removeChild(a);
789
+ URL.revokeObjectURL(url);
790
+ };
791
+ newBtn.style.display = 'inline-block';
792
+ }
793
+
794
+ function buildCurl(route) {
795
+ const { url, method, headers, body } = getRequestData(route);
796
+ let curl = `curl -X ${method} "${url}"`;
797
+ for (const [k, v] of Object.entries(headers)) {
798
+ curl += ` \\\n -H "${k}: ${v}"`;
799
+ }
800
+ if (body) {
801
+ // Escape quotes? Simplification
802
+ curl += ` \\\n -d '${body.replace(/'/g, "'\\''")}'`;
803
+ }
804
+ return curl;
805
+ }
806
+
807
+ function buildFetch(route) {
808
+ const { url, method, headers, body } = getRequestData(route);
809
+ const options = {
810
+ method: method,
811
+ headers: headers,
812
+ body: body ? JSON.parse(body) : undefined // simplified, assuming JSON
813
+ };
814
+ // If not JSON, leave body as string
815
+ if (body && options.body === undefined) options.body = body;
816
+
817
+ return `fetch("${url}", ${JSON.stringify(options, null, 2)})`;
818
+ }
819
+
820
+ function copyToClipboard(text) {
821
+ navigator.clipboard.writeText(text).then(() => {
822
+ // Optional toast
823
+ });
824
+ }
825
+
826
+
827
+ function formatSize(bytes) {
828
+ if (bytes === 0) return '0 B';
829
+ const k = 1024;
830
+ const sizes = ['B', 'KB', 'MB', 'GB'];
831
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
832
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
833
+ }
834
+
835
+
836
+ // --- Helpers ---
837
+ function parseMarkdown(text) {
838
+ if (!text) return '';
839
+ if (typeof marked === 'undefined') return text;
840
+
841
+ const renderer = new marked.Renderer();
842
+ const originalBlockquote = renderer.blockquote.bind(renderer);
843
+
844
+ renderer.blockquote = (quote) => {
845
+ const match = quote.match(/^<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/);
846
+ if (match) {
847
+ const type = match[1];
848
+ const content = quote.replace(/^<p>\[!.*?\]\s*/, '');
849
+ return `<div class="markdown-alert ${type.toLowerCase()}">
850
+ <div class="markdown-alert-title">${type}</div>
851
+ ${content}
852
+ </div>`;
853
+ }
854
+ return originalBlockquote(quote);
855
+ };
856
+
857
+ return marked.parse(text, { renderer });
858
+ }
859
+
860
+ function setupSidebar() {
861
+ const sidebar = document.querySelector('.sidebar');
862
+ if (!sidebar) return;
863
+
864
+ // Collapsible Groups (top-level)
865
+ document.querySelectorAll('.nav-group-title').forEach(title => {
866
+ title.addEventListener('click', (e) => {
867
+ const group = e.currentTarget.parentElement;
868
+ group.classList.toggle('collapsed');
869
+ });
870
+ });
871
+
872
+ // Collapsible Subgroups (nested)
873
+ document.querySelectorAll('.nav-subgroup-title').forEach(title => {
874
+ title.addEventListener('click', (e) => {
875
+ e.stopPropagation(); // Prevent bubbling to parent group
876
+ const subgroup = e.currentTarget.parentElement;
877
+ subgroup.classList.toggle('collapsed');
878
+ });
879
+ });
880
+ }