zero-http 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/documentation/controllers/cleanup.js +25 -0
- package/documentation/controllers/echo.js +14 -0
- package/documentation/controllers/headers.js +1 -0
- package/documentation/controllers/proxy.js +112 -0
- package/documentation/controllers/root.js +1 -0
- package/documentation/controllers/uploads.js +289 -0
- package/documentation/full-server.js +129 -0
- package/documentation/public/data/api.json +167 -0
- package/documentation/public/data/examples.json +62 -0
- package/documentation/public/data/options.json +13 -0
- package/documentation/public/index.html +414 -0
- package/documentation/public/prism-overrides.css +40 -0
- package/documentation/public/scripts/app.js +44 -0
- package/documentation/public/scripts/data-sections.js +300 -0
- package/documentation/public/scripts/helpers.js +166 -0
- package/documentation/public/scripts/playground.js +71 -0
- package/documentation/public/scripts/proxy.js +98 -0
- package/documentation/public/scripts/ui.js +210 -0
- package/documentation/public/scripts/uploads.js +459 -0
- package/documentation/public/styles.css +310 -0
- package/documentation/public/vendor/icons/fetch.svg +23 -0
- package/documentation/public/vendor/icons/plug.svg +27 -0
- package/documentation/public/vendor/icons/static.svg +35 -0
- package/documentation/public/vendor/icons/stream.svg +22 -0
- package/documentation/public/vendor/icons/zero.svg +21 -0
- package/documentation/public/vendor/prism-copy-to-clipboard.min.js +27 -0
- package/documentation/public/vendor/prism-javascript.min.js +1 -0
- package/documentation/public/vendor/prism-json.min.js +1 -0
- package/documentation/public/vendor/prism-okaidia.css +1 -0
- package/documentation/public/vendor/prism-toolbar.css +27 -0
- package/documentation/public/vendor/prism-toolbar.min.js +41 -0
- package/documentation/public/vendor/prism.min.js +1 -0
- package/index.js +43 -0
- package/lib/app.js +159 -0
- package/lib/body/index.js +14 -0
- package/lib/body/json.js +54 -0
- package/lib/body/multipart.js +310 -0
- package/lib/body/raw.js +40 -0
- package/lib/body/rawBuffer.js +74 -0
- package/lib/body/sendError.js +17 -0
- package/lib/body/text.js +43 -0
- package/lib/body/typeMatch.js +22 -0
- package/lib/body/urlencoded.js +166 -0
- package/lib/cors.js +72 -0
- package/lib/fetch.js +218 -0
- package/lib/logger.js +68 -0
- package/lib/rateLimit.js +64 -0
- package/lib/request.js +76 -0
- package/lib/response.js +165 -0
- package/lib/router.js +87 -0
- package/lib/static.js +196 -0
- package/package.json +44 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* data-sections.js
|
|
3
|
+
* Fetches and renders the three data-driven documentation sections:
|
|
4
|
+
* - API Reference (/data/api.json)
|
|
5
|
+
* - Options Table (/data/options.json)
|
|
6
|
+
* - Code Examples (/data/examples.json)
|
|
7
|
+
*
|
|
8
|
+
* Also populates the sidebar TOC with sub-items for the API reference and
|
|
9
|
+
* examples sections.
|
|
10
|
+
*
|
|
11
|
+
* Depends on: helpers.js (provides $, escapeHtml, slugify, showJsonResult,
|
|
12
|
+
* highlightAllPre)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/* ── TOC Helpers ───────────────────────────────────────────────────────────── */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find a top-level `<li>` in the sidebar TOC whose link matches the given
|
|
19
|
+
* href, then append a sub-list of items beneath it.
|
|
20
|
+
* @param {string} href - Hash href to match (e.g. "#api-reference").
|
|
21
|
+
* @param {Object[]} items - Array of `{ slug, label }` objects.
|
|
22
|
+
*/
|
|
23
|
+
function populateTocSub(href, items)
|
|
24
|
+
{
|
|
25
|
+
const nav = document.querySelector('.toc-sidebar nav ul');
|
|
26
|
+
if (!nav || !items || !items.length) return;
|
|
27
|
+
|
|
28
|
+
const parentLi = Array.from(nav.children).find(li =>
|
|
29
|
+
{
|
|
30
|
+
const a = li.querySelector && li.querySelector(`a[href="${href}"]`);
|
|
31
|
+
return !!a;
|
|
32
|
+
});
|
|
33
|
+
if (!parentLi) return;
|
|
34
|
+
|
|
35
|
+
/* Remove existing sub-list if present (re-render safe) */
|
|
36
|
+
const existing = parentLi.querySelector('ul.toc-sub');
|
|
37
|
+
if (existing) existing.remove();
|
|
38
|
+
|
|
39
|
+
const sub = document.createElement('ul');
|
|
40
|
+
sub.className = 'toc-sub';
|
|
41
|
+
|
|
42
|
+
for (const { slug, label } of items)
|
|
43
|
+
{
|
|
44
|
+
const li = document.createElement('li');
|
|
45
|
+
li.className = 'toc-sub-item';
|
|
46
|
+
|
|
47
|
+
const a = document.createElement('a');
|
|
48
|
+
a.href = '#' + slug;
|
|
49
|
+
a.textContent = label;
|
|
50
|
+
a.addEventListener('click', () => document.body.classList.remove('toc-open'));
|
|
51
|
+
|
|
52
|
+
li.appendChild(a);
|
|
53
|
+
sub.appendChild(li);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
parentLi.appendChild(sub);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ── API Reference ─────────────────────────────────────────────────────────── */
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render a list of API items into the `#api-items` container and Prism-highlight
|
|
63
|
+
* any code examples.
|
|
64
|
+
* @param {HTMLElement} container - Target element.
|
|
65
|
+
* @param {Object[]} list - API item descriptors.
|
|
66
|
+
*/
|
|
67
|
+
function renderApiList(container, list)
|
|
68
|
+
{
|
|
69
|
+
container.innerHTML = '';
|
|
70
|
+
if (!list || !list.length) { container.textContent = 'No API items'; return; }
|
|
71
|
+
|
|
72
|
+
for (const it of list)
|
|
73
|
+
{
|
|
74
|
+
const d = document.createElement('details');
|
|
75
|
+
d.className = 'acc nested';
|
|
76
|
+
d.id = 'api-' + slugify(it.name || '');
|
|
77
|
+
|
|
78
|
+
const s = document.createElement('summary');
|
|
79
|
+
s.innerHTML = `<strong>${escapeHtml(it.name)}</strong>`;
|
|
80
|
+
d.appendChild(s);
|
|
81
|
+
|
|
82
|
+
const body = document.createElement('div');
|
|
83
|
+
body.className = 'acc-body';
|
|
84
|
+
|
|
85
|
+
/* Description */
|
|
86
|
+
if (it.description)
|
|
87
|
+
{
|
|
88
|
+
const p = document.createElement('p');
|
|
89
|
+
p.innerHTML = escapeHtml(it.description);
|
|
90
|
+
body.appendChild(p);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Options table */
|
|
94
|
+
if (Array.isArray(it.options) && it.options.length)
|
|
95
|
+
{
|
|
96
|
+
const table = document.createElement('table');
|
|
97
|
+
table.innerHTML = '<thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>';
|
|
98
|
+
const tbody = document.createElement('tbody');
|
|
99
|
+
for (const opt of it.options)
|
|
100
|
+
{
|
|
101
|
+
const tr = document.createElement('tr');
|
|
102
|
+
tr.innerHTML =
|
|
103
|
+
`<td><code>${escapeHtml(opt.option)}</code></td>` +
|
|
104
|
+
`<td>${escapeHtml(opt.type || '')}</td>` +
|
|
105
|
+
`<td>${escapeHtml(opt.default || '')}</td>` +
|
|
106
|
+
`<td>${escapeHtml(opt.notes || '')}</td>`;
|
|
107
|
+
tbody.appendChild(tr);
|
|
108
|
+
}
|
|
109
|
+
table.appendChild(tbody);
|
|
110
|
+
body.appendChild(table);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Methods table */
|
|
114
|
+
if (Array.isArray(it.methods) && it.methods.length)
|
|
115
|
+
{
|
|
116
|
+
const table = document.createElement('table');
|
|
117
|
+
table.innerHTML = '<thead><tr><th>Method</th><th>Signature</th><th>Description</th></tr></thead>';
|
|
118
|
+
const tbody = document.createElement('tbody');
|
|
119
|
+
for (const m of it.methods)
|
|
120
|
+
{
|
|
121
|
+
const tr = document.createElement('tr');
|
|
122
|
+
tr.innerHTML =
|
|
123
|
+
`<td><code>${escapeHtml(m.method || '')}</code></td>` +
|
|
124
|
+
`<td><code>${escapeHtml(m.signature || '')}</code></td>` +
|
|
125
|
+
`<td>${escapeHtml(m.description || '')}</td>`;
|
|
126
|
+
tbody.appendChild(tr);
|
|
127
|
+
}
|
|
128
|
+
table.appendChild(tbody);
|
|
129
|
+
body.appendChild(table);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Example code block */
|
|
133
|
+
if (it.example)
|
|
134
|
+
{
|
|
135
|
+
const h6 = document.createElement('h6');
|
|
136
|
+
h6.textContent = 'Example';
|
|
137
|
+
const pre = document.createElement('pre');
|
|
138
|
+
pre.className = 'language-javascript code';
|
|
139
|
+
const code = document.createElement('code');
|
|
140
|
+
code.className = 'language-javascript';
|
|
141
|
+
code.textContent = it.example;
|
|
142
|
+
pre.appendChild(code);
|
|
143
|
+
body.appendChild(h6);
|
|
144
|
+
body.appendChild(pre);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
d.appendChild(body);
|
|
148
|
+
container.appendChild(d);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try { highlightAllPre(); } catch (e) { }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Fetch the API reference JSON, render it, populate the sidebar TOC, and wire
|
|
156
|
+
* the search / clear filter controls.
|
|
157
|
+
*/
|
|
158
|
+
async function loadApiReference()
|
|
159
|
+
{
|
|
160
|
+
try
|
|
161
|
+
{
|
|
162
|
+
const res = await fetch('/data/api.json', { cache: 'no-store' });
|
|
163
|
+
if (!res.ok) return;
|
|
164
|
+
|
|
165
|
+
const items = await res.json();
|
|
166
|
+
const container = document.getElementById('api-items');
|
|
167
|
+
if (!container) return;
|
|
168
|
+
|
|
169
|
+
window._apiItems = items;
|
|
170
|
+
renderApiList(container, items);
|
|
171
|
+
|
|
172
|
+
/* Sidebar TOC sub-items */
|
|
173
|
+
populateTocSub('#api-reference', items.map(it => ({
|
|
174
|
+
slug: 'api-' + slugify(it.name),
|
|
175
|
+
label: it.name || '',
|
|
176
|
+
})));
|
|
177
|
+
|
|
178
|
+
/* Search / clear filter */
|
|
179
|
+
const search = document.getElementById('api-search');
|
|
180
|
+
const clearBtn = document.getElementById('api-clear');
|
|
181
|
+
|
|
182
|
+
const doFilter = () =>
|
|
183
|
+
{
|
|
184
|
+
const q = (search && search.value || '').trim().toLowerCase();
|
|
185
|
+
if (!q) return renderApiList(container, window._apiItems);
|
|
186
|
+
const filtered = window._apiItems.filter(it =>
|
|
187
|
+
(it.name || '').toLowerCase().includes(q) ||
|
|
188
|
+
(it.description || '').toLowerCase().includes(q) ||
|
|
189
|
+
JSON.stringify(it.options || []).toLowerCase().includes(q)
|
|
190
|
+
);
|
|
191
|
+
renderApiList(container, filtered);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (search) search.addEventListener('input', doFilter);
|
|
195
|
+
if (clearBtn) clearBtn.addEventListener('click', () => { if (search) search.value = ''; renderApiList(container, window._apiItems); });
|
|
196
|
+
} catch (e) { }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ── Options Table ─────────────────────────────────────────────────────────── */
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Fetch the options JSON and render a `<table>` into `#options-items`.
|
|
203
|
+
*/
|
|
204
|
+
async function loadOptions()
|
|
205
|
+
{
|
|
206
|
+
try
|
|
207
|
+
{
|
|
208
|
+
const container = document.getElementById('options-items');
|
|
209
|
+
if (!container) return;
|
|
210
|
+
|
|
211
|
+
const res = await fetch('/data/options.json', { cache: 'no-store' });
|
|
212
|
+
if (!res.ok) { container.textContent = 'Error loading options: ' + res.status + ' ' + res.statusText; return; }
|
|
213
|
+
|
|
214
|
+
let items;
|
|
215
|
+
try { items = await res.json(); }
|
|
216
|
+
catch (err) { container.textContent = 'Error parsing options JSON'; return; }
|
|
217
|
+
|
|
218
|
+
const table = document.createElement('table');
|
|
219
|
+
table.innerHTML = '<thead><tr><th>Option</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>';
|
|
220
|
+
const tbody = document.createElement('tbody');
|
|
221
|
+
|
|
222
|
+
for (const it of items)
|
|
223
|
+
{
|
|
224
|
+
const tr = document.createElement('tr');
|
|
225
|
+
tr.innerHTML =
|
|
226
|
+
`<td><strong>${escapeHtml(it.option || '')}</strong></td>` +
|
|
227
|
+
`<td>${escapeHtml(it.type || '')}</td>` +
|
|
228
|
+
`<td>${escapeHtml(it.default || '')}</td>` +
|
|
229
|
+
`<td>${escapeHtml(it.notes || it.description || '')}</td>`;
|
|
230
|
+
tbody.appendChild(tr);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
table.appendChild(tbody);
|
|
234
|
+
container.innerHTML = '';
|
|
235
|
+
container.appendChild(table);
|
|
236
|
+
} catch (e) { console.error('loadOptions error', e); }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ── Code Examples ─────────────────────────────────────────────────────────── */
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Fetch the examples JSON, render each as a collapsible accordion, and
|
|
243
|
+
* populate the sidebar TOC.
|
|
244
|
+
*/
|
|
245
|
+
async function loadExamples()
|
|
246
|
+
{
|
|
247
|
+
try
|
|
248
|
+
{
|
|
249
|
+
const container = document.getElementById('examples-items');
|
|
250
|
+
if (!container) return;
|
|
251
|
+
|
|
252
|
+
const res = await fetch('/data/examples.json', { cache: 'no-store' });
|
|
253
|
+
if (!res.ok) { container.textContent = 'Error loading examples: ' + res.status; return; }
|
|
254
|
+
|
|
255
|
+
const items = await res.json();
|
|
256
|
+
container.innerHTML = '';
|
|
257
|
+
|
|
258
|
+
for (const it of items)
|
|
259
|
+
{
|
|
260
|
+
const id = 'example-' + slugify(it.title);
|
|
261
|
+
const d = document.createElement('details');
|
|
262
|
+
d.className = 'acc nested';
|
|
263
|
+
d.id = id;
|
|
264
|
+
|
|
265
|
+
const s = document.createElement('summary');
|
|
266
|
+
s.innerHTML = `<strong>${escapeHtml(it.title || '')}</strong>`;
|
|
267
|
+
d.appendChild(s);
|
|
268
|
+
|
|
269
|
+
const body = document.createElement('div');
|
|
270
|
+
body.className = 'acc-body';
|
|
271
|
+
|
|
272
|
+
if (it.description)
|
|
273
|
+
{
|
|
274
|
+
const p = document.createElement('p');
|
|
275
|
+
p.className = 'muted';
|
|
276
|
+
p.textContent = it.description;
|
|
277
|
+
body.appendChild(p);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const pre = document.createElement('pre');
|
|
281
|
+
pre.className = it.language ? 'language-' + it.language + ' code' : 'code';
|
|
282
|
+
const code = document.createElement('code');
|
|
283
|
+
if (it.language) code.className = 'language-' + it.language;
|
|
284
|
+
code.textContent = it.code || '';
|
|
285
|
+
pre.appendChild(code);
|
|
286
|
+
body.appendChild(pre);
|
|
287
|
+
|
|
288
|
+
d.appendChild(body);
|
|
289
|
+
container.appendChild(d);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try { highlightAllPre(); } catch (e) { }
|
|
293
|
+
|
|
294
|
+
/* Sidebar TOC sub-items */
|
|
295
|
+
populateTocSub('#simple-examples', items.map(it => ({
|
|
296
|
+
slug: 'example-' + slugify(it.title),
|
|
297
|
+
label: it.title || '',
|
|
298
|
+
})));
|
|
299
|
+
} catch (e) { console.error('loadExamples error', e); }
|
|
300
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* helpers.js
|
|
3
|
+
* Shared DOM selectors, formatters, and rendering utilities used across all
|
|
4
|
+
* documentation scripts. Loaded first so every other module can rely on these.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* ── DOM Selectors ─────────────────────────────────────────────────────────── */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Query a single element by CSS selector.
|
|
11
|
+
* @param {string} sel - CSS selector string.
|
|
12
|
+
* @param {Element} ctx - Optional root element (defaults to `document`).
|
|
13
|
+
* @returns {Element|null}
|
|
14
|
+
*/
|
|
15
|
+
const $ = (sel, ctx = document) => ctx.querySelector(sel);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Query all matching elements and return a real array.
|
|
19
|
+
* @param {string} sel - CSS selector string.
|
|
20
|
+
* @param {Element} ctx - Optional root element (defaults to `document`).
|
|
21
|
+
* @returns {Element[]}
|
|
22
|
+
*/
|
|
23
|
+
const $$ = (sel, ctx = document) => Array.from((ctx || document).querySelectorAll(sel));
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Attach an event listener, safely no-ops if element is null.
|
|
27
|
+
* @param {Element|null} el - Target element.
|
|
28
|
+
* @param {string} evt - Event name.
|
|
29
|
+
* @param {Function} cb - Callback.
|
|
30
|
+
*/
|
|
31
|
+
function on(el, evt, cb)
|
|
32
|
+
{
|
|
33
|
+
if (!el) return;
|
|
34
|
+
el.addEventListener(evt, cb);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ── String Utilities ──────────────────────────────────────────────────────── */
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Escape HTML special characters for safe insertion into the DOM.
|
|
41
|
+
* @param {string} s - Raw string.
|
|
42
|
+
* @returns {string} Escaped string.
|
|
43
|
+
*/
|
|
44
|
+
function escapeHtml(s)
|
|
45
|
+
{
|
|
46
|
+
return s
|
|
47
|
+
.replace(/&/g, '&')
|
|
48
|
+
.replace(/</g, '<')
|
|
49
|
+
.replace(/>/g, '>');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert a human-readable name into a URL-safe slug.
|
|
54
|
+
* @param {string} str - Input string.
|
|
55
|
+
* @returns {string} Lower-cased, hyphenated slug.
|
|
56
|
+
*/
|
|
57
|
+
function slugify(str)
|
|
58
|
+
{
|
|
59
|
+
return (str || '').toString()
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
62
|
+
.replace(/(^-|-$)/g, '');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format a byte count into a human-readable string (B / KB / MB / GB).
|
|
67
|
+
* @param {number} n - Byte count.
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function formatBytes(n)
|
|
71
|
+
{
|
|
72
|
+
if (n === 0) return '0 B';
|
|
73
|
+
const k = 1024;
|
|
74
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
75
|
+
const i = Math.floor(Math.log(n) / Math.log(k));
|
|
76
|
+
return (n / Math.pow(k, i)).toFixed(i ? 1 : 0) + ' ' + sizes[i];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ── JSON / Code Rendering ─────────────────────────────────────────────────── */
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a highlighted `<pre>` block containing pretty-printed JSON.
|
|
83
|
+
* @param {*} obj - Any JSON-serialisable value.
|
|
84
|
+
* @returns {string} HTML string.
|
|
85
|
+
*/
|
|
86
|
+
function jsonHtml(obj)
|
|
87
|
+
{
|
|
88
|
+
return `<pre class="code language-json"><code>${escapeHtml(JSON.stringify(obj, null, 2))}</code></pre>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Render a JSON object into a container element and trigger Prism highlighting.
|
|
93
|
+
* @param {Element|null} container - Target element.
|
|
94
|
+
* @param {*} obj - JSON-serialisable value.
|
|
95
|
+
*/
|
|
96
|
+
function showJsonResult(container, obj)
|
|
97
|
+
{
|
|
98
|
+
if (!container) return;
|
|
99
|
+
container.innerHTML = jsonHtml(obj);
|
|
100
|
+
try { highlightAllPre(); } catch (e) { }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ── Prism / Code-Block Helpers ────────────────────────────────────────────── */
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Trigger Prism syntax highlighting on all `<pre class="code">` blocks, or
|
|
107
|
+
* fall back to a simple escape-and-wrap if Prism is not loaded.
|
|
108
|
+
*/
|
|
109
|
+
function highlightAllPre()
|
|
110
|
+
{
|
|
111
|
+
if (window.Prism && typeof Prism.highlightAll === 'function')
|
|
112
|
+
{
|
|
113
|
+
try { Prism.highlightAll(); } catch (e) { }
|
|
114
|
+
document.querySelectorAll('pre.code').forEach(p => p.dataset.miniExpressHighlighted = '1');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try
|
|
118
|
+
{
|
|
119
|
+
document.querySelectorAll('pre.code').forEach(p =>
|
|
120
|
+
{
|
|
121
|
+
if (p.dataset.miniExpressHighlighted) return;
|
|
122
|
+
const raw = p.textContent || p.innerText || '';
|
|
123
|
+
p.innerHTML = '<code>' + escapeHtml(raw) + '</code>';
|
|
124
|
+
p.dataset.miniExpressHighlighted = '1';
|
|
125
|
+
});
|
|
126
|
+
} catch (e) { }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Strip common leading whitespace from every `<pre>` block so that indented
|
|
131
|
+
* source pasted into the HTML renders flush-left.
|
|
132
|
+
*/
|
|
133
|
+
function dedentAllPre()
|
|
134
|
+
{
|
|
135
|
+
document.querySelectorAll('pre').forEach(pre =>
|
|
136
|
+
{
|
|
137
|
+
try
|
|
138
|
+
{
|
|
139
|
+
if (pre.dataset.miniExpressDedented) return;
|
|
140
|
+
const txt = pre.textContent || '';
|
|
141
|
+
const lines = txt.replace(/\r/g, '').split('\n');
|
|
142
|
+
|
|
143
|
+
while (lines.length && lines[0].trim() === '') lines.shift();
|
|
144
|
+
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
|
|
145
|
+
if (!lines.length) { pre.dataset.miniExpressDedented = '1'; return; }
|
|
146
|
+
|
|
147
|
+
const indents = lines.filter(l => l.trim()).map(l =>
|
|
148
|
+
{
|
|
149
|
+
const match = l.match(/^[\t ]*/)[0] || '';
|
|
150
|
+
return match.replace(/\t/g, ' ').length;
|
|
151
|
+
});
|
|
152
|
+
const minIndent = indents.length ? Math.min(...indents) : 0;
|
|
153
|
+
|
|
154
|
+
if (minIndent > 0)
|
|
155
|
+
{
|
|
156
|
+
const dedented = lines.map(l =>
|
|
157
|
+
{
|
|
158
|
+
const s = l.replace(/\t/g, ' ');
|
|
159
|
+
return s.slice(Math.min(minIndent, s.length));
|
|
160
|
+
}).join('\n');
|
|
161
|
+
pre.textContent = dedented;
|
|
162
|
+
}
|
|
163
|
+
pre.dataset.miniExpressDedented = '1';
|
|
164
|
+
} catch (e) { }
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* playground.js
|
|
3
|
+
* Echo playground forms — JSON, URL-encoded, and plain-text body parsers.
|
|
4
|
+
*
|
|
5
|
+
* Depends on: helpers.js (provides $, on, escapeHtml, showJsonResult,
|
|
6
|
+
* highlightAllPre)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wire the three echo playground forms so submissions hit the server and
|
|
11
|
+
* display the response. Called once from the DOMContentLoaded handler in
|
|
12
|
+
* app.js.
|
|
13
|
+
*/
|
|
14
|
+
function initPlayground()
|
|
15
|
+
{
|
|
16
|
+
/* JSON echo */
|
|
17
|
+
on($('#jsonPlay'), 'submit', async (e) =>
|
|
18
|
+
{
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
const raw = e.target.json.value || '';
|
|
21
|
+
const playResult = $('#playResult');
|
|
22
|
+
|
|
23
|
+
try { JSON.parse(raw); }
|
|
24
|
+
catch (err)
|
|
25
|
+
{
|
|
26
|
+
playResult.innerHTML = `<pre class="code"><code>${escapeHtml('Invalid JSON: ' + err.message)}</code></pre>`;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const r = await fetch('/echo', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: raw,
|
|
34
|
+
});
|
|
35
|
+
const j = await r.json();
|
|
36
|
+
showJsonResult(playResult, j);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/* URL-encoded echo */
|
|
40
|
+
on($('#urlPlay'), 'submit', async (e) =>
|
|
41
|
+
{
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
const body = e.target.url.value || '';
|
|
44
|
+
const playResult = $('#playResult');
|
|
45
|
+
|
|
46
|
+
const r = await fetch('/echo-urlencoded', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
49
|
+
body,
|
|
50
|
+
});
|
|
51
|
+
const j = await r.json();
|
|
52
|
+
showJsonResult(playResult, j);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/* Plain-text echo */
|
|
56
|
+
on($('#textPlay'), 'submit', async (e) =>
|
|
57
|
+
{
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
const txt = e.target.txt.value || '';
|
|
60
|
+
const playResult = $('#playResult');
|
|
61
|
+
|
|
62
|
+
const r = await fetch('/echo-text', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
65
|
+
body: txt,
|
|
66
|
+
});
|
|
67
|
+
const text = await r.text();
|
|
68
|
+
playResult.innerHTML = `<pre class="code"><code>${escapeHtml(text)}</code></pre>`;
|
|
69
|
+
try { highlightAllPre(); } catch (e) { }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* proxy.js
|
|
3
|
+
* Proxy playground — fetches an external URL through the server and renders
|
|
4
|
+
* the response based on its content-type (JSON, image, audio/video, text,
|
|
5
|
+
* or a binary download link).
|
|
6
|
+
*
|
|
7
|
+
* Depends on: helpers.js (provides $, on, escapeHtml, showJsonResult,
|
|
8
|
+
* highlightAllPre)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wire the proxy form. Called once from the DOMContentLoaded handler in
|
|
13
|
+
* app.js.
|
|
14
|
+
*/
|
|
15
|
+
function initProxy()
|
|
16
|
+
{
|
|
17
|
+
const proxyForm = $('#proxyForm');
|
|
18
|
+
const proxyResult = $('#proxyResult');
|
|
19
|
+
if (!proxyForm) return;
|
|
20
|
+
|
|
21
|
+
on(proxyForm, 'submit', async (e) =>
|
|
22
|
+
{
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
const urlInput = $('#proxyUrl');
|
|
25
|
+
const url = urlInput && urlInput.value ? urlInput.value.trim() : '';
|
|
26
|
+
|
|
27
|
+
if (!url)
|
|
28
|
+
{
|
|
29
|
+
proxyResult.innerHTML = `<pre class="code"><code>${escapeHtml('Please enter a URL')}</code></pre>`;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try
|
|
34
|
+
{
|
|
35
|
+
proxyResult.innerHTML = `<div class="muted">Fetching ${escapeHtml(url)}…</div>`;
|
|
36
|
+
const r = await fetch('/proxy?url=' + encodeURIComponent(url));
|
|
37
|
+
|
|
38
|
+
/* Upstream error */
|
|
39
|
+
if (r.status >= 400)
|
|
40
|
+
{
|
|
41
|
+
const j = await r.json();
|
|
42
|
+
showJsonResult(proxyResult, j);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ct = (r.headers && typeof r.headers.get === 'function')
|
|
47
|
+
? (r.headers.get('content-type') || '')
|
|
48
|
+
: '';
|
|
49
|
+
|
|
50
|
+
/* JSON */
|
|
51
|
+
if (ct.includes('application/json') || ct.includes('application/problem+json'))
|
|
52
|
+
{
|
|
53
|
+
showJsonResult(proxyResult, await r.json());
|
|
54
|
+
}
|
|
55
|
+
/* Image */
|
|
56
|
+
else if (ct.startsWith('image/'))
|
|
57
|
+
{
|
|
58
|
+
const blob = new Blob([await r.arrayBuffer()], { type: ct });
|
|
59
|
+
const src = URL.createObjectURL(blob);
|
|
60
|
+
proxyResult.innerHTML =
|
|
61
|
+
`<div style="display:flex;align-items:center;gap:12px">` +
|
|
62
|
+
`<img src="${src}" style="max-width:240px;max-height:240px;border-radius:8px"/>` +
|
|
63
|
+
`<div class="mono" style="max-width:480px;overflow:auto">${escapeHtml('Image received: ' + ct)}</div></div>`;
|
|
64
|
+
}
|
|
65
|
+
/* Audio / Video / Octet-stream */
|
|
66
|
+
else if (ct.startsWith('audio/') || ct.startsWith('video/') || ct === 'application/octet-stream' || ct.includes('wav') || ct.includes('wave'))
|
|
67
|
+
{
|
|
68
|
+
const proxiedUrl = '/proxy?url=' + encodeURIComponent(url);
|
|
69
|
+
let mediaHtml = '';
|
|
70
|
+
if (ct.startsWith('audio/'))
|
|
71
|
+
mediaHtml = `<audio controls src="${proxiedUrl}" style="max-width:480px;display:block;margin-bottom:8px"></audio>`;
|
|
72
|
+
else if (ct.startsWith('video/'))
|
|
73
|
+
mediaHtml = `<video controls src="${proxiedUrl}" style="max-width:480px;display:block;margin-bottom:8px"></video>`;
|
|
74
|
+
proxyResult.innerHTML = `<div>${mediaHtml}<div class="mono">${escapeHtml('Streaming: ' + ct)}</div></div>`;
|
|
75
|
+
}
|
|
76
|
+
/* Text */
|
|
77
|
+
else if (ct.startsWith('text/') || ct === '')
|
|
78
|
+
{
|
|
79
|
+
const txt = await r.text();
|
|
80
|
+
proxyResult.innerHTML = `<pre class="code"><code>${escapeHtml(txt)}</code></pre>`;
|
|
81
|
+
try { highlightAllPre(); } catch (e) { }
|
|
82
|
+
}
|
|
83
|
+
/* Binary fallback — offer download */
|
|
84
|
+
else
|
|
85
|
+
{
|
|
86
|
+
const ab = await r.arrayBuffer();
|
|
87
|
+
const blob = new Blob([ab], { type: ct || 'application/octet-stream' });
|
|
88
|
+
const href = URL.createObjectURL(blob);
|
|
89
|
+
proxyResult.innerHTML =
|
|
90
|
+
`<div class="mono">${escapeHtml('Binary response: ' + ct + ' — ' + ab.byteLength + ' bytes')}</div>` +
|
|
91
|
+
`<div style="margin-top:8px"><a href="${href}" download="proxied-file">Download file</a></div>`;
|
|
92
|
+
}
|
|
93
|
+
} catch (err)
|
|
94
|
+
{
|
|
95
|
+
proxyResult.innerHTML = `<pre class="code"><code>${escapeHtml(String(err))}</code></pre>`;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|