@deckbuilderkit/editor 0.0.0-stage → 0.1.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 (56) hide show
  1. package/package.json +53 -4
  2. package/src/ArchiveView.tsx +111 -0
  3. package/src/CodeEditor.tsx +265 -0
  4. package/src/CodeView.tsx +67 -0
  5. package/src/DiffView.tsx +139 -0
  6. package/src/EnvView.tsx +42 -0
  7. package/src/FilePreview.tsx +108 -0
  8. package/src/FontView.tsx +48 -0
  9. package/src/HarView.tsx +70 -0
  10. package/src/HexView.tsx +28 -0
  11. package/src/HtmlPreview.tsx +23 -0
  12. package/src/MarkdownEditor.tsx +90 -0
  13. package/src/MarkdownToc.tsx +135 -0
  14. package/src/MarkdownView.tsx +84 -0
  15. package/src/MediaView.tsx +17 -0
  16. package/src/NotebookView.tsx +74 -0
  17. package/src/PdfView.tsx +289 -0
  18. package/src/RequestEditor.tsx +419 -0
  19. package/src/ResponseView.tsx +89 -0
  20. package/src/SqlGraph.tsx +322 -0
  21. package/src/SqlSteps.tsx +487 -0
  22. package/src/SqliteView.tsx +68 -0
  23. package/src/SvgView.tsx +231 -0
  24. package/src/TableView.tsx +85 -0
  25. package/src/VarSuggest.tsx +125 -0
  26. package/src/curl-file.ts +70 -0
  27. package/src/editorOptions.ts +83 -0
  28. package/src/editorOptionsContext.ts +4 -0
  29. package/src/fileKinds.ts +69 -0
  30. package/src/http-client.ts +9 -0
  31. package/src/http-import.ts +594 -0
  32. package/src/http.ts +251 -0
  33. package/src/import-warning.ts +8 -0
  34. package/src/index.ts +56 -0
  35. package/src/languages.ts +107 -0
  36. package/src/markdown.ts +123 -0
  37. package/src/markdownLive.ts +289 -0
  38. package/src/options.ts +3 -0
  39. package/src/pdf-url.d.ts +4 -0
  40. package/src/sql/format.ts +637 -0
  41. package/src/sql/formatOptions.ts +71 -0
  42. package/src/sql/lineage.ts +365 -0
  43. package/src/sql/lint.ts +951 -0
  44. package/src/sql/markerHighlight.ts +50 -0
  45. package/src/sql/markers.ts +138 -0
  46. package/src/sql/outline.ts +662 -0
  47. package/src/sql/statements.ts +83 -0
  48. package/src/sql/tokens.ts +156 -0
  49. package/src/sql/xmlQuery.ts +249 -0
  50. package/src/sqlProblems.ts +40 -0
  51. package/src/table.ts +89 -0
  52. package/src/text-save.ts +28 -0
  53. package/src/theme.ts +86 -0
  54. package/src/toc.ts +11 -0
  55. package/src/variables.ts +123 -0
  56. package/README.md +0 -3
@@ -0,0 +1,9 @@
1
+ import type { HttpRequestDraft, HttpRequestSpec, HttpResponseInfo, RestEnvironment } from '@deckbuilderkit/server-core/protocol';
2
+
3
+ /** The server calls the REST/curl views make; the app builds it from its api and passes it to them. */
4
+ export interface HttpClient {
5
+ /** Sends `request` after substituting `{{name}}` from `environment` (id; omitted = built-ins only); `draft` is the unresolved editor draft for the history. */
6
+ send: (request: HttpRequestSpec, secretRef: string | undefined, signal: AbortSignal, env?: { environment?: string; draft?: HttpRequestDraft }) => Promise<HttpResponseInfo>;
7
+ /** REST environments; secret variables come back with `value: ''` and `set`. */
8
+ environments: () => Promise<RestEnvironment[]>;
9
+ }
@@ -0,0 +1,594 @@
1
+ import { HTTP_METHODS, type HttpFormField, type HttpMethod, type HttpRequestDraft } from '@deckbuilderkit/server-core/protocol';
2
+ import { emptyDraft, parseAuthValue, timeoutFromSeconds } from './http';
3
+
4
+ /**
5
+ * A request read from a cURL command or an `.http` file. `warnings` are `<code>:<argument>`
6
+ * strings (`unknownFlag:--foo`, `dataFile:@body.json`, `unresolvedVar:host`, `bodyFile:x.json`,
7
+ * `badHeader:…`, `badMethod:…`, `noUrl:`) for the UI to translate. `auth` is the Authorization
8
+ * value taken out of the headers.
9
+ */
10
+ export interface ImportedRequest {
11
+ name?: string;
12
+ draft: HttpRequestDraft;
13
+ auth?: string;
14
+ warnings: string[];
15
+ }
16
+
17
+ const METHODS = HTTP_METHODS as readonly string[];
18
+ const isJsonType = (type: string) => /json/i.test(type);
19
+
20
+ function base64Utf8(text: string): string {
21
+ let binary = '';
22
+ for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
23
+ return btoa(binary);
24
+ }
25
+
26
+ function parsesAsJson(text: string): boolean {
27
+ try {
28
+ JSON.parse(text);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ interface FormBody {
36
+ lang: 'form' | 'multipart';
37
+ rows: HttpFormField[];
38
+ }
39
+
40
+ /**
41
+ * Builds the draft; the last Authorization header leaves the headers and becomes `auth`, its
42
+ * Basic/Bearer scheme the draft's `authKind`. With `form` the body is form rows; a multipart
43
+ * Content-Type header is dropped then, as its boundary no longer matches.
44
+ */
45
+ function buildRequest(method: HttpMethod, url: string, pairs: Array<[string, string]>, body: string, warnings: string[], form?: FormBody): ImportedRequest {
46
+ let auth: string | undefined;
47
+ let contentType = '';
48
+ const headers = [];
49
+ for (const [name, value] of pairs) {
50
+ const lower = name.toLowerCase();
51
+ if (lower === 'authorization') {
52
+ auth = value;
53
+ continue;
54
+ }
55
+ if (lower === 'content-type') {
56
+ contentType = value;
57
+ if (form?.lang === 'multipart') continue;
58
+ }
59
+ headers.push({ name, value, enabled: true });
60
+ }
61
+ const draft: HttpRequestDraft = form
62
+ ? { ...emptyDraft(), method, url, headers, body: '', bodyLang: form.lang, form: form.rows }
63
+ : {
64
+ ...emptyDraft(),
65
+ method,
66
+ url,
67
+ headers,
68
+ body,
69
+ bodyLang: body === '' || isJsonType(contentType) || parsesAsJson(body) ? 'json' : 'text',
70
+ };
71
+ const authKind = auth !== undefined ? parseAuthValue(auth).kind : undefined;
72
+ if (authKind) draft.authKind = authKind;
73
+ return { draft, ...(auth !== undefined ? { auth } : {}), warnings };
74
+ }
75
+
76
+ /** An `application/x-www-form-urlencoded` body as rows (`+` is a space); undefined when it does not decode. */
77
+ function urlencodedRows(body: string): HttpFormField[] | undefined {
78
+ const text = body.replace(/\n/g, '');
79
+ if (text === '') return undefined;
80
+ try {
81
+ return text
82
+ .split('&')
83
+ .filter((pair) => pair !== '')
84
+ .map((pair) => {
85
+ const eq = pair.indexOf('=');
86
+ const decode = (s: string) => decodeURIComponent(s.replace(/\+/g, ' '));
87
+ return eq < 0 ? { name: decode(pair), value: '' } : { name: decode(pair.slice(0, eq)), value: decode(pair.slice(eq + 1)) };
88
+ });
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * A `multipart/form-data` body as rows when every part is a plain text field (only a
96
+ * `Content-Disposition: form-data; name="…"` header, no filename, no `<` file include); undefined otherwise.
97
+ */
98
+ function multipartRows(body: string, contentType: string): HttpFormField[] | undefined {
99
+ const boundary = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(contentType);
100
+ const mark = boundary ? `--${boundary[1] ?? boundary[2]}` : undefined;
101
+ if (!mark) return undefined;
102
+ const chunks = body.replace(/\r\n/g, '\n').split(mark);
103
+ const end = chunks.findIndex((c, i) => i > 0 && c.startsWith('--'));
104
+ if (end < 0 || chunks[0].trim() !== '') return undefined;
105
+ const rows: HttpFormField[] = [];
106
+ for (const chunk of chunks.slice(1, end)) {
107
+ const part = chunk.replace(/^\n/, '').replace(/\n$/, '');
108
+ const split = part.indexOf('\n\n');
109
+ const head = split < 0 ? part : part.slice(0, split);
110
+ const value = split < 0 ? '' : part.slice(split + 2);
111
+ const lines = head.split('\n').filter((l) => l.trim() !== '');
112
+ const named = lines.length === 1 ? /^content-disposition:\s*form-data;\s*name="([^"]*)"\s*$/i.exec(lines[0].trim()) : null;
113
+ if (!named || /^<\s/.test(value)) return undefined;
114
+ rows.push({ name: named[1], value });
115
+ }
116
+ return rows.length ? rows : undefined;
117
+ }
118
+
119
+ /** The form rows an `.http` body maps to by its Content-Type, if it is a simple form. */
120
+ function httpFormBody(pairs: Array<[string, string]>, body: string): FormBody | undefined {
121
+ const type = [...pairs].reverse().find(([n]) => n.toLowerCase() === 'content-type')?.[1] ?? '';
122
+ if (/^application\/x-www-form-urlencoded\b/i.test(type)) {
123
+ const rows = urlencodedRows(body);
124
+ return rows && { lang: 'form', rows };
125
+ }
126
+ if (/^multipart\/form-data\b/i.test(type)) {
127
+ const rows = multipartRows(body, type);
128
+ return rows && { lang: 'multipart', rows };
129
+ }
130
+ return undefined;
131
+ }
132
+
133
+ // --- cURL ---
134
+
135
+ const ANSI_ESCAPES: Record<string, string> = { n: '\n', r: '\r', t: '\t', a: '\x07', b: '\b', e: '\x1b', f: '\f', v: '\v', '\\': '\\', "'": "'", '"': '"', '?': '?' };
136
+
137
+ /** Reads a `$'…'` string whose opening quote is at `start`; returns the text and the index of the closing quote. */
138
+ function readAnsiC(text: string, start: number): { value: string; end: number } {
139
+ let value = '';
140
+ let i = start + 1;
141
+ for (; i < text.length && text[i] !== "'"; i++) {
142
+ if (text[i] !== '\\') {
143
+ value += text[i];
144
+ continue;
145
+ }
146
+ const next = text[i + 1] ?? '';
147
+ const hex = /^(?:x([0-9a-fA-F]{1,2})|u([0-9a-fA-F]{1,4})|U([0-9a-fA-F]{1,8}))/.exec(text.slice(i + 1, i + 10));
148
+ const octal = /^[0-7]{1,3}/.exec(text.slice(i + 1, i + 4));
149
+ if (next in ANSI_ESCAPES) {
150
+ value += ANSI_ESCAPES[next];
151
+ i += 1;
152
+ } else if (hex) {
153
+ value += String.fromCodePoint(parseInt(hex[1] ?? hex[2] ?? hex[3], 16));
154
+ i += hex[0].length;
155
+ } else if (octal) {
156
+ value += String.fromCharCode(parseInt(octal[0], 8));
157
+ i += octal[0].length;
158
+ } else {
159
+ value += `\\${next}`;
160
+ i += 1;
161
+ }
162
+ }
163
+ return { value, end: i };
164
+ }
165
+
166
+ function tokenizeBash(text: string): string[] {
167
+ const tokens: string[] = [];
168
+ let cur = '';
169
+ let has = false;
170
+ const flush = () => {
171
+ if (has) tokens.push(cur);
172
+ cur = '';
173
+ has = false;
174
+ };
175
+ for (let i = 0; i < text.length; i++) {
176
+ const c = text[i];
177
+ if (/\s/.test(c)) {
178
+ flush();
179
+ } else if (c === '\\') {
180
+ if (text[i + 1] === '\n') {
181
+ i += 1;
182
+ } else if (i + 1 < text.length) {
183
+ cur += text[i + 1];
184
+ has = true;
185
+ i += 1;
186
+ }
187
+ } else if (c === "'") {
188
+ const end = text.indexOf("'", i + 1);
189
+ const stop = end < 0 ? text.length : end;
190
+ cur += text.slice(i + 1, stop);
191
+ has = true;
192
+ i = stop;
193
+ } else if (c === '$' && text[i + 1] === "'") {
194
+ const { value, end } = readAnsiC(text, i + 1);
195
+ cur += value;
196
+ has = true;
197
+ i = end;
198
+ } else if (c === '"') {
199
+ has = true;
200
+ i += 1;
201
+ for (; i < text.length && text[i] !== '"'; i++) {
202
+ if (text[i] === '\\' && i + 1 < text.length && '"\\$`\n'.includes(text[i + 1])) {
203
+ if (text[i + 1] !== '\n') cur += text[i + 1];
204
+ i += 1;
205
+ } else {
206
+ cur += text[i];
207
+ }
208
+ }
209
+ } else {
210
+ cur += c;
211
+ has = true;
212
+ }
213
+ }
214
+ flush();
215
+ return tokens;
216
+ }
217
+
218
+ /** cmd.exe: `^` escapes the next character; `^` before a line break continues the command (two breaks = a newline in the text). */
219
+ function unescapeCmd(text: string): string {
220
+ let out = '';
221
+ for (let i = 0; i < text.length; i++) {
222
+ if (text[i] !== '^') {
223
+ out += text[i];
224
+ continue;
225
+ }
226
+ const next = text[i + 1];
227
+ if (next === undefined) break;
228
+ if (next === '\n') {
229
+ if (text[i + 2] === '\n') {
230
+ out += '\n';
231
+ i += 2;
232
+ } else {
233
+ i += 1;
234
+ }
235
+ } else {
236
+ out += next;
237
+ i += 1;
238
+ }
239
+ }
240
+ return out;
241
+ }
242
+
243
+ function tokenizeCmd(text: string): string[] {
244
+ const src = unescapeCmd(text);
245
+ const tokens: string[] = [];
246
+ let cur = '';
247
+ let has = false;
248
+ let quoted = false;
249
+ for (let i = 0; i < src.length; i++) {
250
+ const c = src[i];
251
+ if (quoted) {
252
+ if (c === '\\' && src[i + 1] === '"') {
253
+ cur += '"';
254
+ i += 1;
255
+ } else if (c === '"' && src[i + 1] === '"') {
256
+ cur += '"';
257
+ i += 1;
258
+ } else if (c === '"') {
259
+ quoted = false;
260
+ } else {
261
+ cur += c;
262
+ }
263
+ } else if (c === '"') {
264
+ quoted = true;
265
+ has = true;
266
+ } else if (/\s/.test(c)) {
267
+ if (has) tokens.push(cur);
268
+ cur = '';
269
+ has = false;
270
+ } else {
271
+ cur += c;
272
+ has = true;
273
+ }
274
+ }
275
+ if (has) tokens.push(cur);
276
+ return tokens;
277
+ }
278
+
279
+ /** Flags that take a value, by long name → the short letter they share. */
280
+ const LONG_VALUE: Record<string, string> = {
281
+ request: 'X',
282
+ header: 'H',
283
+ data: 'd',
284
+ 'data-raw': 'd',
285
+ 'data-binary': 'd',
286
+ 'data-ascii': 'd',
287
+ 'data-urlencode': 'urlencode',
288
+ json: 'json',
289
+ user: 'u',
290
+ cookie: 'b',
291
+ 'user-agent': 'A',
292
+ referer: 'e',
293
+ 'max-time': 'm',
294
+ url: 'url',
295
+ form: 'F',
296
+ 'form-string': 'formString',
297
+ };
298
+ const LONG_BOOL: Record<string, string> = { get: 'G', insecure: 'k', location: 'L', head: 'I' };
299
+ const IGNORED_LONG = new Set(['compressed', 'silent', 'show-error', 'verbose', 'include', 'fail', 'globoff', 'http1.1', 'http2', 'no-progress-meter']);
300
+ const VALUE_SHORT = 'XHdubAemF';
301
+ const BOOL_SHORT = 'GkLI';
302
+ const IGNORED_SHORT = 'sSvifg';
303
+
304
+ /** Percent-encodes like curl's --data-urlencode (everything but the RFC 3986 unreserved characters). */
305
+ const urlencode = (text: string) => encodeURIComponent(text).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
306
+
307
+ function urlencodedData(value: string, warnings: string[]): string {
308
+ const at = value.indexOf('@');
309
+ const eq = value.indexOf('=');
310
+ if (eq < 0 && at >= 0) {
311
+ warnings.push(`dataFile:${value.slice(at)}`);
312
+ return value;
313
+ }
314
+ if (eq < 0) return urlencode(value);
315
+ const name = value.slice(0, eq);
316
+ return name ? `${name}=${urlencode(value.slice(eq + 1))}` : urlencode(value.slice(eq + 1));
317
+ }
318
+
319
+ /** Reads a cURL command as the browser devtools copy it, in its bash or cmd form. Throws `Error('notCurl')` for other text. */
320
+ export function parseCurl(text: string): ImportedRequest {
321
+ const src = text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n').trim();
322
+ const cmd = /\^\n/.test(src) || /\^"/.test(src);
323
+ const tokens = cmd ? tokenizeCmd(src) : tokenizeBash(src);
324
+ if (tokens[0] === '$') tokens.shift();
325
+ const first = tokens.shift();
326
+ if (!first || !/^curl(\.exe)?$/i.test(first)) throw new Error('notCurl');
327
+
328
+ const warnings: string[] = [];
329
+ const headers: Array<[string, string]> = [];
330
+ const data: string[] = [];
331
+ /** `--data-urlencode name=value` rows; undefined once other data (or a nameless/file value) makes the body raw. */
332
+ let formRows: HttpFormField[] | undefined = [];
333
+ const multipart: HttpFormField[] = [];
334
+ let url: string | undefined;
335
+ let method: string | undefined;
336
+ let asGet = false;
337
+ let head = false;
338
+ let insecure: boolean | undefined;
339
+ let followRedirects: boolean | undefined;
340
+ let timeoutMs: number | undefined;
341
+
342
+ const addHeader = (line: string) => {
343
+ const colon = line.indexOf(':');
344
+ if (colon > 0) headers.push([line.slice(0, colon).trim(), line.slice(colon + 1).trim()]);
345
+ else if (colon < 0 && line.endsWith(';') && line.length > 1) headers.push([line.slice(0, -1).trim(), '']);
346
+ else if (colon < 0) warnings.push(`badHeader:${line}`);
347
+ };
348
+ const addData = (value: string) => {
349
+ if (value.startsWith('@')) warnings.push(`dataFile:${value}`);
350
+ data.push(value);
351
+ formRows = undefined;
352
+ };
353
+ const addUrlencoded = (value: string) => {
354
+ data.push(urlencodedData(value, warnings));
355
+ const eq = value.indexOf('=');
356
+ const at = value.indexOf('@');
357
+ if (eq > 0 && (at < 0 || at > eq)) formRows?.push({ name: value.slice(0, eq), value: value.slice(eq + 1) });
358
+ else formRows = undefined;
359
+ };
360
+ /** `-F name=value`; file parts (`@file`, `<file`) are skipped with a `bodyFile` warning. */
361
+ const addPart = (value: string, literal: boolean) => {
362
+ const eq = value.indexOf('=');
363
+ if (eq <= 0) {
364
+ warnings.push(`extraArgument:-F ${value}`);
365
+ return;
366
+ }
367
+ const content = value.slice(eq + 1);
368
+ if (!literal && /^[@<]/.test(content)) warnings.push(`bodyFile:${content.slice(1)}`);
369
+ else multipart.push({ name: value.slice(0, eq), value: content });
370
+ };
371
+ const withValue = (key: string, value: string) => {
372
+ switch (key) {
373
+ case 'X':
374
+ method = value.toUpperCase();
375
+ break;
376
+ case 'H':
377
+ addHeader(value);
378
+ break;
379
+ case 'd':
380
+ addData(value);
381
+ break;
382
+ case 'urlencode':
383
+ addUrlencoded(value);
384
+ break;
385
+ case 'F':
386
+ addPart(value, false);
387
+ break;
388
+ case 'formString':
389
+ addPart(value, true);
390
+ break;
391
+ case 'json':
392
+ addData(value);
393
+ if (!headers.some(([n]) => n.toLowerCase() === 'content-type')) headers.push(['Content-Type', 'application/json']);
394
+ if (!headers.some(([n]) => n.toLowerCase() === 'accept')) headers.push(['Accept', 'application/json']);
395
+ break;
396
+ case 'u':
397
+ headers.push(['Authorization', `Basic ${base64Utf8(value)}`]);
398
+ break;
399
+ case 'b':
400
+ if (value.includes('=')) headers.push(['Cookie', value]);
401
+ else warnings.push(`cookieFile:${value}`);
402
+ break;
403
+ case 'A':
404
+ headers.push(['User-Agent', value]);
405
+ break;
406
+ case 'e':
407
+ headers.push(['Referer', value]);
408
+ break;
409
+ case 'm':
410
+ timeoutMs = timeoutFromSeconds(value);
411
+ break;
412
+ case 'url':
413
+ url ??= value;
414
+ break;
415
+ }
416
+ };
417
+ const flag = (key: string) => {
418
+ if (key === 'G') asGet = true;
419
+ else if (key === 'k') insecure = true;
420
+ else if (key === 'L') followRedirects = true;
421
+ else if (key === 'I') head = true;
422
+ };
423
+
424
+ for (let i = 0; i < tokens.length; i++) {
425
+ const token = tokens[i];
426
+ if (token.startsWith('--') && token.length > 2) {
427
+ const eq = token.indexOf('=');
428
+ const name = eq > 0 ? token.slice(2, eq) : token.slice(2);
429
+ if (name in LONG_VALUE) {
430
+ const value = eq > 0 ? token.slice(eq + 1) : tokens[++i];
431
+ if (value === undefined) warnings.push(`missingValue:--${name}`);
432
+ else withValue(LONG_VALUE[name], value);
433
+ } else if (name in LONG_BOOL) {
434
+ flag(LONG_BOOL[name]);
435
+ } else if (!IGNORED_LONG.has(name)) {
436
+ warnings.push(`unknownFlag:--${name}`);
437
+ }
438
+ } else if (token.startsWith('-') && token.length > 1) {
439
+ for (let j = 1; j < token.length; j++) {
440
+ const ch = token[j];
441
+ if (VALUE_SHORT.includes(ch)) {
442
+ const value = token.slice(j + 1) || tokens[++i];
443
+ if (value === undefined) warnings.push(`missingValue:-${ch}`);
444
+ else withValue(ch, value);
445
+ break;
446
+ }
447
+ if (BOOL_SHORT.includes(ch)) flag(ch);
448
+ else if (!IGNORED_SHORT.includes(ch)) warnings.push(`unknownFlag:-${ch}`);
449
+ }
450
+ } else if (url === undefined) {
451
+ url = token;
452
+ } else {
453
+ warnings.push(`extraArgument:${token}`);
454
+ }
455
+ }
456
+
457
+ let target = url ?? '';
458
+ if (!target) warnings.push('noUrl:');
459
+ let body = data.join('&');
460
+ if (asGet && body) {
461
+ target += `${target.includes('?') ? '&' : '?'}${body}`;
462
+ body = '';
463
+ }
464
+ const form: FormBody | undefined =
465
+ multipart.length && !data.length
466
+ ? { lang: 'multipart', rows: multipart }
467
+ : !asGet && formRows?.length && data.length === formRows.length
468
+ ? { lang: 'form', rows: formRows }
469
+ : undefined;
470
+ let verb: HttpMethod = body || form ? 'POST' : head ? 'HEAD' : 'GET';
471
+ if (method !== undefined) {
472
+ if (METHODS.includes(method)) verb = method as HttpMethod;
473
+ else warnings.push(`badMethod:${method}`);
474
+ }
475
+ const result = buildRequest(verb, target, headers, body, warnings, form);
476
+ if (timeoutMs !== undefined) result.draft.timeoutMs = timeoutMs;
477
+ if (insecure !== undefined) result.draft.insecure = insecure;
478
+ if (followRedirects !== undefined) result.draft.followRedirects = followRedirects;
479
+ return result;
480
+ }
481
+
482
+ // --- .http files ---
483
+
484
+ interface RawRequest {
485
+ name?: string;
486
+ method: HttpMethod;
487
+ url: string;
488
+ headers: Array<[string, string]>;
489
+ body: string;
490
+ warnings: string[];
491
+ }
492
+
493
+ const isComment = (trimmed: string) => trimmed.startsWith('#') || trimmed.startsWith('//');
494
+ const VARIABLE = /^@([\w.-]+)\s*=\s*(.*)$/;
495
+ const MAX_VAR_DEPTH = 5;
496
+
497
+ /**
498
+ * Reads a VS Code REST Client / IntelliJ `.http` file: requests separated by `###` (the text after
499
+ * it names the next request, as does `# @name x`), file variables `@var = value` used as `{{var}}`,
500
+ * a request line `[METHOD] URL [HTTP/1.1]` with `?`/`&` continuation lines, headers up to a blank
501
+ * line, then the body. `#` and `//` lines are comments everywhere.
502
+ */
503
+ export function parseHttpFile(text: string): ImportedRequest[] {
504
+ const lines = text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n').split('\n');
505
+ const blocks: Array<{ title: string; lines: string[] }> = [{ title: '', lines: [] }];
506
+ for (const line of lines) {
507
+ const separator = /^#{3,}(.*)$/.exec(line);
508
+ if (separator) blocks.push({ title: separator[1].trim(), lines: [] });
509
+ else blocks[blocks.length - 1].lines.push(line);
510
+ }
511
+
512
+ const variables = new Map<string, string>();
513
+ const raws: RawRequest[] = [];
514
+ for (const block of blocks) {
515
+ let name: string | undefined;
516
+ let request: RawRequest | undefined;
517
+ let phase: 'start' | 'headers' | 'body' = 'start';
518
+ const body: string[] = [];
519
+ for (const line of block.lines) {
520
+ const trimmed = line.trim();
521
+ if (isComment(trimmed)) {
522
+ const named = /^(?:#|\/\/)\s*@name\s+(.+)$/.exec(trimmed);
523
+ if (named) name = named[1].trim();
524
+ continue;
525
+ }
526
+ if (phase === 'body') {
527
+ body.push(line);
528
+ } else if (phase === 'start') {
529
+ if (trimmed === '') continue;
530
+ const variable = VARIABLE.exec(trimmed);
531
+ if (variable) {
532
+ variables.set(variable[1], variable[2].trim());
533
+ continue;
534
+ }
535
+ const parts = /^([A-Za-z]+)\s+(.+)$/.exec(trimmed);
536
+ const warnings: string[] = [];
537
+ let method: HttpMethod = 'GET';
538
+ let rest = trimmed;
539
+ if (parts && METHODS.includes(parts[1].toUpperCase())) {
540
+ method = parts[1].toUpperCase() as HttpMethod;
541
+ rest = parts[2];
542
+ } else if (parts && /^[A-Z]+$/.test(parts[1])) {
543
+ warnings.push(`badMethod:${parts[1]}`);
544
+ rest = parts[2];
545
+ }
546
+ request = { method, url: rest.replace(/\s+HTTP\/\d(?:\.\d)?$/i, '').trim(), headers: [], body: '', warnings };
547
+ phase = 'headers';
548
+ } else if (trimmed === '') {
549
+ phase = 'body';
550
+ } else if (request && request.headers.length === 0 && (trimmed.startsWith('?') || trimmed.startsWith('&'))) {
551
+ request.url += trimmed;
552
+ } else if (request) {
553
+ const colon = trimmed.indexOf(':');
554
+ if (colon > 0) request.headers.push([trimmed.slice(0, colon).trim(), trimmed.slice(colon + 1).trim()]);
555
+ else request.warnings.push(`badHeader:${trimmed}`);
556
+ }
557
+ }
558
+ if (!request) continue;
559
+ while (body.length && body[0].trim() === '') body.shift();
560
+ while (body.length && body[body.length - 1].trim() === '') body.pop();
561
+ const file = body.length === 1 ? /^<@?\s+(.+)$/.exec(body[0].trim()) : null;
562
+ if (file) request.warnings.push(`bodyFile:${file[1].trim()}`);
563
+ request.body = file ? '' : body.join('\n');
564
+ request.name = name ?? (block.title || undefined);
565
+ raws.push(request);
566
+ }
567
+
568
+ const substitute = (value: string, missing: Set<string>, depth = 0): string =>
569
+ value.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (all, key: string) => {
570
+ const known = variables.get(key);
571
+ if (known !== undefined && depth < MAX_VAR_DEPTH) return substitute(known, missing, depth + 1);
572
+ missing.add(key);
573
+ return all;
574
+ });
575
+
576
+ return raws.map((raw) => {
577
+ const missing = new Set<string>();
578
+ const sub = (value: string) => substitute(value, missing);
579
+ const headers = raw.headers.map(([n, v]): [string, string] => [sub(n), sub(v)]);
580
+ const body = sub(raw.body);
581
+ const result = buildRequest(raw.method, sub(raw.url), headers, body, raw.warnings, httpFormBody(headers, body));
582
+ if (raw.name !== undefined) result.name = sub(raw.name);
583
+ if (result.auth !== undefined) result.auth = sub(result.auth);
584
+ for (const key of missing) result.warnings.push(`unresolvedVar:${key}`);
585
+ return result;
586
+ });
587
+ }
588
+
589
+ const CURL_START = /^(?:\$\s+)?curl(?:\.exe)?(?:\s|$)/i;
590
+
591
+ /** Pasted text → requests: a cURL command gives one, anything else is read as `.http` content. */
592
+ export function parseImport(text: string): ImportedRequest[] {
593
+ return CURL_START.test(text.trim()) ? [parseCurl(text)] : parseHttpFile(text);
594
+ }