@executablemd/testing 0.3.1 → 0.4.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.
Files changed (94) hide show
  1. package/esm/{testing/npm/src/src → src}/assertions.js +15 -24
  2. package/esm/{testing/npm/src/src → src}/handlers.js +140 -10
  3. package/esm/{testing/npm/src/src → src}/vocabulary.js +3 -0
  4. package/package.json +3 -3
  5. package/types/src/assertions.d.ts +11 -1
  6. package/types/src/handlers.d.ts +1 -0
  7. package/esm/core/mod.js +0 -55
  8. package/esm/core/src/api.js +0 -31
  9. package/esm/core/src/collect.js +0 -24
  10. package/esm/core/src/component-api.js +0 -47
  11. package/esm/core/src/content-context.js +0 -23
  12. package/esm/core/src/deno-compiler.js +0 -51
  13. package/esm/core/src/errors.js +0 -13
  14. package/esm/core/src/eval-context.js +0 -13
  15. package/esm/core/src/eval-handler.js +0 -79
  16. package/esm/core/src/eval-interpolate.js +0 -63
  17. package/esm/core/src/eval-transform.js +0 -378
  18. package/esm/core/src/execute.js +0 -378
  19. package/esm/core/src/expand.js +0 -988
  20. package/esm/core/src/frontmatter.js +0 -98
  21. package/esm/core/src/heal.js +0 -29
  22. package/esm/core/src/interpolate.js +0 -39
  23. package/esm/core/src/modifiers/daemon.js +0 -60
  24. package/esm/core/src/modifiers/persist.js +0 -35
  25. package/esm/core/src/modifiers/timeout.js +0 -55
  26. package/esm/core/src/modifiers.js +0 -113
  27. package/esm/core/src/output/normalize.js +0 -33
  28. package/esm/core/src/output/terminal.js +0 -20
  29. package/esm/core/src/render.js +0 -30
  30. package/esm/core/src/replay-stream.js +0 -58
  31. package/esm/core/src/sample-api.js +0 -45
  32. package/esm/core/src/scanner.js +0 -632
  33. package/esm/core/src/temp-file-compiler.js +0 -68
  34. package/esm/core/src/types.js +0 -7
  35. package/esm/core/src/validate.js +0 -82
  36. package/esm/durable-streams/combinators.js +0 -227
  37. package/esm/durable-streams/context.js +0 -13
  38. package/esm/durable-streams/divergence.js +0 -56
  39. package/esm/durable-streams/each.js +0 -193
  40. package/esm/durable-streams/effect.js +0 -273
  41. package/esm/durable-streams/ephemeral.js +0 -80
  42. package/esm/durable-streams/errors.js +0 -92
  43. package/esm/durable-streams/http-stream.js +0 -207
  44. package/esm/durable-streams/mod.js +0 -34
  45. package/esm/durable-streams/operations.js +0 -88
  46. package/esm/durable-streams/replay-guard.js +0 -87
  47. package/esm/durable-streams/replay-index.js +0 -132
  48. package/esm/durable-streams/run.js +0 -137
  49. package/esm/durable-streams/serialize.js +0 -65
  50. package/esm/durable-streams/stream.js +0 -56
  51. package/esm/durable-streams/types.js +0 -10
  52. package/esm/runtime/apis.js +0 -237
  53. package/esm/runtime/find-free-port.js +0 -33
  54. package/esm/runtime/mod.js +0 -20
  55. /package/esm/{testing/npm/src/_dnt.polyfills.js → _dnt.polyfills.js} +0 -0
  56. /package/esm/{testing/npm/src/_dnt.shims.js → _dnt.shims.js} +0 -0
  57. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/almost_equals.js +0 -0
  58. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/array_includes.js +0 -0
  59. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assert.js +0 -0
  60. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assertion_error.js +0 -0
  61. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equal.js +0 -0
  62. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equals.js +0 -0
  63. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/exists.js +0 -0
  64. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/fail.js +0 -0
  65. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/false.js +0 -0
  66. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater.js +0 -0
  67. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater_or_equal.js +0 -0
  68. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/instance_of.js +0 -0
  69. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/is_error.js +0 -0
  70. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less.js +0 -0
  71. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less_or_equal.js +0 -0
  72. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/match.js +0 -0
  73. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/mod.js +0 -0
  74. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_equals.js +0 -0
  75. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_instance_of.js +0 -0
  76. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_match.js +0 -0
  77. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_strict_equals.js +0 -0
  78. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/object_match.js +0 -0
  79. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/rejects.js +0 -0
  80. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/strict_equals.js +0 -0
  81. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/string_includes.js +0 -0
  82. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/throws.js +0 -0
  83. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unimplemented.js +0 -0
  84. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unreachable.js +0 -0
  85. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/build_message.js +0 -0
  86. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff.js +0 -0
  87. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff_str.js +0 -0
  88. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/format.js +0 -0
  89. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/styles.js +0 -0
  90. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/types.js +0 -0
  91. /package/esm/{testing/npm/src/mod.js → mod.js} +0 -0
  92. /package/esm/{testing/npm/src/src → src}/journal.js +0 -0
  93. /package/esm/{testing/npm/src/src → src}/test-api.js +0 -0
  94. /package/esm/{testing/npm/src/src → src}/use-testing.js +0 -0
@@ -1,632 +0,0 @@
1
- /**
2
- * Boundary scanner for Executable MDX (spec §2.2, §3.5).
3
- *
4
- * Parses raw markdown text into a flat sequence of Segments.
5
- * Identifies two kinds of execution boundaries:
6
- * 1. Component invocations — `<PascalCase ...>` tags
7
- * 2. Executable code blocks — fenced blocks with `exec` in the info string
8
- *
9
- * Everything else is passive text.
10
- */
11
- // ---------------------------------------------------------------------------
12
- // Info string parsing (spec §3.5)
13
- // ---------------------------------------------------------------------------
14
- export function parseInfoString(infoString) {
15
- const tokens = infoString.trim().split(/\s+/);
16
- const language = tokens[0] ?? "";
17
- const modifiers = [];
18
- for (let i = 1; i < tokens.length; i++) {
19
- const token = tokens[i];
20
- // Bracket params: sample[model=phi3-mini]
21
- const bracketIdx = token.indexOf("[");
22
- if (bracketIdx >= 0) {
23
- const closeBracket = token.indexOf("]", bracketIdx);
24
- if (closeBracket >= 0) {
25
- modifiers.push({
26
- name: token.slice(0, bracketIdx),
27
- params: token.slice(bracketIdx + 1, closeBracket),
28
- });
29
- continue;
30
- }
31
- }
32
- // Plain params: timeout=30s
33
- const eqIdx = token.indexOf("=");
34
- if (eqIdx >= 0) {
35
- modifiers.push({
36
- name: token.slice(0, eqIdx),
37
- params: token.slice(eqIdx + 1),
38
- });
39
- }
40
- else {
41
- modifiers.push({ name: token });
42
- }
43
- }
44
- return {
45
- language,
46
- modifiers,
47
- executable: modifiers.some((m) => m.name === "exec" || m.name === "eval"),
48
- };
49
- }
50
- function computeLineStarts(text) {
51
- const starts = [0];
52
- for (let i = 0; i < text.length; i++) {
53
- if (text[i] === "\n") {
54
- starts.push(i + 1);
55
- }
56
- }
57
- return starts;
58
- }
59
- function positionAt(index, offset) {
60
- const { lineStarts, origin } = index;
61
- let low = 0;
62
- let high = lineStarts.length - 1;
63
- while (low < high) {
64
- const mid = (low + high + 1) >> 1;
65
- if (lineStarts[mid] <= offset) {
66
- low = mid;
67
- }
68
- else {
69
- high = mid - 1;
70
- }
71
- }
72
- const localLine = low + 1;
73
- const column = offset - lineStarts[low] + 1;
74
- return {
75
- path: origin?.path,
76
- offset: (origin?.baseOffset ?? 0) + offset,
77
- line: (origin?.baseLine ?? 1) + localLine - 1,
78
- column,
79
- };
80
- }
81
- /**
82
- * Scan raw markdown text into segments.
83
- *
84
- * Identifies component invocations (`<PascalCase ...>`), executable code
85
- * blocks (fenced with `exec` in info string), and everything else as text.
86
- *
87
- * When `origin` is provided, component invocations carry `position` values
88
- * expressed in the original file's coordinates.
89
- */
90
- export function scanSegments(text, origin) {
91
- const index = { origin, lineStarts: computeLineStarts(text) };
92
- const segments = [];
93
- let pos = 0;
94
- let textStart = 0;
95
- while (pos < text.length) {
96
- // Check for fenced code block
97
- const fenceMatch = matchFenceOpen(text, pos);
98
- if (fenceMatch) {
99
- // Flush text before fence
100
- if (pos > textStart) {
101
- pushText(segments, text.slice(textStart, pos));
102
- }
103
- const fenceEnd = findFenceClose(text, fenceMatch.contentStart, fenceMatch.fenceChar, fenceMatch.fenceLen);
104
- const content = text.slice(fenceMatch.contentStart, fenceEnd.contentEnd);
105
- const fullFence = text.slice(pos, fenceEnd.fenceEnd);
106
- const parsed = parseInfoString(fenceMatch.infoString);
107
- if (parsed.executable) {
108
- segments.push({
109
- type: "codeBlock",
110
- language: parsed.language,
111
- content,
112
- modifiers: parsed.modifiers,
113
- executable: true,
114
- });
115
- }
116
- else {
117
- // Non-executable code block: preserve as text
118
- pushText(segments, fullFence);
119
- }
120
- pos = fenceEnd.fenceEnd;
121
- textStart = pos;
122
- continue;
123
- }
124
- // Skip inline code spans — content inside backticks is inert
125
- if (text[pos] === "`") {
126
- const codeEnd = skipInlineCode(text, pos);
127
- if (codeEnd !== -1) {
128
- pos = codeEnd;
129
- continue;
130
- }
131
- }
132
- // Check for component invocation: `<` followed by uppercase letter
133
- if (text[pos] === "<" && pos + 1 < text.length && /[A-Z]/.test(text[pos + 1])) {
134
- // Make sure we're not inside a fenced code block (handled above)
135
- const component = parseComponentTag(text, pos, index);
136
- if (component) {
137
- // Flush text before component
138
- if (pos > textStart) {
139
- pushText(segments, text.slice(textStart, pos));
140
- }
141
- segments.push(component.segment);
142
- pos = component.end;
143
- textStart = pos;
144
- continue;
145
- }
146
- }
147
- pos++;
148
- }
149
- // Flush remaining text
150
- if (textStart < text.length) {
151
- pushText(segments, text.slice(textStart));
152
- }
153
- return segments;
154
- }
155
- /**
156
- * Check if position is at the start of a fenced code block opening.
157
- * Must be at the start of a line (pos === 0 or preceded by newline).
158
- * Supports both backtick and tilde fences, with optional leading spaces (0-3).
159
- */
160
- function matchFenceOpen(text, pos) {
161
- // Must be at start of line
162
- if (pos > 0 && text[pos - 1] !== "\n")
163
- return null;
164
- let i = pos;
165
- // Optional leading spaces (0-3)
166
- let spaces = 0;
167
- while (spaces < 3 && i < text.length && text[i] === " ") {
168
- spaces++;
169
- i++;
170
- }
171
- if (i >= text.length)
172
- return null;
173
- const fenceChar = text[i];
174
- if (fenceChar !== "`" && fenceChar !== "~")
175
- return null;
176
- // Count fence characters (minimum 3)
177
- let fenceLen = 0;
178
- while (i < text.length && text[i] === fenceChar) {
179
- fenceLen++;
180
- i++;
181
- }
182
- if (fenceLen < 3)
183
- return null;
184
- // Info string goes to end of line
185
- const lineEnd = text.indexOf("\n", i);
186
- const infoString = lineEnd === -1 ? text.slice(i).trim() : text.slice(i, lineEnd).trim();
187
- // Backtick fences: info string must not contain backticks
188
- if (fenceChar === "`" && infoString.includes("`"))
189
- return null;
190
- const contentStart = lineEnd === -1 ? text.length : lineEnd + 1;
191
- return { fenceChar, fenceLen, infoString, contentStart };
192
- }
193
- /**
194
- * Find the closing fence for an open code block.
195
- */
196
- function findFenceClose(text, contentStart, fenceChar, fenceLen) {
197
- let pos = contentStart;
198
- while (pos < text.length) {
199
- // Must be at start of line
200
- const lineStart = pos;
201
- // Optional leading spaces (0-3)
202
- let spaces = 0;
203
- while (spaces < 3 && pos < text.length && text[pos] === " ") {
204
- spaces++;
205
- pos++;
206
- }
207
- // Check for closing fence
208
- if (pos < text.length && text[pos] === fenceChar) {
209
- let closeFenceLen = 0;
210
- while (pos < text.length && text[pos] === fenceChar) {
211
- closeFenceLen++;
212
- pos++;
213
- }
214
- // Closing fence must be at least as long as opening and followed by
215
- // only optional spaces then newline or end of string
216
- if (closeFenceLen >= fenceLen) {
217
- // Skip optional trailing spaces
218
- while (pos < text.length && text[pos] === " ") {
219
- pos++;
220
- }
221
- if (pos >= text.length || text[pos] === "\n") {
222
- const fenceEnd = pos < text.length ? pos + 1 : pos;
223
- return { contentEnd: lineStart, fenceEnd };
224
- }
225
- }
226
- }
227
- // Skip to next line
228
- const nextNewline = text.indexOf("\n", pos);
229
- if (nextNewline === -1) {
230
- // No closing fence found — content goes to end
231
- return { contentEnd: text.length, fenceEnd: text.length };
232
- }
233
- pos = nextNewline + 1;
234
- }
235
- return { contentEnd: text.length, fenceEnd: text.length };
236
- }
237
- /**
238
- * Parse a JSX component tag starting at `<` (pos).
239
- * Returns the parsed component invocation and the position after it,
240
- * or null if this is not a valid component tag.
241
- */
242
- function parseComponentTag(text, start, index) {
243
- let pos = start + 1; // Skip '<'
244
- // Parse tag name — must start with uppercase, can contain dots
245
- const nameStart = pos;
246
- if (pos >= text.length || !/[A-Z]/.test(text[pos]))
247
- return null;
248
- while (pos < text.length && /[A-Za-z0-9._]/.test(text[pos])) {
249
- pos++;
250
- }
251
- const name = text.slice(nameStart, pos);
252
- if (!name)
253
- return null;
254
- // Parse attributes
255
- const { props, expressions, end: attrEnd } = parseAttributes(text, pos);
256
- if (attrEnd === -1)
257
- return null;
258
- pos = attrEnd;
259
- // Skip whitespace
260
- pos = skipWhitespace(text, pos);
261
- // Self-closing tag?
262
- if (pos < text.length && text[pos] === "/") {
263
- pos++; // Skip '/'
264
- if (pos >= text.length || text[pos] !== ">")
265
- return null;
266
- pos++; // Skip '>'
267
- return {
268
- segment: {
269
- type: "component",
270
- name,
271
- props,
272
- expressions,
273
- children: [],
274
- selfClosing: true,
275
- position: positionAt(index, start),
276
- },
277
- end: pos,
278
- };
279
- }
280
- // Opening tag: must end with '>'
281
- if (pos >= text.length || text[pos] !== ">")
282
- return null;
283
- pos++; // Skip '>'
284
- // Parse children until closing tag
285
- const { children, end: childEnd } = parseChildren(text, pos, name, index);
286
- if (childEnd === -1)
287
- return null;
288
- return {
289
- segment: {
290
- type: "component",
291
- name,
292
- props,
293
- expressions,
294
- children,
295
- selfClosing: false,
296
- position: positionAt(index, start),
297
- },
298
- end: childEnd,
299
- };
300
- }
301
- function parseAttributes(text, pos) {
302
- const props = {};
303
- const expressions = {};
304
- while (pos < text.length) {
305
- pos = skipWhitespace(text, pos);
306
- if (pos >= text.length)
307
- return { props, expressions, end: -1 };
308
- // End of attributes?
309
- if (text[pos] === "/" || text[pos] === ">") {
310
- return { props, expressions, end: pos };
311
- }
312
- // Spread props: {...expr}
313
- if (text[pos] === "{" &&
314
- pos + 2 < text.length &&
315
- text[pos + 1] === "." &&
316
- text[pos + 2] === ".") {
317
- // Skip spread — consume the expression
318
- const exprEnd = findMatchingBrace(text, pos);
319
- if (exprEnd === -1)
320
- return { props, expressions, end: -1 };
321
- // We don't evaluate spread props — just skip them
322
- pos = exprEnd + 1;
323
- continue;
324
- }
325
- // Attribute name
326
- const attrNameStart = pos;
327
- while (pos < text.length && /[A-Za-z0-9_-]/.test(text[pos])) {
328
- pos++;
329
- }
330
- const attrName = text.slice(attrNameStart, pos);
331
- if (!attrName)
332
- return { props, expressions, end: -1 };
333
- pos = skipWhitespace(text, pos);
334
- // Boolean attribute (no value): `verbose`
335
- if (pos >= text.length ||
336
- text[pos] === "/" ||
337
- text[pos] === ">" ||
338
- /[A-Za-z{]/.test(text[pos])) {
339
- // Check it's not `=`
340
- if (pos < text.length && text[pos] !== "=") {
341
- props[attrName] = true;
342
- continue;
343
- }
344
- }
345
- // Must have '='
346
- if (pos >= text.length || text[pos] !== "=") {
347
- // Boolean attribute before /> or >
348
- props[attrName] = true;
349
- continue;
350
- }
351
- pos++; // Skip '='
352
- pos = skipWhitespace(text, pos);
353
- // Attribute value
354
- if (pos >= text.length)
355
- return { props, expressions, end: -1 };
356
- if (text[pos] === '"') {
357
- // String attribute: "value"
358
- const strEnd = findClosingQuote(text, pos + 1, '"');
359
- if (strEnd === -1)
360
- return { props, expressions, end: -1 };
361
- props[attrName] = text.slice(pos + 1, strEnd);
362
- pos = strEnd + 1;
363
- }
364
- else if (text[pos] === "'") {
365
- // String attribute: 'value'
366
- const strEnd = findClosingQuote(text, pos + 1, "'");
367
- if (strEnd === -1)
368
- return { props, expressions, end: -1 };
369
- props[attrName] = text.slice(pos + 1, strEnd);
370
- pos = strEnd + 1;
371
- }
372
- else if (text[pos] === "{") {
373
- // Expression attribute: {expr}
374
- const exprEnd = findMatchingBrace(text, pos);
375
- if (exprEnd === -1)
376
- return { props, expressions, end: -1 };
377
- const exprText = text.slice(pos + 1, exprEnd).trim();
378
- const result = parseExpressionValue(exprText);
379
- if (result.kind === "resolved") {
380
- props[attrName] = result.value;
381
- }
382
- else {
383
- expressions[attrName] = result.expression;
384
- }
385
- pos = exprEnd + 1;
386
- }
387
- else {
388
- return { props, expressions, end: -1 };
389
- }
390
- }
391
- return { props, expressions, end: -1 };
392
- }
393
- /**
394
- * Find matching closing brace, respecting nested braces, strings, and
395
- * template literals.
396
- */
397
- function findMatchingBrace(text, start) {
398
- let depth = 0;
399
- let pos = start;
400
- while (pos < text.length) {
401
- const ch = text[pos];
402
- if (ch === "{") {
403
- depth++;
404
- }
405
- else if (ch === "}") {
406
- depth--;
407
- if (depth === 0)
408
- return pos;
409
- }
410
- else if (ch === '"' || ch === "'") {
411
- // Skip string literal
412
- const end = findClosingQuote(text, pos + 1, ch);
413
- if (end === -1)
414
- return -1;
415
- pos = end;
416
- }
417
- else if (ch === "`") {
418
- // Skip template literal
419
- pos = skipTemplateLiteral(text, pos);
420
- if (pos === -1)
421
- return -1;
422
- }
423
- pos++;
424
- }
425
- return -1;
426
- }
427
- /**
428
- * Skip a template literal starting at the opening backtick.
429
- * Returns the position of the closing backtick, or -1.
430
- */
431
- function skipTemplateLiteral(text, start) {
432
- let pos = start + 1; // Skip opening `
433
- while (pos < text.length) {
434
- const ch = text[pos];
435
- if (ch === "`") {
436
- return pos;
437
- }
438
- else if (ch === "\\" && pos + 1 < text.length) {
439
- pos += 2; // Skip escape sequence
440
- continue;
441
- }
442
- else if (ch === "$" && pos + 1 < text.length && text[pos + 1] === "{") {
443
- // Template expression — find matching brace
444
- const braceEnd = findMatchingBrace(text, pos + 1);
445
- if (braceEnd === -1)
446
- return -1;
447
- pos = braceEnd + 1;
448
- continue;
449
- }
450
- pos++;
451
- }
452
- return -1;
453
- }
454
- function findClosingQuote(text, start, quote) {
455
- let pos = start;
456
- while (pos < text.length) {
457
- if (text[pos] === "\\") {
458
- pos += 2;
459
- continue;
460
- }
461
- if (text[pos] === quote)
462
- return pos;
463
- pos++;
464
- }
465
- return -1;
466
- }
467
- /**
468
- * Parse a JSX expression value.
469
- *
470
- * JSON-compatible literals (numbers, booleans, null, objects, arrays) are
471
- * resolved at scan time. Everything else is an eval expression — raw
472
- * expression text to be evaluated against env.values at expansion time.
473
- */
474
- export function parseExpressionValue(expr) {
475
- const trimmed = expr.trim();
476
- // Try JSON-compatible parse for objects, arrays
477
- try {
478
- // Handle some JSX expression patterns
479
- // Convert single-quoted strings to double-quoted for JSON
480
- const jsonCandidate = trimmed
481
- // Object/array shorthand: { key: "value" } → {"key": "value"}
482
- .replace(/(\{|,)\s*([A-Za-z_]\w*)\s*:/g, '$1 "$2":')
483
- // Single-quoted string values inside objects
484
- .replace(/:\s*'([^']*)'/g, ': "$1"');
485
- const parsed = JSON.parse(jsonCandidate);
486
- return { kind: "resolved", value: parsed };
487
- }
488
- catch {
489
- // Fall through
490
- }
491
- // Number
492
- if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
493
- return { kind: "resolved", value: Number(trimmed) };
494
- }
495
- // Boolean
496
- if (trimmed === "true")
497
- return { kind: "resolved", value: true };
498
- if (trimmed === "false")
499
- return { kind: "resolved", value: false };
500
- // null/undefined
501
- if (trimmed === "null" || trimmed === "undefined") {
502
- return { kind: "resolved", value: null };
503
- }
504
- // Everything else is an eval expression — evaluate at expansion time
505
- return { kind: "eval", expression: trimmed };
506
- }
507
- function parseChildren(text, start, tagName, index) {
508
- let pos = start;
509
- let textStart = pos;
510
- const children = [];
511
- // Build the closing tag pattern
512
- const closingTag = `</${tagName}>`;
513
- while (pos < text.length) {
514
- // Check for closing tag
515
- if (text.startsWith(closingTag, pos)) {
516
- // Flush remaining text as child
517
- if (pos > textStart) {
518
- pushText(children, text.slice(textStart, pos));
519
- }
520
- return { children, end: pos + closingTag.length };
521
- }
522
- // Check for fenced code block inside children
523
- const fenceMatch = matchFenceOpen(text, pos);
524
- if (fenceMatch) {
525
- if (pos > textStart) {
526
- pushText(children, text.slice(textStart, pos));
527
- }
528
- const fenceEnd = findFenceClose(text, fenceMatch.contentStart, fenceMatch.fenceChar, fenceMatch.fenceLen);
529
- const content = text.slice(fenceMatch.contentStart, fenceEnd.contentEnd);
530
- const fullFence = text.slice(pos, fenceEnd.fenceEnd);
531
- const parsed = parseInfoString(fenceMatch.infoString);
532
- if (parsed.executable) {
533
- children.push({
534
- type: "codeBlock",
535
- language: parsed.language,
536
- content,
537
- modifiers: parsed.modifiers,
538
- executable: true,
539
- });
540
- }
541
- else {
542
- pushText(children, fullFence);
543
- }
544
- pos = fenceEnd.fenceEnd;
545
- textStart = pos;
546
- continue;
547
- }
548
- // Skip inline code spans — content inside backticks is inert
549
- if (text[pos] === "`") {
550
- const codeEnd = skipInlineCode(text, pos);
551
- if (codeEnd !== -1) {
552
- pos = codeEnd;
553
- continue;
554
- }
555
- }
556
- // Check for nested component
557
- if (text[pos] === "<" && pos + 1 < text.length && /[A-Z]/.test(text[pos + 1])) {
558
- const nested = parseComponentTag(text, pos, index);
559
- if (nested) {
560
- if (pos > textStart) {
561
- pushText(children, text.slice(textStart, pos));
562
- }
563
- children.push(nested.segment);
564
- pos = nested.end;
565
- textStart = pos;
566
- continue;
567
- }
568
- }
569
- pos++;
570
- }
571
- // No closing tag found
572
- return { children, end: -1 };
573
- }
574
- // ---------------------------------------------------------------------------
575
- // Inline code span skipping
576
- // ---------------------------------------------------------------------------
577
- /**
578
- * If `pos` is at a backtick, skip past the closing backtick sequence
579
- * of the inline code span. Returns the position after the closing
580
- * backticks, or -1 if no matching close is found (unclosed code span).
581
- *
582
- * Handles multi-backtick sequences per CommonMark: the opening and
583
- * closing sequences must have the same number of backticks.
584
- */
585
- function skipInlineCode(text, pos) {
586
- if (text[pos] !== "`")
587
- return -1;
588
- // Count opening backticks
589
- let backtickLen = 0;
590
- let p = pos;
591
- while (p < text.length && text[p] === "`") {
592
- backtickLen++;
593
- p++;
594
- }
595
- // Search for matching closing sequence (same length)
596
- while (p < text.length) {
597
- if (text[p] === "`") {
598
- let closeLen = 0;
599
- while (p < text.length && text[p] === "`") {
600
- closeLen++;
601
- p++;
602
- }
603
- if (closeLen === backtickLen) {
604
- return p; // position after closing backticks
605
- }
606
- // Wrong length — keep searching
607
- continue;
608
- }
609
- p++;
610
- }
611
- return -1; // unclosed code span
612
- }
613
- // ---------------------------------------------------------------------------
614
- // Helpers
615
- // ---------------------------------------------------------------------------
616
- function skipWhitespace(text, pos) {
617
- while (pos < text.length && /\s/.test(text[pos])) {
618
- pos++;
619
- }
620
- return pos;
621
- }
622
- function pushText(segments, content) {
623
- if (content.length === 0)
624
- return;
625
- // Merge with previous text segment if possible
626
- const last = segments[segments.length - 1];
627
- if (last && last.type === "text") {
628
- last.content += content;
629
- return;
630
- }
631
- segments.push({ type: "text", content });
632
- }
@@ -1,68 +0,0 @@
1
- /**
2
- * Temp-file eval block compiler middleware for Node and Bun.
3
- *
4
- * Compiles eval block source into generator functions by writing
5
- * temporary .ts files to `.xmd-eval/` and dynamically importing them.
6
- * Bun handles .ts natively; Node requires tsx.
7
- *
8
- * Standard imports (Effection, executable.md APIs) are captured in the middleware
9
- * closure — they are not part of the Compiler API interface.
10
- *
11
- * Installed automatically by `execute` when running on Node or Bun.
12
- */
13
- import { call } from "effection";
14
- import { API } from "@executablemd/runtime";
15
- // STANDARD_IMPORTS below resolve at runtime from generated eval modules;
16
- // without these static anchors, `deno compile --exclude-unused-npm` prunes
17
- // the packages from the binary and every eval block using them fails.
18
- import "@effectionx/converge";
19
- import "@effectionx/fetch";
20
- import { writeFile, unlink, mkdir } from "node:fs/promises";
21
- import { resolve } from "node:path";
22
- import { randomUUID } from "node:crypto";
23
- /**
24
- * Standard import statements prepended to every generated eval module.
25
- * Captured in the middleware closure — not exposed on the public API.
26
- */
27
- const STANDARD_IMPORTS = [
28
- 'import { sleep, spawn, call, resource, useScope, createChannel, each, suspend, createSignal } from "effection";',
29
- 'import { when } from "@effectionx/converge";',
30
- 'import { fetch } from "@effectionx/fetch";',
31
- 'import { useContent, Sample } from "@executablemd/core";',
32
- 'import { findFreePort } from "@executablemd/runtime";',
33
- ];
34
- const EVAL_DIR = ".xmd-eval";
35
- /**
36
- * Install the temp-file compiler as middleware on the current scope.
37
- *
38
- * Works on Node (via tsx) and Bun (native .ts support).
39
- * Writes each compiled module to `.xmd-eval/<uuid>.ts`, imports it,
40
- * captures the default export, and deletes the file.
41
- */
42
- export function* useTempFileCompiler() {
43
- // Ensure the eval directory exists
44
- yield* call(() => mkdir(EVAL_DIR, { recursive: true }));
45
- yield* API.Compiler.around({
46
- *compile([source, options], next) {
47
- void next; // terminal middleware — does not delegate
48
- const userImports = options?.imports ?? [];
49
- const allImports = [...STANDARD_IMPORTS, ...userImports];
50
- const importLines = allImports.join("\n");
51
- const moduleSource = [importLines, `export default function*(env) {`, source, `}`].join("\n");
52
- const tmpPath = resolve(EVAL_DIR, `${randomUUID()}.ts`);
53
- yield* call(() => writeFile(tmpPath, moduleSource, "utf-8"));
54
- try {
55
- const fileUrl = new URL(`file://${tmpPath}`).href;
56
- const mod = yield* call(() => import(fileUrl));
57
- if (typeof mod.default !== "function") {
58
- throw new Error(`useTempFileCompiler: expected default export to be a generator function, got ${typeof mod.default}`);
59
- }
60
- return mod.default;
61
- }
62
- finally {
63
- // Clean up temp file — don't await, fire and forget
64
- unlink(tmpPath).catch(() => { });
65
- }
66
- },
67
- });
68
- }