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.
- package/dist/analyzer-BqIe1p0R.js +35 -0
- package/dist/analyzer-BqIe1p0R.js.map +1 -0
- package/dist/analyzer-CKLGLFtx.cjs +35 -0
- package/dist/analyzer-CKLGLFtx.cjs.map +1 -0
- package/dist/{analyzer-Ce_7JxZh.js → analyzer.impl-CV6W1Eq7.js} +238 -21
- package/dist/analyzer.impl-CV6W1Eq7.js.map +1 -0
- package/dist/{analyzer-Bei1sVWp.cjs → analyzer.impl-D9Yi1Hax.cjs} +237 -20
- package/dist/analyzer.impl-D9Yi1Hax.cjs.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +19 -7
- package/dist/http-server-BEMPIs33.cjs.map +1 -1
- package/dist/http-server-CCeagTyU.js.map +1 -1
- package/dist/index.cjs +1500 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1482 -256
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/plugin.d.ts +9 -0
- package/dist/plugins/application/api-explorer/static/explorer-client.mjs +880 -0
- package/dist/plugins/application/api-explorer/static/style.css +767 -0
- package/dist/plugins/application/api-explorer/static/theme.css +128 -0
- package/dist/plugins/application/asyncapi/generator.d.ts +3 -0
- package/dist/plugins/application/asyncapi/plugin.d.ts +15 -0
- package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +748 -0
- package/dist/plugins/application/asyncapi/static/style.css +565 -0
- package/dist/plugins/application/asyncapi/static/theme.css +128 -0
- package/dist/plugins/application/auth.d.ts +3 -1
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +3 -1
- package/dist/plugins/application/dashboard/plugin.d.ts +13 -3
- package/dist/plugins/application/dashboard/static/registry.css +0 -53
- package/dist/plugins/application/dashboard/static/styles.css +29 -20
- package/dist/plugins/application/dashboard/static/tabulator.css +83 -31
- package/dist/plugins/application/dashboard/static/theme.css +128 -0
- package/dist/plugins/application/graphql-apollo.d.ts +33 -0
- package/dist/plugins/application/graphql-yoga.d.ts +25 -0
- package/dist/plugins/application/openapi/analyzer.d.ts +12 -119
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +167 -0
- package/dist/plugins/application/scalar.d.ts +9 -2
- package/dist/router.d.ts +80 -51
- package/dist/shokupan.d.ts +14 -8
- package/dist/util/datastore.d.ts +71 -7
- package/dist/util/decorators.d.ts +2 -2
- package/dist/util/types.d.ts +96 -3
- package/package.json +32 -12
- package/dist/analyzer-Bei1sVWp.cjs.map +0 -1
- package/dist/analyzer-Ce_7JxZh.js.map +0 -1
- package/dist/plugins/application/dashboard/static/scrollbar.css +0 -24
- 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, '"') : ''}">${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
|
+
}
|