@orkestrel/markdown 0.0.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.
@@ -0,0 +1,2833 @@
1
+ //#region src/core/constants.ts
2
+ /**
3
+ * The URL schemes `renderHTML` permits on a link `href` - anything else (notably
4
+ * `javascript:`, `data:`, `vbscript:`, `file:`) is dropped to an empty `href` so a
5
+ * hostile link can never execute. Frozen, lower-case; a relative / anchor /
6
+ * scheme-less `href` (no `scheme:` prefix) is always allowed.
7
+ */
8
+ var SAFE_URL_SCHEMES = /* @__PURE__ */ new Set([
9
+ "http",
10
+ "https",
11
+ "mailto",
12
+ "tel"
13
+ ]);
14
+ /**
15
+ * The maximum recursion depth the parse pipeline (`parseDocument` and its
16
+ * `parsers.ts` helpers) and the `helpers.ts` traversal / render functions
17
+ * (`renderHTML`, `renderMarkdown`, `walkNodes`, `foldNode`) honor before degrading to
18
+ * literal text - bounds blockquote nesting, inline nesting (emphasis / links), and
19
+ * traversal/render recursion so pathological or hostile input (deeply nested
20
+ * blockquotes, runaway emphasis) cannot exhaust the call stack. Past this depth the
21
+ * parser treats the remaining content as literal text instead of recursing further.
22
+ */
23
+ var MAX_DEPTH = 64;
24
+ Object.freeze([
25
+ "null",
26
+ "boolean",
27
+ "object",
28
+ "array",
29
+ "number",
30
+ "integer",
31
+ "string"
32
+ ]);
33
+ /** Determine whether a value is `null`. */
34
+ function isNull(value) {
35
+ return value === null;
36
+ }
37
+ /** Determine whether a value is `undefined`. */
38
+ function isUndefined(value) {
39
+ return value === void 0;
40
+ }
41
+ /** Determine whether a value is a string. */
42
+ function isString(value) {
43
+ return typeof value === "string";
44
+ }
45
+ /**
46
+ * Determine whether a value is a number.
47
+ *
48
+ * @remarks
49
+ * Includes `NaN` and `±Infinity` — use {@link isFiniteNumber} to exclude them.
50
+ */
51
+ function isNumber(value) {
52
+ return typeof value === "number";
53
+ }
54
+ /** Determine whether a value is a finite number (excludes `NaN` and `±Infinity`). */
55
+ function isFiniteNumber(value) {
56
+ return typeof value === "number" && Number.isFinite(value);
57
+ }
58
+ /** Determine whether a value is a finite integer (excludes `NaN`, `±Infinity`, and fractional numbers). */
59
+ function isInteger(value) {
60
+ return Number.isInteger(value);
61
+ }
62
+ /** Determine whether a value is a boolean. */
63
+ function isBoolean(value) {
64
+ return typeof value === "boolean";
65
+ }
66
+ /**
67
+ * Determine whether a value is a non-null object.
68
+ *
69
+ * @remarks
70
+ * `true` for arrays, class instances, plain objects, `Map`, `Set`, etc. — use
71
+ * {@link isRecord} when you need a plain-record check.
72
+ */
73
+ function isObject(value) {
74
+ return typeof value === "object" && value !== null;
75
+ }
76
+ /**
77
+ * Determine whether a value is a plain record (object literal or null-prototype),
78
+ * not an array or class instance.
79
+ *
80
+ * @remarks
81
+ * Use instead of {@link isObject} to distinguish a plain `{}` /
82
+ * `Object.create(null)` from arrays, `Date`, `Map`, etc. The prototype-chain
83
+ * test is realm-agnostic: rather than comparing against the current realm's
84
+ * `Object.prototype` (which a plain object from another `vm.Context`, iframe,
85
+ * or worker would fail), it accepts any value whose prototype is `null`, OR
86
+ * whose prototype's own prototype is `null` — the shape every plain object
87
+ * has in every realm, since `Object.prototype` itself always sits one step
88
+ * above `null`. Arrays and class instances are still rejected: an array's
89
+ * prototype chain runs through `Array.prototype` before `null`, and a class
90
+ * instance's runs through the class's own prototype. The whole body runs
91
+ * inside `attempt` (AGENTS §14) so a revoked `Proxy` or a hostile
92
+ * `getPrototypeOf` trap cannot escape as a thrown error.
93
+ */
94
+ function isRecord(value) {
95
+ const outcome = attempt(() => {
96
+ if (!isObject(value) || isArray(value)) return false;
97
+ const prototype = Object.getPrototypeOf(value);
98
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
99
+ });
100
+ return outcome.success && outcome.value;
101
+ }
102
+ /** Determine whether a value is an array. */
103
+ function isArray(value) {
104
+ return Array.isArray(value);
105
+ }
106
+ /** Determine whether a value is the empty string `''`. */
107
+ function isEmptyString(value) {
108
+ return isString(value) && value.length === 0;
109
+ }
110
+ /** Determine whether a value is a non-empty string (at least one character). */
111
+ function isNonEmptyString(value) {
112
+ return isString(value) && value.length > 0;
113
+ }
114
+ /** Determine whether a value is a non-empty array (at least one element). */
115
+ function isNonEmptyArray(value) {
116
+ return isArray(value) && value.length > 0;
117
+ }
118
+ /**
119
+ * Determine whether a value is a cycle-safe JSON value.
120
+ *
121
+ * @remarks
122
+ * Total guard: never throws, returns `false` for cycles, functions, `Date`
123
+ * instances, class instances, `NaN`, and `±Infinity`. Arrays and plain records
124
+ * are walked with an ancestor set so recursive input fails instead of hanging.
125
+ * The whole walk runs inside `attempt` (AGENTS §14): a hostile getter on a
126
+ * record property, or a revoked `Proxy` anywhere in the structure, is caught
127
+ * and yields `false` instead of escaping as a thrown error.
128
+ *
129
+ * @param value - The value to test
130
+ * @returns `true` when the value has a JSON representation
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * isJSONValue({ nested: [1, 'x', null] }) // true
135
+ * isJSONValue(Number.NaN) // false
136
+ * ```
137
+ */
138
+ function isJSONValue(value) {
139
+ const ancestors = /* @__PURE__ */ new WeakSet();
140
+ const check = (entry) => {
141
+ if (entry === null || isString(entry) || isBoolean(entry) || isFiniteNumber(entry)) return true;
142
+ if (Array.isArray(entry)) {
143
+ if (ancestors.has(entry)) return false;
144
+ ancestors.add(entry);
145
+ const valid = entry.every(check);
146
+ ancestors.delete(entry);
147
+ return valid;
148
+ }
149
+ if (!isRecord(entry)) return false;
150
+ if (ancestors.has(entry)) return false;
151
+ ancestors.add(entry);
152
+ const valid = Object.values(entry).every(check);
153
+ ancestors.delete(entry);
154
+ return valid;
155
+ };
156
+ const outcome = attempt(() => check(value));
157
+ return outcome.success && outcome.value;
158
+ }
159
+ /**
160
+ * Invoke a callback and capture its outcome as a {@link Result}, never letting
161
+ * a throw escape.
162
+ *
163
+ * @remarks
164
+ * The single sanctioned never-throw boundary for the guards (AGENTS §14). The
165
+ * `whereOf`, `lazyOf`, and `transformOf` combinators invoke caller-supplied
166
+ * callbacks *inside* a guard body, yet a guard must NEVER throw — it returns a
167
+ * `boolean`. This converts a throwing callback into a `Failure` so the
168
+ * surrounding guard can treat it as a non-match instead of propagating the
169
+ * exception, written once and shared rather than copy-pasted as ad-hoc
170
+ * `try`/`catch`.
171
+ *
172
+ * @param callback - The callback to invoke with no arguments
173
+ * @returns A `Success` carrying the return value, or a `Failure` carrying the
174
+ * thrown reason normalised to an `Error`
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * const outcome = attempt(() => predicate(value))
179
+ * return outcome.success && outcome.value
180
+ * ```
181
+ */
182
+ function attempt(callback) {
183
+ try {
184
+ return {
185
+ success: true,
186
+ value: callback()
187
+ };
188
+ } catch (reason) {
189
+ if (reason instanceof Error) return {
190
+ success: false,
191
+ error: reason
192
+ };
193
+ let message = "Unknown thrown value";
194
+ try {
195
+ message = String(reason);
196
+ } catch {}
197
+ return {
198
+ success: false,
199
+ error: new Error(message)
200
+ };
201
+ }
202
+ }
203
+ /**
204
+ * Build a deterministic pseudo-random source seeded from a single number.
205
+ *
206
+ * @remarks
207
+ * A mulberry32 generator — the same seed always yields the same sequence, so
208
+ * generated seed data is reproducible across runs. Used as the default random
209
+ * source for {@link compileGenerator}, seeded from the wall clock so casual
210
+ * callers still get varied output without passing a source themselves.
211
+ *
212
+ * @param seed - The seed for the sequence
213
+ * @returns A {@link RandomFunction} returning values in `[0, 1)`
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * const random = seededRandom(42)
218
+ * random() // always the same first value for seed 42
219
+ * ```
220
+ */
221
+ function seededRandom(seed) {
222
+ let state = seed >>> 0;
223
+ return () => {
224
+ state = state + 1831565813 >>> 0;
225
+ let t = state;
226
+ t = Math.imul(t ^ t >>> 15, t | 1);
227
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
228
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
229
+ };
230
+ }
231
+ function arrayOf(elementGuard) {
232
+ return (value) => {
233
+ if (!isArray(value)) return false;
234
+ const outcome = attempt(() => value.every(elementGuard));
235
+ return outcome.success && outcome.value;
236
+ };
237
+ }
238
+ /**
239
+ * Build a guard that accepts values identical (via `Object.is`) to one of the
240
+ * provided literal primitives.
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * const isRole = literalOf('admin', 'member', 'guest')
245
+ * isRole('admin') // true
246
+ * isRole('owner') // false
247
+ * ```
248
+ */
249
+ function literalOf(...literals) {
250
+ return (value) => literals.some((literal) => Object.is(literal, value));
251
+ }
252
+ /**
253
+ * Build a guard that accepts plain records matching a guard shape.
254
+ *
255
+ * @remarks
256
+ * Three calling modes depending on the `optional` argument:
257
+ * - **No `optional`** — all shape keys required; extra keys rejected.
258
+ * - **`optional: K[]`** — the listed keys are optional; all others required.
259
+ * - **`optional: true`** — every shape key is optional.
260
+ *
261
+ * Key presence is tested with `Object.hasOwn`, so a shape key satisfied only by
262
+ * an inherited prototype member (`toString`, `constructor`, …) counts as absent.
263
+ * A non-object / `null` / array input returns `false` rather than throwing. The
264
+ * extra-key check only inspects `Object.keys` (string keys), so an extra
265
+ * enumerable SYMBOL key is never rejected — intentional, for JSON fidelity, and
266
+ * matches the compiled guard.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * const isUser = recordOf({ name: isString, age: isNumber })
271
+ * isUser({ name: 'Ada', age: 36 }) // true
272
+ * isUser({ name: 'Ada' }) // false — age missing
273
+ *
274
+ * const isPartial = recordOf({ name: isString, age: isNumber }, ['age'])
275
+ * isPartial({ name: 'Ada' }) // true
276
+ * ```
277
+ */
278
+ function recordOf(shape, optional) {
279
+ const allowed = /* @__PURE__ */ new Set();
280
+ for (const key in shape) if (Object.prototype.hasOwnProperty.call(shape, key)) allowed.add(key);
281
+ const optionalSet = new Set(optional === true ? [...allowed] : isArray(optional) ? optional.map((key) => String(key)) : []);
282
+ return (value) => {
283
+ if (!isRecord(value)) return false;
284
+ const outcome = attempt(() => {
285
+ for (const key of Object.keys(value)) if (!allowed.has(key)) return false;
286
+ for (const key in shape) {
287
+ if (!Object.prototype.hasOwnProperty.call(shape, key)) continue;
288
+ const present = Object.hasOwn(value, key);
289
+ if (!optionalSet.has(key) && !present) return false;
290
+ if (present) {
291
+ const guard = shape[key];
292
+ if (!guard(value[key])) return false;
293
+ }
294
+ }
295
+ return true;
296
+ });
297
+ return outcome.success && outcome.value;
298
+ };
299
+ }
300
+ function orOf(left, right) {
301
+ return (value) => left(value) || right(value);
302
+ }
303
+ function unionOf(...guards) {
304
+ return (value) => guards.some((guard) => guard(value));
305
+ }
306
+ function intersectionOf(...guards) {
307
+ return (value) => guards.every((guard) => guard(value));
308
+ }
309
+ function whereOf(base, predicate) {
310
+ return (value) => {
311
+ if (!base(value)) return false;
312
+ const outcome = attempt(() => predicate(value));
313
+ return outcome.success && outcome.value;
314
+ };
315
+ }
316
+ /**
317
+ * Defer guard creation until first use by calling `thunk()` on every
318
+ * invocation.
319
+ *
320
+ * @remarks
321
+ * `thunk` is called on every guard call, not cached — this lets it close over a
322
+ * binding assigned *after* `lazyOf` is called, the primary use case for
323
+ * self-referential recursive guards. Per §14 a throw from `thunk` (or the guard
324
+ * it resolves to) is contained and reported as a non-match.
325
+ *
326
+ * A recursive guard built this way has no cycle/depth detection: a cyclic or
327
+ * pathologically deep input is stack-bounded — the overflow is contained and the
328
+ * guard returns `false` rather than throwing, but it is not validated correctly
329
+ * past that bound.
330
+ *
331
+ * @example
332
+ * ```ts
333
+ * type Tree = { value: number; children: Tree[] }
334
+ * let isTree: Guard<Tree>
335
+ * isTree = recordOf({ value: isNumber, children: arrayOf(lazyOf(() => isTree)) })
336
+ * ```
337
+ */
338
+ function lazyOf(thunk) {
339
+ return (value) => {
340
+ const outcome = attempt(() => thunk()(value));
341
+ return outcome.success && outcome.value;
342
+ };
343
+ }
344
+ /**
345
+ * Build a guard that accepts finite numbers within an inclusive `[min, max]`
346
+ * range.
347
+ *
348
+ * @remarks
349
+ * Refines {@link isFiniteNumber} with the bound comparison, so `NaN` /
350
+ * `±Infinity` are rejected before any comparison runs. An absent bound never
351
+ * constrains that side. Reused for a number's own value AND, applied to a
352
+ * `.length`, for string and array length refinements — the single source of the
353
+ * bound logic shared by the compiled guard and parser (compilers.ts).
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * const inRange = boundsOf(1, 5)
358
+ * inRange(3) // true
359
+ * inRange(0) // false — below min
360
+ * inRange(6) // false — above max
361
+ *
362
+ * const atLeastTwo = boundsOf(2)
363
+ * atLeastTwo(2) // true — unbounded above
364
+ * ```
365
+ */
366
+ function boundsOf(min, max) {
367
+ return whereOf(isFiniteNumber, (value) => (min === void 0 || value >= min) && (max === void 0 || value <= max));
368
+ }
369
+ /**
370
+ * Build a guard that accepts strings satisfying optional length and pattern
371
+ * refinements — `min` / `max` length and a `pattern`.
372
+ *
373
+ * @remarks
374
+ * Composes {@link isString} with {@link boundsOf} on the string's `.length` and
375
+ * an inline `pattern.test` (the same refinement {@link matchOf} performs). When all three options are absent it returns
376
+ * the bare {@link isString} guard (the unconstrained fast path), so an
377
+ * unrefined string leaf pays no wrapping cost. The single source of the string
378
+ * refinement shared by the compiled guard and parser (compilers.ts).
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * const isSlug = stringOf({ min: 1, max: 32, pattern: /^[a-z-]+$/ })
383
+ * isSlug('hello-world') // true
384
+ * isSlug('') // false — below min
385
+ * isSlug('Hello') // false — pattern miss
386
+ *
387
+ * stringOf() // identical to isString
388
+ * ```
389
+ */
390
+ function stringOf(options) {
391
+ const min = options?.min;
392
+ const max = options?.max;
393
+ const pattern = options?.pattern;
394
+ if (min === void 0 && max === void 0 && pattern === void 0) return isString;
395
+ const withinLength = boundsOf(min, max);
396
+ return whereOf(isString, (value) => withinLength(value.length) && (pattern === void 0 || pattern.test(value)));
397
+ }
398
+ /**
399
+ * Extend a guard to also allow `null`.
400
+ *
401
+ * @example
402
+ * ```ts
403
+ * const isNullableString = nullableOf(isString)
404
+ * isNullableString('hi') // true
405
+ * isNullableString(null) // true
406
+ * isNullableString(42) // false
407
+ * ```
408
+ */
409
+ function nullableOf(guard) {
410
+ return (value) => value === null || guard(value);
411
+ }
412
+ /**
413
+ * Parse an unknown value to a string.
414
+ *
415
+ * @remarks
416
+ * A string is returned unchanged; a finite number is coerced to its decimal
417
+ * string (`42` → `'42'`). `NaN`, `±Infinity`, and every other type → `undefined`.
418
+ *
419
+ * @param value - The value to parse
420
+ * @returns A string, or `undefined`
421
+ */
422
+ function parseString(value) {
423
+ if (isString(value)) return value;
424
+ if (isFiniteNumber(value)) return String(value);
425
+ }
426
+ /**
427
+ * Parse an unknown value to a finite number.
428
+ *
429
+ * @remarks
430
+ * A finite number is returned unchanged; a non-blank numeric string is parsed
431
+ * via `Number(...)`. `NaN`, `±Infinity`, blank/non-numeric strings, and every
432
+ * other type → `undefined`.
433
+ *
434
+ * @param value - The value to parse
435
+ * @returns A finite number, or `undefined`
436
+ */
437
+ function parseNumber(value) {
438
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
439
+ if (isString(value)) {
440
+ if (value.trim() === "") return void 0;
441
+ const parsed = Number(value);
442
+ return Number.isFinite(parsed) ? parsed : void 0;
443
+ }
444
+ }
445
+ /**
446
+ * Parse an unknown value to a finite integer.
447
+ *
448
+ * @remarks
449
+ * Accepts whatever {@link parseNumber} accepts, then requires the result to have
450
+ * no fractional part. `3.14` / `'3.14'` → `undefined`.
451
+ *
452
+ * @param value - The value to parse
453
+ * @returns A finite integer, or `undefined`
454
+ */
455
+ function parseInteger(value) {
456
+ const parsed = parseNumber(value);
457
+ if (parsed === void 0) return void 0;
458
+ return Number.isInteger(parsed) ? parsed : void 0;
459
+ }
460
+ /**
461
+ * Parse an unknown value to a boolean.
462
+ *
463
+ * @remarks
464
+ * A boolean is returned unchanged. The strings `'true'` / `'false'` / `'1'` /
465
+ * `'0'` and the numbers `1` / `0` coerce to the matching boolean. Everything
466
+ * else → `undefined`.
467
+ *
468
+ * @param value - The value to parse
469
+ * @returns A boolean, or `undefined`
470
+ */
471
+ function parseBoolean(value) {
472
+ if (typeof value === "boolean") return value;
473
+ if (value === "true" || value === "1" || value === 1) return true;
474
+ if (value === "false" || value === "0" || value === 0) return false;
475
+ }
476
+ /**
477
+ * Parse an unknown value to a plain record — the input reference, never cloned.
478
+ *
479
+ * @param value - The value to parse
480
+ * @returns The record, or `undefined`
481
+ */
482
+ function parseRecord(value) {
483
+ return isRecord(value) ? value : void 0;
484
+ }
485
+ /**
486
+ * Validate that a {@link ContractShape} tree is well-formed — a pure recursive
487
+ * prepass run before compilation.
488
+ *
489
+ * @remarks
490
+ * Fail-fast, per AGENTS §12: a malformed shape is a programmer error, so this
491
+ * throws a plain `Error` immediately rather than surfacing as a silently-wrong
492
+ * guard, parser, schema, or generator later. Checks, recursively:
493
+ *
494
+ * - An {@link OptionalShape} is only legal as a direct object-property value —
495
+ * `optionalShape` wrapping an array item, a union variant, another
496
+ * optional/nullable's inner shape, `additionalProperties`, or the top-level
497
+ * shape all throw. An object property IS the one legal placement: its value
498
+ * is unwrapped to `.inner` before recursing, so `.inner` itself is validated
499
+ * as a normal (non-optional-wrapping) shape.
500
+ * - A {@link UnionShape} needs at least one variant; a {@link LiteralShape}
501
+ * needs at least one value and rejects non-finite (`NaN` / `Infinity` /
502
+ * `-Infinity`) number values.
503
+ * - A bounded {@link StringShape} / {@link NumberShape} / {@link ArrayShape}
504
+ * needs `min <= max` when both are set.
505
+ * - An integer {@link NumberShape} (`integer: true`) needs a non-empty integer
506
+ * range: `Math.ceil(min ?? -Infinity) <= Math.floor(max ?? Infinity)`.
507
+ * - `null` / `json` / `raw` / `boolean` are always-valid leaves. Recursion
508
+ * continues into array items, object properties (and `additionalProperties`
509
+ * when it is a shape), union variants, and optional/nullable inner shapes.
510
+ *
511
+ * @param shape - The shape to validate
512
+ * @throws {Error} When the shape is malformed
513
+ */
514
+ function validateShape(shape) {
515
+ switch (shape.type) {
516
+ case "string":
517
+ if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a string shape has min greater than max");
518
+ return;
519
+ case "number":
520
+ if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a number shape has min greater than max");
521
+ if (shape.integer === true) {
522
+ if (Math.ceil(shape.min ?? Number.NEGATIVE_INFINITY) > Math.floor(shape.max ?? Number.POSITIVE_INFINITY)) throw new Error("validateShape: an integer number shape has an empty integer range");
523
+ }
524
+ return;
525
+ case "boolean":
526
+ case "null":
527
+ case "json":
528
+ case "raw": return;
529
+ case "literal":
530
+ if (shape.values.length === 0) throw new Error("validateShape: a literal shape needs at least one value");
531
+ for (const value of shape.values) if (typeof value === "number" && !Number.isFinite(value)) throw new Error("validateShape: a literal shape may not contain non-finite number values");
532
+ return;
533
+ case "array":
534
+ if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: an array shape has min greater than max");
535
+ validateShape(shape.items);
536
+ return;
537
+ case "object": {
538
+ for (const key of Object.keys(shape.properties)) {
539
+ const child = shape.properties[key];
540
+ if (child === void 0) continue;
541
+ validateShape(child.type === "optional" ? child.inner : child);
542
+ }
543
+ const extra = shape.additionalProperties;
544
+ if (extra !== void 0 && extra !== true && extra !== false) validateShape(extra);
545
+ return;
546
+ }
547
+ case "union":
548
+ if (shape.variants.length === 0) throw new Error("validateShape: a union shape needs at least one variant");
549
+ for (const variant of shape.variants) validateShape(variant);
550
+ return;
551
+ case "optional": throw new Error("validateShape: an optional shape may only appear as a direct object-property value");
552
+ case "nullable":
553
+ validateShape(shape.inner);
554
+ return;
555
+ }
556
+ }
557
+ /**
558
+ * Compile a {@link ContractShape} into a JSON Schema document.
559
+ *
560
+ * @remarks
561
+ * Object shapes emit `additionalProperties: false` (unless opened) and list only
562
+ * required keys in `required`; nullable shapes emit an `anyOf` with `{ type:
563
+ * 'null' }`. Emission only — it never inspects a runtime value.
564
+ *
565
+ * @param shape - The shape to compile
566
+ * @returns The emitted JSON Schema
567
+ */
568
+ function compileSchema(shape) {
569
+ switch (shape.type) {
570
+ case "string": return {
571
+ type: "string",
572
+ ...shape.min !== void 0 ? { minLength: shape.min } : {},
573
+ ...shape.max !== void 0 ? { maxLength: shape.max } : {},
574
+ ...shape.pattern !== void 0 ? { pattern: shape.pattern.source } : {},
575
+ ...shape.description !== void 0 ? { description: shape.description } : {}
576
+ };
577
+ case "number": return {
578
+ type: shape.integer === true ? "integer" : "number",
579
+ ...shape.min !== void 0 ? { minimum: shape.min } : {},
580
+ ...shape.max !== void 0 ? { maximum: shape.max } : {},
581
+ ...shape.description !== void 0 ? { description: shape.description } : {}
582
+ };
583
+ case "boolean": return {
584
+ type: "boolean",
585
+ ...shape.description !== void 0 ? { description: shape.description } : {}
586
+ };
587
+ case "null": return {
588
+ type: "null",
589
+ ...shape.description !== void 0 ? { description: shape.description } : {}
590
+ };
591
+ case "json": return { ...shape.description !== void 0 ? { description: shape.description } : {} };
592
+ case "literal": return {
593
+ enum: [...shape.values],
594
+ ...shape.description !== void 0 ? { description: shape.description } : {}
595
+ };
596
+ case "array": return {
597
+ type: "array",
598
+ items: compileSchema(shape.items),
599
+ ...shape.min !== void 0 ? { minItems: shape.min } : {},
600
+ ...shape.max !== void 0 ? { maxItems: shape.max } : {},
601
+ ...shape.description !== void 0 ? { description: shape.description } : {}
602
+ };
603
+ case "object": {
604
+ const properties = {};
605
+ const required = [];
606
+ for (const key of Object.keys(shape.properties)) {
607
+ const child = shape.properties[key];
608
+ if (child === void 0) continue;
609
+ properties[key] = compileSchema(child);
610
+ if (child.type !== "optional") required.push(key);
611
+ }
612
+ const extra = shape.additionalProperties;
613
+ const additionalProperties = extra === true ? true : extra !== void 0 && extra !== false ? compileSchema(extra) : false;
614
+ return {
615
+ type: "object",
616
+ ...Object.keys(properties).length > 0 ? { properties } : {},
617
+ ...required.length > 0 ? { required } : {},
618
+ additionalProperties,
619
+ ...shape.description !== void 0 ? { description: shape.description } : {}
620
+ };
621
+ }
622
+ case "union": return {
623
+ ...shape.mode === "oneOf" ? { oneOf: shape.variants.map((variant) => compileSchema(variant)) } : { anyOf: shape.variants.map((variant) => compileSchema(variant)) },
624
+ ...shape.description !== void 0 ? { description: shape.description } : {}
625
+ };
626
+ case "optional": return compileSchema(shape.inner);
627
+ case "nullable": return { anyOf: [compileSchema(shape.inner), { type: "null" }] };
628
+ case "raw": return shape.schema;
629
+ }
630
+ }
631
+ /**
632
+ * Compile a {@link ContractShape} into a runtime type guard.
633
+ *
634
+ * @remarks
635
+ * Reuses the combinators: `literalOf` for literals, `arrayOf` for arrays,
636
+ * `recordOf` for closed objects, `unionOf` for unions, `nullableOf` for nullable,
637
+ * and `whereOf` for constraint refinement. Like every guard it is total — it
638
+ * never throws (AGENTS §14).
639
+ *
640
+ * @param shape - The shape to compile
641
+ * @returns A guard narrowing to the shape's inferred type
642
+ */
643
+ function compileGuard(shape) {
644
+ switch (shape.type) {
645
+ case "string": return stringOf({
646
+ min: shape.min,
647
+ max: shape.max,
648
+ pattern: shape.pattern
649
+ });
650
+ case "number": {
651
+ const base = shape.integer === true ? isInteger : isFiniteNumber;
652
+ if (shape.min === void 0 && shape.max === void 0) return base;
653
+ return shape.integer === true ? intersectionOf(isInteger, boundsOf(shape.min, shape.max)) : boundsOf(shape.min, shape.max);
654
+ }
655
+ case "boolean": return isBoolean;
656
+ case "null": return isNull;
657
+ case "json": return isJSONValue;
658
+ case "literal": return literalOf(...shape.values);
659
+ case "array": {
660
+ const base = arrayOf(compileGuard(shape.items));
661
+ if (shape.min === void 0 && shape.max === void 0) return base;
662
+ const withinLength = boundsOf(shape.min, shape.max);
663
+ return whereOf(base, (value) => withinLength(value.length));
664
+ }
665
+ case "object": {
666
+ const map = Object.create(null);
667
+ const optionalKeys = [];
668
+ for (const key of Object.keys(shape.properties)) {
669
+ const child = shape.properties[key];
670
+ if (child === void 0) continue;
671
+ if (child.type === "optional") {
672
+ map[key] = compileGuard(child.inner);
673
+ optionalKeys.push(key);
674
+ } else map[key] = compileGuard(child);
675
+ }
676
+ const extra = shape.additionalProperties;
677
+ if (extra === void 0 || extra === false) return optionalKeys.length > 0 ? recordOf(map, optionalKeys) : recordOf(map);
678
+ const additional = extra === true ? void 0 : compileGuard(extra);
679
+ const required = Object.keys(map).filter((key) => !optionalKeys.includes(key));
680
+ return (value) => {
681
+ if (!isRecord(value)) return false;
682
+ for (const key of required) if (!Object.hasOwn(value, key)) return false;
683
+ const outcome = attempt(() => {
684
+ for (const key of Object.keys(value)) {
685
+ const guard = Object.hasOwn(map, key) ? map[key] : void 0;
686
+ if (guard !== void 0) {
687
+ if (!guard(value[key])) return false;
688
+ } else if (additional !== void 0 && !additional(value[key])) return false;
689
+ }
690
+ return true;
691
+ });
692
+ return outcome.success && outcome.value;
693
+ };
694
+ }
695
+ case "union": return unionOf(...shape.variants.map((variant) => compileGuard(variant)));
696
+ case "optional": return orOf(isUndefined, compileGuard(shape.inner));
697
+ case "nullable": return nullableOf(compileGuard(shape.inner));
698
+ case "raw": return (_value) => true;
699
+ }
700
+ }
701
+ /**
702
+ * Compile a {@link ContractShape} into an input parser.
703
+ *
704
+ * @remarks
705
+ * Reuses the leaf parsers (`parseString` / `parseInteger` / `parseNumber` /
706
+ * `parseBoolean` / `parseRecord`) and coerces structurally. An object fails as a
707
+ * whole on any required-field failure; a union returns a guard-valid value
708
+ * unchanged, otherwise the first variant that both parses and guards wins.
709
+ *
710
+ * After coercing a leaf, it re-applies that leaf's REFINEMENTS through the same
711
+ * combinators `compileGuard` uses — `stringOf` for a string's length/pattern and
712
+ * `boundsOf` for a number's value and an array's length — so a value that coerces
713
+ * but violates a bound parses to `undefined`. The result is full parse↔guard
714
+ * soundness (AGENTS §14): a non-`undefined` parse always satisfies the contract's
715
+ * `is`, refinements included.
716
+ *
717
+ * @param shape - The shape to compile
718
+ * @returns A parser yielding the shape's inferred type or `undefined`
719
+ */
720
+ function compileParser(shape) {
721
+ switch (shape.type) {
722
+ case "string": {
723
+ if (shape.min === void 0 && shape.max === void 0 && shape.pattern === void 0) return parseString;
724
+ const guard = stringOf({
725
+ min: shape.min,
726
+ max: shape.max,
727
+ pattern: shape.pattern
728
+ });
729
+ return (value) => {
730
+ const parsed = parseString(value);
731
+ return parsed !== void 0 && guard(parsed) ? parsed : void 0;
732
+ };
733
+ }
734
+ case "number": {
735
+ const base = shape.integer === true ? parseInteger : parseNumber;
736
+ if (shape.min === void 0 && shape.max === void 0) return base;
737
+ const within = boundsOf(shape.min, shape.max);
738
+ return (value) => {
739
+ const parsed = base(value);
740
+ return parsed !== void 0 && within(parsed) ? parsed : void 0;
741
+ };
742
+ }
743
+ case "boolean": return parseBoolean;
744
+ case "null": return (value) => value === null ? null : void 0;
745
+ case "json": return (value) => isJSONValue(value) ? value : void 0;
746
+ case "literal": {
747
+ const allowed = new Set(shape.values);
748
+ return (value) => {
749
+ if (allowed.has(value)) return value;
750
+ if (isString(value)) {
751
+ const trimmed = value.trim();
752
+ if (allowed.has(trimmed)) return trimmed;
753
+ }
754
+ };
755
+ }
756
+ case "array": {
757
+ const item = compileParser(shape.items);
758
+ const unbounded = shape.min === void 0 && shape.max === void 0;
759
+ const withinLength = boundsOf(shape.min, shape.max);
760
+ return (value) => {
761
+ if (!isArray(value)) return void 0;
762
+ const result = [];
763
+ for (const entry of value) {
764
+ const parsed = item(entry);
765
+ if (parsed === void 0) return void 0;
766
+ result.push(parsed);
767
+ }
768
+ return unbounded || withinLength(result.length) ? result : void 0;
769
+ };
770
+ }
771
+ case "object": {
772
+ const entries = [];
773
+ for (const key of Object.keys(shape.properties)) {
774
+ const child = shape.properties[key];
775
+ if (child === void 0) continue;
776
+ const optional = child.type === "optional";
777
+ entries.push({
778
+ key,
779
+ parse: compileParser(optional ? child.inner : child),
780
+ optional
781
+ });
782
+ }
783
+ const known = new Set(entries.map((entry) => entry.key));
784
+ const extra = shape.additionalProperties;
785
+ const additional = extra === void 0 || extra === false || extra === true ? void 0 : compileParser(extra);
786
+ const open = extra === true || additional !== void 0;
787
+ return (value) => {
788
+ const record = parseRecord(value);
789
+ if (record === void 0) return void 0;
790
+ const outcome = attempt(() => {
791
+ const result = Object.create(null);
792
+ for (const entry of entries) {
793
+ const raw = record[entry.key];
794
+ if (raw === void 0) {
795
+ if (entry.optional) continue;
796
+ return;
797
+ }
798
+ const parsed = entry.parse(raw);
799
+ if (parsed === void 0) return void 0;
800
+ result[entry.key] = parsed;
801
+ }
802
+ if (open) for (const key of Object.keys(record)) {
803
+ if (known.has(key)) continue;
804
+ if (additional === void 0) result[key] = record[key];
805
+ else {
806
+ const parsed = additional(record[key]);
807
+ if (parsed === void 0) return void 0;
808
+ result[key] = parsed;
809
+ }
810
+ }
811
+ return result;
812
+ });
813
+ return outcome.success ? outcome.value : void 0;
814
+ };
815
+ }
816
+ case "union": {
817
+ const variants = shape.variants.map((variant) => ({
818
+ parse: compileParser(variant),
819
+ guard: compileGuard(variant)
820
+ }));
821
+ return (value) => {
822
+ for (const variant of variants) if (variant.guard(value)) return value;
823
+ for (const variant of variants) {
824
+ const parsed = variant.parse(value);
825
+ if (parsed !== void 0 && variant.guard(parsed)) return parsed;
826
+ }
827
+ };
828
+ }
829
+ case "optional": {
830
+ const inner = compileParser(shape.inner);
831
+ return (value) => value === void 0 ? void 0 : inner(value);
832
+ }
833
+ case "nullable": {
834
+ const inner = compileParser(shape.inner);
835
+ return (value) => value === null ? null : inner(value);
836
+ }
837
+ case "raw": return (value) => value;
838
+ }
839
+ }
840
+ /**
841
+ * Compile a {@link ContractShape} into a deterministic seed value.
842
+ *
843
+ * @remarks
844
+ * The same shape and the same `random` source always produce the same value, so
845
+ * seed data is reproducible. Defaults to a {@link seededRandom} source seeded
846
+ * from the wall clock when none is supplied. Throws on a degenerate empty
847
+ * `literalShape` / `unionShape`, on a pattern-constrained `stringShape` whose
848
+ * generated sample cannot satisfy the pattern, or on a `rawShape` (its embedded
849
+ * schema is arbitrary and cannot be auto-generated) — a programmer error that
850
+ * cannot generate a value (AGENTS §12). `createContract` runs
851
+ * {@link validateShape} first, so a degenerate `literalShape` / `unionShape` /
852
+ * bounded shape is normally caught there; these throws remain here as defense
853
+ * for standalone `compileGenerator` use.
854
+ *
855
+ * @param shape - The shape to generate from
856
+ * @param random - A seeded random source (defaults to `seededRandom(Date.now())`)
857
+ * @returns A value matching the shape
858
+ */
859
+ function compileGenerator(shape, random = seededRandom(Date.now())) {
860
+ switch (shape.type) {
861
+ case "string": {
862
+ const min = shape.min ?? 0;
863
+ const max = shape.max ?? Math.max(min, 12);
864
+ const length = Math.max(min, Math.min(max, 8));
865
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
866
+ let value = "";
867
+ for (let index = 0; index < length; index += 1) value += alphabet[Math.floor(random() * 36)];
868
+ if (shape.pattern !== void 0 && !shape.pattern.test(value)) throw new Error("compileGenerator: a pattern-constrained string shape cannot be auto-generated — supply or verify values another way");
869
+ return value;
870
+ }
871
+ case "number": {
872
+ const min = shape.min ?? 0;
873
+ const max = shape.max ?? 100;
874
+ if (shape.integer === true) {
875
+ const lo = Math.ceil(min);
876
+ const hi = Math.floor(max);
877
+ return Math.floor(random() * (hi - lo + 1)) + lo;
878
+ }
879
+ return random() * (max - min) + min;
880
+ }
881
+ case "boolean": return random() >= .5;
882
+ case "null": return null;
883
+ case "json": {
884
+ const pick = Math.floor(random() * 5);
885
+ if (pick === 0) return null;
886
+ if (pick === 1) return random() >= .5;
887
+ if (pick === 2) return Math.floor(random() * 1e3);
888
+ if (pick === 3) {
889
+ const alphabet = "abcdefghijklmnopqrstuvwxyz";
890
+ let value = "";
891
+ for (let index = 0; index < 6; index += 1) value += alphabet[Math.floor(random() * 26)];
892
+ return value;
893
+ }
894
+ return { value: Math.floor(random() * 1e3) };
895
+ }
896
+ case "literal":
897
+ if (shape.values.length === 0) throw new Error("compileGenerator: a literal shape needs at least one value");
898
+ return shape.values[Math.floor(random() * shape.values.length)];
899
+ case "array": {
900
+ const lo = shape.min ?? Math.min(1, shape.max ?? 1);
901
+ const hi = shape.max ?? Math.max(lo, 3);
902
+ const length = Math.floor(random() * (hi - lo + 1)) + lo;
903
+ const result = [];
904
+ for (let index = 0; index < length; index += 1) result.push(compileGenerator(shape.items, random));
905
+ return result;
906
+ }
907
+ case "object": {
908
+ const result = {};
909
+ for (const key of Object.keys(shape.properties)) {
910
+ const child = shape.properties[key];
911
+ if (child === void 0) continue;
912
+ if (child.type === "optional" && random() < .3) continue;
913
+ result[key] = compileGenerator(child, random);
914
+ }
915
+ const extra = shape.additionalProperties;
916
+ if (extra !== void 0 && extra !== true && extra !== false) {
917
+ const count = 1 + Math.floor(random() * 2);
918
+ for (let index = 0; index < count; index += 1) {
919
+ const key = `key${index}`;
920
+ if (Object.hasOwn(result, key)) continue;
921
+ result[key] = compileGenerator(extra, random);
922
+ }
923
+ }
924
+ return result;
925
+ }
926
+ case "union":
927
+ if (shape.variants.length === 0) throw new Error("compileGenerator: a union shape needs at least one variant");
928
+ return compileGenerator(shape.variants[Math.floor(random() * shape.variants.length)], random);
929
+ case "optional": return compileGenerator(shape.inner, random);
930
+ case "nullable": return random() < .2 ? null : compileGenerator(shape.inner, random);
931
+ case "raw": throw new Error("compileGenerator: a raw shape embeds an arbitrary JSON Schema and cannot be auto-generated — supply values another way");
932
+ }
933
+ }
934
+ function createContract(shape) {
935
+ validateShape(shape);
936
+ const schema = compileSchema(shape);
937
+ const guard = compileGuard(shape);
938
+ const parser = compileParser(shape);
939
+ return {
940
+ schema,
941
+ is: guard,
942
+ parse(value) {
943
+ return parser(value);
944
+ },
945
+ generate(random) {
946
+ return compileGenerator(shape, random);
947
+ }
948
+ };
949
+ }
950
+ /**
951
+ * Build a string {@link StringShape}.
952
+ *
953
+ * @param options - Optional length (`min` / `max`), `pattern`, and `description`
954
+ * @returns A string shape
955
+ *
956
+ * @example
957
+ * ```ts
958
+ * const name = stringShape({ min: 1, max: 80, description: 'Display name' })
959
+ * ```
960
+ */
961
+ function stringShape(options) {
962
+ return {
963
+ type: "string",
964
+ min: options?.min,
965
+ max: options?.max,
966
+ pattern: options?.pattern,
967
+ description: options?.description
968
+ };
969
+ }
970
+ /**
971
+ * Build an integer {@link NumberShape} — forces `integer: true`.
972
+ *
973
+ * @remarks
974
+ * The emitted JSON Schema uses `"type": "integer"` and the guard rejects
975
+ * fractional numbers.
976
+ *
977
+ * @param options - Optional bounds and `description` (no `integer` key)
978
+ * @returns An integer number shape
979
+ */
980
+ function integerShape(options) {
981
+ return {
982
+ type: "number",
983
+ integer: true,
984
+ min: options?.min,
985
+ max: options?.max,
986
+ description: options?.description
987
+ };
988
+ }
989
+ /**
990
+ * Build a {@link BooleanShape}.
991
+ *
992
+ * @param options - Optional `description`
993
+ * @returns A boolean shape
994
+ */
995
+ function booleanShape(options) {
996
+ return {
997
+ type: "boolean",
998
+ description: options?.description
999
+ };
1000
+ }
1001
+ /**
1002
+ * Build a literal shape from a fixed set of primitive values.
1003
+ *
1004
+ * @param values - The permitted literals
1005
+ * @param options - Optional `description`
1006
+ * @returns A literal shape whose `Infer` is the union of `values`
1007
+ *
1008
+ * @example
1009
+ * ```ts
1010
+ * const role = literalShape(['admin', 'member', 'guest'])
1011
+ * // Infer<typeof role> = 'admin' | 'member' | 'guest'
1012
+ *
1013
+ * const via = literalShape(['function', 'tool', 'agent'], { description: 'How to run the step.' })
1014
+ * ```
1015
+ */
1016
+ function literalShape(values, options) {
1017
+ return {
1018
+ type: "literal",
1019
+ values,
1020
+ description: options?.description
1021
+ };
1022
+ }
1023
+ /**
1024
+ * Build an {@link ObjectShape} from a property map.
1025
+ *
1026
+ * @remarks
1027
+ * Wrap any property in {@link optionalShape} to allow its absence. By default
1028
+ * the compiled guard rejects unknown keys; pass `additionalProperties` to open
1029
+ * the object.
1030
+ *
1031
+ * @param properties - Map of property names to child shapes
1032
+ * @param options - Optional `additionalProperties` and `description`
1033
+ * @returns An object shape
1034
+ *
1035
+ * @example
1036
+ * ```ts
1037
+ * const user = objectShape({
1038
+ * name: stringShape({ min: 1 }),
1039
+ * age: integerShape({ min: 0, max: 120 }),
1040
+ * bio: optionalShape(stringShape()),
1041
+ * })
1042
+ * ```
1043
+ */
1044
+ function objectShape(properties, options) {
1045
+ return {
1046
+ type: "object",
1047
+ properties,
1048
+ additionalProperties: options?.additionalProperties,
1049
+ description: options?.description
1050
+ };
1051
+ }
1052
+ /**
1053
+ * Wrap a shape so it may be absent (`undefined`).
1054
+ *
1055
+ * @remarks
1056
+ * As an {@link objectShape} property, the field becomes a true optional property
1057
+ * in the inferred type.
1058
+ *
1059
+ * @param inner - The wrapped shape
1060
+ * @returns An optional shape
1061
+ */
1062
+ function optionalShape(inner) {
1063
+ return {
1064
+ type: "optional",
1065
+ inner
1066
+ };
1067
+ }
1068
+ //#endregion
1069
+ //#region src/core/validators.ts
1070
+ /**
1071
+ * Whether `character` is an inline whitespace character (space / tab / newline) - the
1072
+ * emphasis flanking rule's space test.
1073
+ *
1074
+ * @param character - The character to test
1075
+ * @returns `true` when it is inline whitespace
1076
+ */
1077
+ function isWhitespace(character) {
1078
+ return character === " " || character === " " || character === "\n";
1079
+ }
1080
+ /**
1081
+ * Whether `character` is escapable by a leading backslash - the ASCII punctuation
1082
+ * markdown gives meaning to (so `\*` becomes `*` but `\.` stays `\.`).
1083
+ *
1084
+ * @param character - The single character after a backslash
1085
+ * @returns `true` when a backslash before it is an escape
1086
+ */
1087
+ function isEscapable(character) {
1088
+ return /[\\`*_{}[\]()#+\-.!>~|]/.test(character);
1089
+ }
1090
+ /**
1091
+ * Whether `line` is blank - empty, or containing only whitespace - the markdown
1092
+ * definition of a blank line that block parsing uses to separate paragraphs, skip
1093
+ * gaps, and end list continuations.
1094
+ *
1095
+ * @param line - The candidate line
1096
+ * @returns `true` when the line is blank
1097
+ */
1098
+ function isBlankLine(line) {
1099
+ return isEmptyString(line.trim());
1100
+ }
1101
+ /**
1102
+ * Whether `line` is a blockquote line (`>` optionally indented up to three spaces) -
1103
+ * its content is de-quoted by {@link stripQuote}.
1104
+ *
1105
+ * @param line - The candidate line
1106
+ * @returns `true` when the line begins a blockquote
1107
+ */
1108
+ function isQuote(line) {
1109
+ return /^\s{0,3}>/.test(line);
1110
+ }
1111
+ /**
1112
+ * Whether `line` closes a fence opened by `marker` - the same fence character, a run
1113
+ * at least as long, and nothing else but surrounding whitespace.
1114
+ *
1115
+ * @param line - The candidate closing line
1116
+ * @param marker - The opening fence's marker run (from {@link extractFence})
1117
+ * @returns `true` when `line` closes the fence
1118
+ */
1119
+ function isFenceClose(line, marker) {
1120
+ const character = marker[0] === "~" ? "~" : "`";
1121
+ let index = 0;
1122
+ while (index < line.length && isFenceWhitespace(line[index])) index++;
1123
+ let run = 0;
1124
+ while (index < line.length && line[index] === character) {
1125
+ run++;
1126
+ index++;
1127
+ }
1128
+ if (run < marker.length) return false;
1129
+ while (index < line.length && isFenceWhitespace(line[index])) index++;
1130
+ return index === line.length;
1131
+ }
1132
+ /**
1133
+ * Whether `character` is a regex-`\s`-equivalent whitespace character - the
1134
+ * character class {@link isFenceClose}'s scan treats as surrounding padding.
1135
+ *
1136
+ * @param character - The single character to test, or `undefined` past the end of a line
1137
+ * @returns `true` when it is whitespace
1138
+ */
1139
+ function isFenceWhitespace(character) {
1140
+ return character === " " || character === " " || character === "\n" || character === "\r" || character === "\f" || character === "\v";
1141
+ }
1142
+ /**
1143
+ * Whether `line` is a thematic break (horizontal rule) - three or more of the SAME
1144
+ * marker `-`, `*`, or `_` (optionally space-separated) and nothing else (`---`,
1145
+ * `***`, `___`, `- - -`).
1146
+ *
1147
+ * @param line - The candidate line
1148
+ * @returns `true` when the line is a thematic break
1149
+ */
1150
+ function isThematicBreak(line) {
1151
+ const stripped = line.trim().replace(/\s+/g, "");
1152
+ if (stripped.length < 3) return false;
1153
+ const marker = stripped[0];
1154
+ if (marker !== "-" && marker !== "*" && marker !== "_") return false;
1155
+ return [...stripped].every((character) => character === marker);
1156
+ }
1157
+ /**
1158
+ * Whether the pair (`header`, `delimiter`) opens a GFM table - `delimiter` is a row of
1159
+ * `|`-separated cells each matching `:?-+:?`, the GFM rule that a table requires a
1160
+ * header row IMMEDIATELY followed by a delimiter row.
1161
+ *
1162
+ * @param header - The candidate header line
1163
+ * @param delimiter - The line after it (the candidate delimiter)
1164
+ * @returns `true` when the two lines open a table
1165
+ */
1166
+ function isTableStart(header, delimiter) {
1167
+ if (delimiter === void 0 || !header.includes("|")) return false;
1168
+ const cells = splitTableRow(delimiter);
1169
+ if (cells.length === 0) return false;
1170
+ return cells.every((cell) => /^:?-+:?$/.test(cell.trim()));
1171
+ }
1172
+ /** Determine whether a node is a heading block. */
1173
+ function isHeadingNode(node) {
1174
+ return node.element === "heading";
1175
+ }
1176
+ /** Determine whether a node is a paragraph block. */
1177
+ function isParagraphNode(node) {
1178
+ return node.element === "paragraph";
1179
+ }
1180
+ /** Determine whether a node is a list block. */
1181
+ function isListNode(node) {
1182
+ return node.element === "list";
1183
+ }
1184
+ /** Determine whether a node is a GFM table block. */
1185
+ function isTableNode(node) {
1186
+ return node.element === "table";
1187
+ }
1188
+ /** Determine whether a node is a fenced code block. */
1189
+ function isCodeBlockNode(node) {
1190
+ return node.element === "codeBlock";
1191
+ }
1192
+ /** Determine whether a node is a blockquote block. */
1193
+ function isBlockquoteNode(node) {
1194
+ return node.element === "blockquote";
1195
+ }
1196
+ /** Determine whether a node is a thematic break (horizontal rule) block. */
1197
+ function isThematicBreakNode(node) {
1198
+ return node.element === "thematicBreak";
1199
+ }
1200
+ /** Determine whether a node is a plain text run. */
1201
+ function isTextNode(node) {
1202
+ return node.element === "text";
1203
+ }
1204
+ /** Determine whether a node is an emphasis run (`*em*` / `**strong**`). */
1205
+ function isEmphasisNode(node) {
1206
+ return node.element === "emphasis";
1207
+ }
1208
+ /**
1209
+ * Determine whether a node is an inline code span.
1210
+ *
1211
+ * @remarks
1212
+ * Narrows to {@link CodeSpanNode} - the node whose `element` discriminant is
1213
+ * `'codeSpan'`.
1214
+ */
1215
+ function isCodeSpanNode(node) {
1216
+ return node.element === "codeSpan";
1217
+ }
1218
+ /** Determine whether a node is a link. */
1219
+ function isLinkNode(node) {
1220
+ return node.element === "link";
1221
+ }
1222
+ /**
1223
+ * Determine whether an arbitrary value is a valid {@link InlineNode} - a text
1224
+ * run, emphasis, code span, or link, recursively validated.
1225
+ *
1226
+ * @remarks
1227
+ * Total: never throws, even on cyclic or pathologically deep input - every
1228
+ * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
1229
+ * throw-contained per the `@orkestrel/contract` guard contract (AGENTS §14).
1230
+ *
1231
+ * @param value - The value to test
1232
+ * @returns `true` when `value` is a well-formed {@link InlineNode}
1233
+ *
1234
+ * @example
1235
+ * ```ts
1236
+ * import { isInlineNode } from '@orkestrel/markdown'
1237
+ *
1238
+ * isInlineNode({ element: 'text', value: 'hi' }) // true
1239
+ * isInlineNode({ element: 'text' }) // false - missing `value`
1240
+ * ```
1241
+ */
1242
+ var isInlineNode = unionOf(recordOf({
1243
+ element: literalOf("text"),
1244
+ value: isString
1245
+ }), recordOf({
1246
+ element: literalOf("emphasis"),
1247
+ strong: isBoolean,
1248
+ children: arrayOf(lazyOf(() => isInlineNode))
1249
+ }), recordOf({
1250
+ element: literalOf("codeSpan"),
1251
+ value: isString
1252
+ }), recordOf({
1253
+ element: literalOf("link"),
1254
+ href: isString,
1255
+ children: arrayOf(lazyOf(() => isInlineNode))
1256
+ }));
1257
+ /**
1258
+ * Determine whether an arbitrary value is a valid {@link BlockNode} - a
1259
+ * heading, paragraph, list, table, code block, blockquote, or thematic break,
1260
+ * recursively validated.
1261
+ *
1262
+ * @remarks
1263
+ * Total: never throws, even on cyclic or pathologically deep input - every
1264
+ * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
1265
+ * throw-contained per the `@orkestrel/contract` guard contract (AGENTS §14).
1266
+ * A list item's shape is inlined here (and in {@link isMarkdownNode}) rather
1267
+ * than named separately - it is used at exactly these two sites.
1268
+ *
1269
+ * @param value - The value to test
1270
+ * @returns `true` when `value` is a well-formed {@link BlockNode}
1271
+ *
1272
+ * @example
1273
+ * ```ts
1274
+ * import { isBlockNode } from '@orkestrel/markdown'
1275
+ *
1276
+ * isBlockNode({ element: 'thematicBreak' }) // true
1277
+ * isBlockNode({ element: 'heading' }) // false - missing `level` / `children`
1278
+ * ```
1279
+ */
1280
+ var isBlockNode = unionOf(recordOf({
1281
+ element: literalOf("heading"),
1282
+ level: isNumber,
1283
+ children: arrayOf(isInlineNode)
1284
+ }), recordOf({
1285
+ element: literalOf("paragraph"),
1286
+ children: arrayOf(isInlineNode)
1287
+ }), recordOf({
1288
+ element: literalOf("list"),
1289
+ ordered: isBoolean,
1290
+ start: isNumber,
1291
+ items: arrayOf(recordOf({
1292
+ element: literalOf("listItem"),
1293
+ children: arrayOf(lazyOf(() => isBlockNode))
1294
+ }))
1295
+ }), recordOf({
1296
+ element: literalOf("table"),
1297
+ header: arrayOf(arrayOf(isInlineNode)),
1298
+ rows: arrayOf(arrayOf(arrayOf(isInlineNode))),
1299
+ align: arrayOf(literalOf("none", "left", "right", "center"))
1300
+ }), recordOf({
1301
+ element: literalOf("codeBlock"),
1302
+ lang: isString,
1303
+ code: isString
1304
+ }, ["lang"]), recordOf({
1305
+ element: literalOf("blockquote"),
1306
+ children: arrayOf(lazyOf(() => isBlockNode))
1307
+ }), recordOf({ element: literalOf("thematicBreak") }));
1308
+ /**
1309
+ * Determine whether an arbitrary value is a valid {@link MarkdownNode} - the
1310
+ * {@link MarkdownDocument} root, a {@link BlockNode}, a {@link ListItemNode}, or
1311
+ * an {@link InlineNode}, recursively validated.
1312
+ *
1313
+ * @remarks
1314
+ * Total: never throws, even on cyclic or pathologically deep input - every
1315
+ * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
1316
+ * throw-contained per the `@orkestrel/contract` guard contract (AGENTS §14).
1317
+ * A list item's shape is inlined here (and in {@link isBlockNode}) rather than
1318
+ * named separately - it is used at exactly these two sites.
1319
+ *
1320
+ * @param value - The value to test
1321
+ * @returns `true` when `value` is a well-formed {@link MarkdownNode}
1322
+ *
1323
+ * @example
1324
+ * ```ts
1325
+ * import { isMarkdownNode } from '@orkestrel/markdown'
1326
+ *
1327
+ * isMarkdownNode({ element: 'text', value: 'hi' }) // true
1328
+ * isMarkdownNode({ element: 'bogus' }) // false
1329
+ * ```
1330
+ */
1331
+ var isMarkdownNode = unionOf(lazyOf(() => isMarkdownDocument), lazyOf(() => isBlockNode), recordOf({
1332
+ element: literalOf("listItem"),
1333
+ children: arrayOf(lazyOf(() => isBlockNode))
1334
+ }), lazyOf(() => isInlineNode));
1335
+ /**
1336
+ * Determine whether an arbitrary value is a valid {@link MarkdownDocument} -
1337
+ * the parsed-AST root {@link parseDocument} returns, recursively
1338
+ * validated.
1339
+ *
1340
+ * @remarks
1341
+ * Total: never throws, even on cyclic or pathologically deep input - every
1342
+ * combinator involved (`recordOf`, `arrayOf`) is throw-contained per the
1343
+ * `@orkestrel/contract` guard contract (AGENTS §14).
1344
+ *
1345
+ * @param value - The value to test
1346
+ * @returns `true` when `value` is a well-formed {@link MarkdownDocument}
1347
+ *
1348
+ * @example
1349
+ * ```ts
1350
+ * import { isMarkdownDocument } from '@orkestrel/markdown'
1351
+ *
1352
+ * isMarkdownDocument({ element: 'document', children: [] }) // true
1353
+ * isMarkdownDocument({ element: 'document' }) // false - missing `children`
1354
+ * ```
1355
+ */
1356
+ var isMarkdownDocument = recordOf({
1357
+ element: literalOf("document"),
1358
+ children: arrayOf(isBlockNode)
1359
+ });
1360
+ //#endregion
1361
+ //#region src/core/helpers.ts
1362
+ /**
1363
+ * Normalize line endings to `\n` and split a markdown document into its lines - CRLF
1364
+ * (`\r\n`) and bare CR (`\r`) both collapse to `\n` first, so a Windows-origin
1365
+ * document parses identically. A single trailing newline does not yield a final
1366
+ * empty line.
1367
+ *
1368
+ * @param markdown - The raw markdown source
1369
+ * @returns The document's lines, line-terminators stripped
1370
+ */
1371
+ function splitLines(markdown) {
1372
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
1373
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1374
+ return lines;
1375
+ }
1376
+ /**
1377
+ * The count of leading space / tab characters on `line` (a tab counts as one) - the
1378
+ * indent that decides whether a list item's continuation belongs to the item.
1379
+ *
1380
+ * @param line - The line to measure
1381
+ * @returns The number of leading space / tab characters
1382
+ */
1383
+ function leadingIndent(line) {
1384
+ let count = 0;
1385
+ for (const character of line) if (character === " " || character === " ") count += 1;
1386
+ else break;
1387
+ return count;
1388
+ }
1389
+ /**
1390
+ * Extract an ATX heading line (`#` … `######` followed by text) into its
1391
+ * `{ level, text }`, or `undefined` when `line` is not a heading. A run of more than 6
1392
+ * `#`s, or `#`s not followed by whitespace + text, is not a
1393
+ * heading; an optional closing `###` run is stripped.
1394
+ *
1395
+ * @param line - The candidate line
1396
+ * @returns The heading level (1–6) and its raw inline text, or `undefined`
1397
+ */
1398
+ function extractHeading(line) {
1399
+ const match = /^(#{1,6})(?:\s+(.*))?$/.exec(line.trimStart());
1400
+ if (!match || match[1] === void 0) return void 0;
1401
+ return {
1402
+ level: match[1].length,
1403
+ text: (match[2] ?? "").replace(/\s+#+\s*$/, "").trim()
1404
+ };
1405
+ }
1406
+ /**
1407
+ * Extract a fenced-code opening line (```` ``` ```` or `~~~`, optionally with an info
1408
+ * string) into its `{ marker, lang }`, or `undefined` when `line` is not a fence
1409
+ * opener. `marker` is the exact fence run (the closer must match the same character +
1410
+ * at least the same length); `lang` is the first word of the info string.
1411
+ *
1412
+ * @param line - The candidate line
1413
+ * @returns The fence marker run and its language tag, or `undefined`
1414
+ */
1415
+ function extractFence(line) {
1416
+ const match = /^\s*(`{3,}|~{3,})\s*(.*)$/.exec(line);
1417
+ if (!match || match[1] === void 0) return void 0;
1418
+ const info = (match[2] ?? "").trim();
1419
+ if (match[1].startsWith("`") && info.includes("`")) return void 0;
1420
+ const lang = isNonEmptyString(info) ? info.split(/\s+/)[0] : void 0;
1421
+ return {
1422
+ marker: match[1],
1423
+ lang
1424
+ };
1425
+ }
1426
+ /**
1427
+ * Extract a list-item line (`-` / `*` / `+` bullet, or `1.` / `1)` ordinal, followed by
1428
+ * a space) into its {@link ListItemParts}, or `undefined` when `line` is not a list
1429
+ * item. `content` is the text after the marker; `marker` is the full marker-plus-space
1430
+ * width (for measuring a continuation's indent).
1431
+ *
1432
+ * @param line - The candidate line
1433
+ * @returns The list-item parts, or `undefined` when not a list item
1434
+ */
1435
+ function extractListItem(line) {
1436
+ const unordered = /^(\s*)([-*+])\s+(.*)$/.exec(line);
1437
+ if (unordered && unordered[1] !== void 0) {
1438
+ const indent = unordered[1].length;
1439
+ const content = unordered[3] ?? "";
1440
+ return {
1441
+ ordered: false,
1442
+ start: 1,
1443
+ content,
1444
+ indent,
1445
+ marker: line.length - content.length
1446
+ };
1447
+ }
1448
+ const ordered = /^(\s*)(\d{1,9})[.)]\s+(.*)$/.exec(line);
1449
+ if (ordered && ordered[1] !== void 0 && ordered[2] !== void 0) {
1450
+ const indent = ordered[1].length;
1451
+ const content = ordered[3] ?? "";
1452
+ return {
1453
+ ordered: true,
1454
+ start: parseInteger(ordered[2]) ?? 1,
1455
+ content,
1456
+ indent,
1457
+ marker: line.length - content.length
1458
+ };
1459
+ }
1460
+ }
1461
+ /**
1462
+ * Strip one level of blockquote marker (`>` plus one optional following space) from a
1463
+ * blockquote line, so the de-quoted lines re-parse as nested blocks.
1464
+ *
1465
+ * @param line - A blockquote line (per {@link isQuote})
1466
+ * @returns The line with its leading `>` (and one space) removed
1467
+ */
1468
+ function stripQuote(line) {
1469
+ return line.replace(/^\s{0,3}>\s?/, "");
1470
+ }
1471
+ /**
1472
+ * Split one GFM table row into its cell strings - outer pipes are optional, an escaped
1473
+ * pipe (`\|`) inside a cell is NOT a separator (it becomes a literal `|`), and the
1474
+ * empty leading / trailing cell produced by an outer `|` is dropped.
1475
+ *
1476
+ * @param row - The raw table row line
1477
+ * @returns The row's cells, in column order
1478
+ */
1479
+ function splitTableRow(row) {
1480
+ const cells = [];
1481
+ let current = "";
1482
+ const trimmed = row.trim();
1483
+ for (let index = 0; index < trimmed.length; index += 1) {
1484
+ const character = trimmed[index];
1485
+ if (character === "\\" && trimmed[index + 1] === "|") {
1486
+ current += "|";
1487
+ index += 1;
1488
+ } else if (character === "|") {
1489
+ cells.push(current);
1490
+ current = "";
1491
+ } else current += character;
1492
+ }
1493
+ cells.push(current);
1494
+ if (isNonEmptyArray(cells) && isEmptyString((cells[0] ?? "").trim())) cells.shift();
1495
+ if (isNonEmptyArray(cells) && isEmptyString((cells[cells.length - 1] ?? "").trim())) cells.pop();
1496
+ return cells;
1497
+ }
1498
+ /**
1499
+ * Derive the per-column {@link TableAlign} list from a GFM delimiter row - `:---`
1500
+ * left, `---:` right, `:---:` center, `---` none.
1501
+ *
1502
+ * @param delimiter - The table's delimiter row
1503
+ * @returns One alignment per column, in column order
1504
+ */
1505
+ function tableAlignments(delimiter) {
1506
+ return splitTableRow(delimiter).map((cell) => {
1507
+ const text = cell.trim();
1508
+ const left = text.startsWith(":");
1509
+ const right = text.endsWith(":");
1510
+ if (left && right) return "center";
1511
+ if (right) return "right";
1512
+ if (left) return "left";
1513
+ return "none";
1514
+ });
1515
+ }
1516
+ /**
1517
+ * Whether the line at `index` starts a NEW block kind (heading / fence / thematic
1518
+ * break / blockquote / list / table) - the paragraph collector stops at such a line
1519
+ * so a block following a paragraph without a blank line still parses (a trusted-input
1520
+ * caller writing a `##` heading directly under a paragraph, with no intervening blank
1521
+ * line).
1522
+ *
1523
+ * @param lines - The document's lines
1524
+ * @param index - The line index to test
1525
+ * @returns `true` when the line begins a different block
1526
+ */
1527
+ function startsBlock(lines, index) {
1528
+ const line = lines[index] ?? "";
1529
+ return extractHeading(line) !== void 0 || extractFence(line) !== void 0 || isThematicBreak(line) || isQuote(line) || extractListItem(line) !== void 0 || isTableStart(line, lines[index + 1]);
1530
+ }
1531
+ /**
1532
+ * Resolve backslash escapes in a raw string to their literal characters - used for a
1533
+ * link `href` (which is not otherwise inline-parsed) and any plain text run.
1534
+ *
1535
+ * @param text - The raw text possibly carrying `\x` escapes
1536
+ * @returns The text with escapable `\x` reduced to `x`
1537
+ */
1538
+ function unescapeText(text) {
1539
+ let out = "";
1540
+ for (let index = 0; index < text.length; index += 1) {
1541
+ const character = text[index] ?? "";
1542
+ if (character === "\\" && isEscapable(text[index + 1] ?? "")) {
1543
+ out += text[index + 1] ?? "";
1544
+ index += 1;
1545
+ } else out += character;
1546
+ }
1547
+ return out;
1548
+ }
1549
+ /**
1550
+ * Merge adjacent text nodes into one - the inline scanner emits a text node per
1551
+ * unrecognized character, so coalescing keeps the AST clean and assertion-friendly.
1552
+ *
1553
+ * @param nodes - The inline nodes (possibly with adjacent text runs)
1554
+ * @returns The nodes with consecutive text nodes concatenated
1555
+ */
1556
+ function coalesceText(nodes) {
1557
+ const out = [];
1558
+ for (const node of nodes) {
1559
+ const last = out[out.length - 1];
1560
+ if (node.element === "text" && last !== void 0 && last.element === "text") out[out.length - 1] = {
1561
+ element: "text",
1562
+ value: last.value + node.value
1563
+ };
1564
+ else out.push(node);
1565
+ }
1566
+ return out;
1567
+ }
1568
+ /**
1569
+ * Scan an inline code span at `start` (a `` ` ``-run … a matching `` ` ``-run of the
1570
+ * SAME length, the CommonMark rule that lets a span contain backticks). Returns the
1571
+ * span's literal text + end index, or `undefined` when no matching closer exists (it
1572
+ * then degrades to literal backticks).
1573
+ *
1574
+ * @param source - The inline source text
1575
+ * @param start - The index of the opening backtick
1576
+ * @param to - The exclusive end of the scan window
1577
+ * @returns The span text + end index, or `undefined`
1578
+ */
1579
+ function scanCode(source, start, to) {
1580
+ let run = 0;
1581
+ while (start + run < to && source[start + run] === "`") run += 1;
1582
+ const open = "`".repeat(run);
1583
+ let search = start + run;
1584
+ for (;;) {
1585
+ const closeAt = source.indexOf(open, search);
1586
+ if (closeAt === -1 || closeAt + run > to) return void 0;
1587
+ if (source[closeAt - 1] !== "`" && source[closeAt + run] !== "`") {
1588
+ let value = source.slice(start + run, closeAt);
1589
+ if (value.length > 2 && value.startsWith(" ") && value.endsWith(" ") && value.trim().length > 0) value = value.slice(1, -1);
1590
+ return {
1591
+ value,
1592
+ end: closeAt + run
1593
+ };
1594
+ }
1595
+ search = closeAt + 1;
1596
+ }
1597
+ }
1598
+ /**
1599
+ * Scan a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1600
+ * must immediately follow and the destination runs to the matching `)` (both respect
1601
+ * nested delimiters + escapes). Returns the link node, or `undefined` when the shape
1602
+ * does not hold (it then degrades to a literal `[`).
1603
+ *
1604
+ * @param source - The inline source text
1605
+ * @param start - The index of the opening `[`
1606
+ * @param to - The exclusive end of the scan window
1607
+ * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1608
+ * at {@link MAX_DEPTH} the link's text children degrade to literal text instead of
1609
+ * recursing further
1610
+ * @returns The parsed {@link LinkNode} + end index, or `undefined`
1611
+ */
1612
+ function scanLink(source, start, to, depth = 0) {
1613
+ let bracketDepth = 0;
1614
+ let close = -1;
1615
+ for (let index = start; index < to; index += 1) {
1616
+ const character = source[index] ?? "";
1617
+ if (character === "\\") {
1618
+ index += 1;
1619
+ continue;
1620
+ }
1621
+ if (character === "[") bracketDepth += 1;
1622
+ else if (character === "]") {
1623
+ bracketDepth -= 1;
1624
+ if (bracketDepth === 0) {
1625
+ close = index;
1626
+ break;
1627
+ }
1628
+ }
1629
+ }
1630
+ if (close === -1 || source[close + 1] !== "(") return void 0;
1631
+ let parenDepth = 0;
1632
+ let parenClose = -1;
1633
+ for (let index = close + 1; index < to; index += 1) {
1634
+ const character = source[index] ?? "";
1635
+ if (character === "\\") {
1636
+ index += 1;
1637
+ continue;
1638
+ }
1639
+ if (character === "(") parenDepth += 1;
1640
+ else if (character === ")") {
1641
+ parenDepth -= 1;
1642
+ if (parenDepth === 0) {
1643
+ parenClose = index;
1644
+ break;
1645
+ }
1646
+ }
1647
+ }
1648
+ if (parenClose === -1) return void 0;
1649
+ return {
1650
+ node: {
1651
+ element: "link",
1652
+ href: unescapeText(source.slice(close + 2, parenClose).trim()),
1653
+ children: scanInline(source, start + 1, close, depth + 1)
1654
+ },
1655
+ end: parenClose + 1
1656
+ };
1657
+ }
1658
+ /**
1659
+ * Scan an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1660
+ * matching closing run of the same marker + width, requiring non-space immediately
1661
+ * inside both delimiters (the CommonMark flanking simplification that blocks `* x *`).
1662
+ * Returns the emphasis node, or `undefined` when no valid closer exists (it then
1663
+ * degrades to a literal marker).
1664
+ *
1665
+ * @param source - The inline source text
1666
+ * @param start - The index of the opening marker
1667
+ * @param to - The exclusive end of the scan window
1668
+ * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1669
+ * at {@link MAX_DEPTH} the emphasis's children degrade to literal text instead of
1670
+ * recursing further
1671
+ * @returns The parsed {@link EmphasisNode} + end index, or `undefined`
1672
+ */
1673
+ function scanEmphasis(source, start, to, depth = 0) {
1674
+ const marker = source[start] ?? "";
1675
+ let run = 0;
1676
+ while (start + run < to && source[start + run] === marker && run < 2) run += 1;
1677
+ const strong = run === 2;
1678
+ const openEnd = start + run;
1679
+ if (openEnd >= to || isWhitespace(source[openEnd] ?? "")) return void 0;
1680
+ let index = openEnd;
1681
+ while (index < to) {
1682
+ const character = source[index] ?? "";
1683
+ if (character === "\\") {
1684
+ index += 2;
1685
+ continue;
1686
+ }
1687
+ if (character === "`") {
1688
+ const span = scanCode(source, index, to);
1689
+ index = span ? span.end : index + 1;
1690
+ continue;
1691
+ }
1692
+ if (character === marker) {
1693
+ let closeRun = 0;
1694
+ while (index + closeRun < to && source[index + closeRun] === marker) closeRun += 1;
1695
+ if (closeRun >= run && !isWhitespace(source[index - 1] ?? "")) return {
1696
+ node: {
1697
+ element: "emphasis",
1698
+ strong,
1699
+ children: scanInline(source, openEnd, index, depth + 1)
1700
+ },
1701
+ end: index + run
1702
+ };
1703
+ index += closeRun;
1704
+ continue;
1705
+ }
1706
+ index += 1;
1707
+ }
1708
+ }
1709
+ /**
1710
+ * Scan the window `[from, to)` of `source` into inline nodes - the single recursive
1711
+ * engine the inline phase runs on (emphasis / link text recurse through it). Linear:
1712
+ * each character is consumed once; a failed construct emits its opening character as
1713
+ * text and advances by one, so there is no re-scan (no ReDoS).
1714
+ *
1715
+ * @param source - The inline source text
1716
+ * @param from - The inclusive start of the scan window
1717
+ * @param to - The exclusive end of the scan window
1718
+ * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1719
+ * incremented by one on every recursive descent through {@link scanLink} /
1720
+ * {@link scanEmphasis}. At {@link MAX_DEPTH} the window is never scanned for markup -
1721
+ * it emits as a single literal text node - so pathological nesting (`[[[[…`,
1722
+ * `****…`) cannot exhaust the call stack.
1723
+ * @returns The parsed inline nodes (NOT yet coalesced)
1724
+ */
1725
+ function scanInline(source, from, to, depth = 0) {
1726
+ if (depth >= 64) return from < to ? [{
1727
+ element: "text",
1728
+ value: source.slice(from, to)
1729
+ }] : [];
1730
+ const nodes = [];
1731
+ let index = from;
1732
+ let pending = "";
1733
+ const flush = () => {
1734
+ if (pending.length > 0) {
1735
+ nodes.push({
1736
+ element: "text",
1737
+ value: pending
1738
+ });
1739
+ pending = "";
1740
+ }
1741
+ };
1742
+ while (index < to) {
1743
+ const character = source[index] ?? "";
1744
+ if (character === "\\" && index + 1 < to && isEscapable(source[index + 1] ?? "")) {
1745
+ pending += source[index + 1] ?? "";
1746
+ index += 2;
1747
+ continue;
1748
+ }
1749
+ if (character === "`") {
1750
+ const span = scanCode(source, index, to);
1751
+ if (span) {
1752
+ flush();
1753
+ nodes.push({
1754
+ element: "codeSpan",
1755
+ value: span.value
1756
+ });
1757
+ index = span.end;
1758
+ continue;
1759
+ }
1760
+ }
1761
+ if (character === "[") {
1762
+ const link = scanLink(source, index, to, depth);
1763
+ if (link) {
1764
+ flush();
1765
+ nodes.push(link.node);
1766
+ index = link.end;
1767
+ continue;
1768
+ }
1769
+ }
1770
+ if (character === "*" || character === "_") {
1771
+ const emphasis = scanEmphasis(source, index, to, depth);
1772
+ if (emphasis) {
1773
+ flush();
1774
+ nodes.push(emphasis.node);
1775
+ index = emphasis.end;
1776
+ continue;
1777
+ }
1778
+ }
1779
+ pending += character;
1780
+ index += 1;
1781
+ }
1782
+ flush();
1783
+ return nodes;
1784
+ }
1785
+ /**
1786
+ * HTML-escape text content - `&` / `<` / `>` / `"` / `'` to their entities - so text
1787
+ * from a markdown document can never inject markup. The renderer applies this to every
1788
+ * text run, code body, and (escaped further) attribute value.
1789
+ *
1790
+ * @param text - The raw text
1791
+ * @returns The HTML-escaped text
1792
+ */
1793
+ function escapeHtml(text) {
1794
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1795
+ }
1796
+ /**
1797
+ * Sanitize + HTML-attribute-escape a link `href` - a destination whose scheme is not
1798
+ * in {@link SAFE_URL_SCHEMES} (notably `javascript:` / `data:` / `vbscript:`), or that
1799
+ * is protocol-relative (`//host/path`, or a backslash variant a browser normalizes to
1800
+ * the same effect - `\\host`, `/\host`, `\/host` - inherits whatever scheme the
1801
+ * embedding page is served over, including an unsafe one), is dropped to an empty
1802
+ * string; a relative / anchor / scheme-less (and non-protocol-relative) destination
1803
+ * (including a SINGLE leading `/` or `\`) is kept;
1804
+ * the surviving value is then HTML-escaped. Defence-in-depth against an XSS `href`,
1805
+ * even though the input is trusted.
1806
+ *
1807
+ * @param href - The raw link destination
1808
+ * @returns A safe, escaped `href` (empty when the scheme is unsafe or protocol-relative)
1809
+ */
1810
+ function sanitizeUrl(href) {
1811
+ let cleaned = "";
1812
+ for (const character of href) {
1813
+ const code = character.codePointAt(0) ?? 0;
1814
+ if (code > 32 && !(code >= 127 && code <= 159)) cleaned += character;
1815
+ }
1816
+ if (/^[/\\]{2}/.exec(cleaned)) return "";
1817
+ const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(cleaned);
1818
+ if (scheme && scheme[1] !== void 0 && !SAFE_URL_SCHEMES.has(scheme[1].toLowerCase())) return "";
1819
+ return escapeHtml(cleaned);
1820
+ }
1821
+ /**
1822
+ * Render a {@link MarkdownNode} (typically a {@link MarkdownDocument}) to a safe HTML
1823
+ * string - the recursive AST → HTML engine (headings, paragraphs, lists, GFM tables,
1824
+ * fenced code, blockquotes, links, emphasis, inline code), escaping every text run and
1825
+ * sanitizing every link `href`.
1826
+ *
1827
+ * @remarks
1828
+ * Total: never throws. At {@link MAX_DEPTH} a value-bearing node (`text` / `codeSpan`)
1829
+ * degrades to its escaped `value`; any other node degrades to `''` instead of
1830
+ * recursing further, so pathologically deep input cannot exhaust the call stack. The
1831
+ * recursive engine and its per-shape sub-steps (inline concatenation, table cell,
1832
+ * tight list-item) are nested inner functions - the only exported surface is
1833
+ * `renderHTML` itself.
1834
+ *
1835
+ * @param node - The AST node to render (a full document, or any sub-node)
1836
+ * @returns The rendered, XSS-safe HTML string
1837
+ *
1838
+ * @example
1839
+ * ```ts
1840
+ * renderHTML({ element: 'document', children: [
1841
+ * { element: 'heading', level: 1, children: [{ element: 'text', value: 'Hi' }] },
1842
+ * ] })
1843
+ * // '<h1>Hi</h1>'
1844
+ * ```
1845
+ */
1846
+ function renderHTML(node) {
1847
+ function render(current, depth) {
1848
+ if (depth >= 64) return "value" in current && typeof current.value === "string" ? escapeHtml(current.value) : "";
1849
+ switch (current.element) {
1850
+ case "document": return current.children.map((child) => render(child, depth + 1)).join("\n");
1851
+ case "heading": return `<h${current.level}>${renderInline(current.children, depth)}</h${current.level}>`;
1852
+ case "paragraph": return `<p>${renderInline(current.children, depth)}</p>`;
1853
+ case "thematicBreak": return "<hr>";
1854
+ case "blockquote": return `<blockquote>\n${current.children.map((child) => render(child, depth + 1)).join("\n")}\n</blockquote>`;
1855
+ case "codeBlock": return `<pre>${current.lang === void 0 ? "<code>" : `<code class="language-${escapeHtml(current.lang)}">`}${escapeHtml(current.code)}</code></pre>`;
1856
+ case "list": {
1857
+ const items = current.items.map((item) => render(item, depth + 1)).join("\n");
1858
+ if (!current.ordered) return `<ul>\n${items}\n</ul>`;
1859
+ return `<ol${current.start !== 1 ? ` start="${current.start}"` : ""}>\n${items}\n</ol>`;
1860
+ }
1861
+ case "listItem": return `<li>${renderItem(current.children, depth)}</li>`;
1862
+ case "table": {
1863
+ const head = `<tr>${current.header.map((cell, column) => renderCell("th", cell, current.align[column], depth)).join("")}</tr>`;
1864
+ const body = current.rows.map((row) => `<tr>${row.map((cell, column) => renderCell("td", cell, current.align[column], depth)).join("")}</tr>`).join("\n");
1865
+ return `<table>\n<thead>\n${head}\n</thead>${isNonEmptyArray(current.rows) ? `\n<tbody>\n${body}\n</tbody>` : ""}\n</table>`;
1866
+ }
1867
+ case "text": return escapeHtml(current.value);
1868
+ case "emphasis": return current.strong ? `<strong>${renderInline(current.children, depth + 1)}</strong>` : `<em>${renderInline(current.children, depth + 1)}</em>`;
1869
+ case "codeSpan": return `<code>${escapeHtml(current.value)}</code>`;
1870
+ case "link": return `<a href="${sanitizeUrl(current.href)}">${renderInline(current.children, depth + 1)}</a>`;
1871
+ default: return "";
1872
+ }
1873
+ }
1874
+ function renderInline(nodes, depth) {
1875
+ return nodes.map((child) => render(child, depth + 1)).join("");
1876
+ }
1877
+ function renderCell(tag, cell, align, depth) {
1878
+ return `<${tag}${align === "left" || align === "right" || align === "center" ? ` style="text-align:${align}"` : ""}>${renderInline(cell, depth + 1)}</${tag}>`;
1879
+ }
1880
+ function renderItem(children, depth) {
1881
+ if (children.length === 1) {
1882
+ const only = children[0];
1883
+ if (only !== void 0 && only.element === "paragraph") return renderInline(only.children, depth);
1884
+ }
1885
+ return children.map((child) => render(child, depth + 1)).join("\n");
1886
+ }
1887
+ return render(node, 0);
1888
+ }
1889
+ /**
1890
+ * Render a {@link MarkdownNode} to its CANONICAL markdown source - the inverse
1891
+ * projection of `renderHTML`, and the serializer a `parse(renderMarkdown(doc))`
1892
+ * round-trip is built on. Canonical forms: `*em*` / `**strong**` (underscore emphasis
1893
+ * normalizes to asterisks), `- ` bullets, `N. ` sequential ordinals (from the list's
1894
+ * `start`), `---` thematic breaks, fenced code blocks (backtick run widened past any
1895
+ * 3+ backtick run inside the body), ATX headings, `> `-prefixed blockquote lines, GFM
1896
+ * tables (1-space-padded cells, `\|`-escaped pipes, an alignment delimiter row), and
1897
+ * `[text](href)` links. A `text` node's literal content is backslash-escaped wherever
1898
+ * it would otherwise re-parse as markup (AGENTS §14 parse↔render soundness).
1899
+ *
1900
+ * @remarks
1901
+ * Total: never throws. At {@link MAX_DEPTH} a value-bearing node degrades to its
1902
+ * escaped `value`; any other node degrades to `''`. Blocks are joined by exactly one
1903
+ * blank line; a document with zero blocks renders `''`.
1904
+ *
1905
+ * @param node - The AST node to render (a full document, or any sub-node)
1906
+ * @returns The canonical markdown source
1907
+ *
1908
+ * @example
1909
+ * ```ts
1910
+ * renderMarkdown({ element: 'document', children: [
1911
+ * { element: 'heading', level: 2, children: [{ element: 'text', value: 'Hi' }] },
1912
+ * ] })
1913
+ * // '## Hi'
1914
+ * ```
1915
+ */
1916
+ function renderMarkdown(node) {
1917
+ function escapeText(value) {
1918
+ let out = "";
1919
+ for (let index = 0; index < value.length; index += 1) {
1920
+ const character = value[index] ?? "";
1921
+ const atLineStart = index === 0 || value[index - 1] === "\n";
1922
+ if (character === "\\" || character === "*" || character === "_" || character === "`" || character === "[" || character === "]") {
1923
+ out += `\\${character}`;
1924
+ continue;
1925
+ }
1926
+ if (atLineStart) {
1927
+ if (character === "#" || character === ">") {
1928
+ out += `\\${character}`;
1929
+ continue;
1930
+ }
1931
+ if ((character === "-" || character === "+") && (value[index + 1] ?? " ") === " ") {
1932
+ out += `\\${character}`;
1933
+ continue;
1934
+ }
1935
+ if (/[0-9]/.test(character)) {
1936
+ let end = index;
1937
+ while (end < value.length && /[0-9]/.test(value[end] ?? "")) end += 1;
1938
+ const marker = value[end];
1939
+ if ((marker === "." || marker === ")") && value[end + 1] === " ") {
1940
+ out += `${value.slice(index, end)}\\${marker}`;
1941
+ index = end;
1942
+ continue;
1943
+ }
1944
+ }
1945
+ }
1946
+ out += character;
1947
+ }
1948
+ return out;
1949
+ }
1950
+ function fenceFor(body, minimum) {
1951
+ let longest = 0;
1952
+ let run = 0;
1953
+ for (const character of body) if (character === "`") {
1954
+ run += 1;
1955
+ longest = Math.max(longest, run);
1956
+ } else run = 0;
1957
+ return "`".repeat(Math.max(minimum, longest + 1));
1958
+ }
1959
+ function renderInline(nodes, depth) {
1960
+ return nodes.map((child) => render(child, depth + 1)).join("");
1961
+ }
1962
+ function renderBlocks(blocks, depth) {
1963
+ return blocks.map((block) => render(block, depth + 1)).join("\n\n");
1964
+ }
1965
+ function renderItem(item, marker, depth) {
1966
+ const body = renderBlocks(item.children, depth + 1);
1967
+ const pad = " ".repeat(marker.length);
1968
+ return body.split("\n").map((line, index) => index === 0 ? marker + line : line === "" ? "" : pad + line).join("\n");
1969
+ }
1970
+ function renderCell(cell, depth) {
1971
+ return renderInline(cell, depth + 1).replace(/\|/g, "\\|");
1972
+ }
1973
+ function renderTable(current, depth) {
1974
+ const columns = current.header.length;
1975
+ return [
1976
+ `| ${current.header.map((cell) => renderCell(cell, depth)).join(" | ")} |`,
1977
+ `| ${current.align.map((align) => {
1978
+ if (align === "left") return ":--";
1979
+ if (align === "right") return "--:";
1980
+ if (align === "center") return ":-:";
1981
+ return "---";
1982
+ }).join(" | ")} |`,
1983
+ ...current.rows.map((row) => {
1984
+ const cells = [];
1985
+ for (let column = 0; column < columns; column += 1) {
1986
+ const cell = row[column];
1987
+ cells.push(cell === void 0 ? "" : renderCell(cell, depth));
1988
+ }
1989
+ return `| ${cells.join(" | ")} |`;
1990
+ })
1991
+ ].join("\n");
1992
+ }
1993
+ function render(current, depth) {
1994
+ if (depth >= 64) return "value" in current && typeof current.value === "string" ? escapeText(current.value) : "";
1995
+ switch (current.element) {
1996
+ case "document": return renderBlocks(current.children, depth);
1997
+ case "heading": {
1998
+ const escaped = renderInline(current.children, depth).replace(/(^|[^\\])(#+)$/, (_match, pre, hashes) => {
1999
+ return `${pre}\\${hashes[0] ?? ""}${hashes.slice(1)}`;
2000
+ });
2001
+ return `${"#".repeat(current.level)} ${escaped}`;
2002
+ }
2003
+ case "paragraph": return renderInline(current.children, depth);
2004
+ case "thematicBreak": return "---";
2005
+ case "blockquote": return renderBlocks(current.children, depth).split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
2006
+ case "codeBlock": {
2007
+ const fence = fenceFor(current.code, 3);
2008
+ return `${fence}${current.lang === void 0 ? "" : current.lang}\n${current.code}\n${fence}`;
2009
+ }
2010
+ case "list": {
2011
+ let ordinal = current.start;
2012
+ return current.items.map((item) => {
2013
+ return renderItem(item, current.ordered ? `${ordinal++}. ` : "- ", depth);
2014
+ }).join("\n");
2015
+ }
2016
+ case "listItem": return renderBlocks(current.children, depth);
2017
+ case "table": return renderTable(current, depth);
2018
+ case "text": return escapeText(current.value);
2019
+ case "emphasis": {
2020
+ const marker = current.strong ? "**" : "*";
2021
+ return `${marker}${renderInline(current.children, depth)}${marker}`;
2022
+ }
2023
+ case "codeSpan": {
2024
+ const fence = fenceFor(current.value, 1);
2025
+ const pad = current.value.startsWith("`") || current.value.endsWith("`") ? " " : "";
2026
+ return `${fence}${pad}${current.value}${pad}${fence}`;
2027
+ }
2028
+ case "link": {
2029
+ const href = current.href.replace(/[\\()]/g, (character) => `\\${character}`);
2030
+ return `[${renderInline(current.children, depth)}](${href})`;
2031
+ }
2032
+ default: return "";
2033
+ }
2034
+ }
2035
+ return render(node, 0);
2036
+ }
2037
+ /**
2038
+ * Depth-first, pre-order, root-inclusive traversal of a {@link MarkdownNode} - yields
2039
+ * the node itself, then recurses into its children (block children, list items, table
2040
+ * header/row cells' inline nodes) in walk order.
2041
+ *
2042
+ * @remarks
2043
+ * Total: never throws. Descent stops at {@link MAX_DEPTH} (the node at the cap is
2044
+ * still yielded; its children are not) so pathologically deep input cannot exhaust
2045
+ * the call stack.
2046
+ *
2047
+ * @param node - The AST node to walk (a full document, or any sub-node)
2048
+ * @returns A generator yielding every visited node, pre-order
2049
+ *
2050
+ * @example
2051
+ * ```ts
2052
+ * const doc = { element: 'document', children: [{ element: 'thematicBreak' }] } as const
2053
+ * [...walkNodes(doc)].map((node) => node.element) // ['document', 'thematicBreak']
2054
+ * ```
2055
+ */
2056
+ function* walkNodes(node) {
2057
+ function* walk(current, depth) {
2058
+ yield current;
2059
+ if (depth >= 64) return;
2060
+ switch (current.element) {
2061
+ case "document":
2062
+ case "heading":
2063
+ case "paragraph":
2064
+ case "blockquote":
2065
+ case "listItem":
2066
+ case "emphasis":
2067
+ case "link":
2068
+ for (const child of current.children) yield* walk(child, depth + 1);
2069
+ return;
2070
+ case "list":
2071
+ for (const item of current.items) yield* walk(item, depth + 1);
2072
+ return;
2073
+ case "table":
2074
+ for (const cell of current.header) for (const inline of cell) yield* walk(inline, depth + 1);
2075
+ for (const row of current.rows) for (const cell of row) for (const inline of cell) yield* walk(inline, depth + 1);
2076
+ return;
2077
+ default: return;
2078
+ }
2079
+ }
2080
+ yield* walk(node, 0);
2081
+ }
2082
+ /**
2083
+ * Fold a {@link MarkdownNode} into a `T` via a total catamorphism - children are
2084
+ * folded first (post-order), then the node's own {@link MarkdownHandler} is invoked
2085
+ * with the already-folded children.
2086
+ *
2087
+ * @remarks
2088
+ * **Table contract.** A {@link TableNode} has no single `children` array - its cells
2089
+ * live in `header` (one inline-node list per column) and `rows` (a list of such
2090
+ * rows). The `table` handler receives ONE folded `T` per inline node, flattened in
2091
+ * walk order across ALL cells - every header cell's inline nodes (column order), then
2092
+ * every body row's cells' inline nodes (row order, then column order) - and reads
2093
+ * `node.header[c].length` / `node.rows[r][c].length` off the table node itself to
2094
+ * recover cell boundaries within the flat list.
2095
+ *
2096
+ * Total: never throws. At `depth >= {@link MAX_DEPTH}` the node's handler is invoked
2097
+ * with an empty children list instead of recursing further.
2098
+ *
2099
+ * @param node - The AST node to fold
2100
+ * @param handlers - The total {@link MarkdownHandlers} table, one handler per element
2101
+ * @param depth - The starting recursion depth (pass `0` at the entry point)
2102
+ * @returns The folded `T`
2103
+ *
2104
+ * @example
2105
+ * ```ts
2106
+ * const countHandlers: MarkdownHandlers<number> = {
2107
+ * document: (_, children) => children.reduce((a, b) => a + b, 1),
2108
+ * // ...one handler per element, each summing its folded children
2109
+ * }
2110
+ * foldNode(document, countHandlers, 0) // total node count
2111
+ * ```
2112
+ */
2113
+ function foldNode(node, handlers, depth) {
2114
+ function dispatch(current, children) {
2115
+ switch (current.element) {
2116
+ case "document": return handlers.document(current, children);
2117
+ case "heading": return handlers.heading(current, children);
2118
+ case "paragraph": return handlers.paragraph(current, children);
2119
+ case "thematicBreak": return handlers.thematicBreak(current, children);
2120
+ case "blockquote": return handlers.blockquote(current, children);
2121
+ case "codeBlock": return handlers.codeBlock(current, children);
2122
+ case "list": return handlers.list(current, children);
2123
+ case "listItem": return handlers.listItem(current, children);
2124
+ case "table": return handlers.table(current, children);
2125
+ case "text": return handlers.text(current, children);
2126
+ case "emphasis": return handlers.emphasis(current, children);
2127
+ case "codeSpan": return handlers.codeSpan(current, children);
2128
+ case "link": return handlers.link(current, children);
2129
+ }
2130
+ }
2131
+ function childNodes(current) {
2132
+ switch (current.element) {
2133
+ case "document":
2134
+ case "heading":
2135
+ case "paragraph":
2136
+ case "blockquote":
2137
+ case "listItem":
2138
+ case "emphasis":
2139
+ case "link": return current.children;
2140
+ case "list": return current.items;
2141
+ case "table": {
2142
+ const header = current.header.flatMap((cell) => cell);
2143
+ const rows = current.rows.flatMap((row) => row.flatMap((cell) => cell));
2144
+ return [...header, ...rows];
2145
+ }
2146
+ default: return [];
2147
+ }
2148
+ }
2149
+ function fold(current, level) {
2150
+ if (level >= 64) return dispatch(current, []);
2151
+ return dispatch(current, childNodes(current).map((child) => fold(child, level + 1)));
2152
+ }
2153
+ return fold(node, depth);
2154
+ }
2155
+ /**
2156
+ * Rewrite a {@link MarkdownDocument} bottom-up (copy-on-write) - each node's children
2157
+ * are rewritten first (post-order), then `rewrite` is applied to the node itself; the
2158
+ * document ROOT is never passed to `rewrite` (the `element: 'document'` invariant
2159
+ * always holds). A table's inline cells and a list's items ARE rewritten.
2160
+ *
2161
+ * @remarks
2162
+ * Never mutates `document` - every level is rebuilt into a fresh object/array, even
2163
+ * when `rewrite` returns its input unchanged. When `rewrite` returns a node whose
2164
+ * `element` does not fit the slot it was called for (a block slot handed a
2165
+ * non-{@link BlockNode}, an inline slot handed a non-{@link InlineNode}, a list-item
2166
+ * slot handed a non-`listItem`), the ill-fitting result is discarded and the
2167
+ * freshly-rebuilt (unrewritten-at-this-level) node is kept instead - `rewriteDocument`
2168
+ * stays total and never produces a structurally invalid document.
2169
+ *
2170
+ * Descent is capped at {@link MAX_DEPTH}, the same cap {@link walkNodes} and
2171
+ * {@link foldNode} observe: at `depth >= MAX_DEPTH` the subtree is passed through
2172
+ * UNCHANGED (by reference, not rebuilt, and `rewrite` is not invoked on it) instead of
2173
+ * recursing further, so a pathologically deep adopted document cannot exhaust the
2174
+ * call stack. {@link MarkdownInterface.map} inherits this cap since it delegates here.
2175
+ *
2176
+ * @param document - The document AST to rewrite
2177
+ * @param rewrite - The bottom-up {@link MarkdownRewriteHandler}
2178
+ * @returns A new, rewritten {@link MarkdownDocument}
2179
+ *
2180
+ * @example
2181
+ * ```ts
2182
+ * rewriteDocument(document, (node) =>
2183
+ * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node,
2184
+ * )
2185
+ * ```
2186
+ */
2187
+ function rewriteDocument(document, rewrite) {
2188
+ function rewriteInline(node, depth) {
2189
+ if (depth >= 64) return node;
2190
+ const rebuilt = rebuildInline(node, depth);
2191
+ const result = rewrite(rebuilt);
2192
+ return isInlineNode(result) ? result : rebuilt;
2193
+ }
2194
+ function rewriteBlock(node, depth) {
2195
+ if (depth >= 64) return node;
2196
+ const rebuilt = rebuildBlock(node, depth);
2197
+ const result = rewrite(rebuilt);
2198
+ return isBlockNode(result) ? result : rebuilt;
2199
+ }
2200
+ function rewriteItem(item, depth) {
2201
+ if (depth >= 64) return item;
2202
+ const rebuilt = {
2203
+ element: "listItem",
2204
+ children: item.children.map((child) => rewriteBlock(child, depth + 1))
2205
+ };
2206
+ const result = rewrite(rebuilt);
2207
+ return result.element === "listItem" ? result : rebuilt;
2208
+ }
2209
+ function rebuildInline(node, depth) {
2210
+ switch (node.element) {
2211
+ case "emphasis": return {
2212
+ ...node,
2213
+ children: node.children.map((child) => rewriteInline(child, depth + 1))
2214
+ };
2215
+ case "link": return {
2216
+ ...node,
2217
+ children: node.children.map((child) => rewriteInline(child, depth + 1))
2218
+ };
2219
+ case "text":
2220
+ case "codeSpan": return node;
2221
+ }
2222
+ }
2223
+ function rebuildBlock(node, depth) {
2224
+ switch (node.element) {
2225
+ case "heading": return {
2226
+ ...node,
2227
+ children: node.children.map((child) => rewriteInline(child, depth + 1))
2228
+ };
2229
+ case "paragraph": return {
2230
+ ...node,
2231
+ children: node.children.map((child) => rewriteInline(child, depth + 1))
2232
+ };
2233
+ case "blockquote": return {
2234
+ ...node,
2235
+ children: node.children.map((child) => rewriteBlock(child, depth + 1))
2236
+ };
2237
+ case "list": return {
2238
+ ...node,
2239
+ items: node.items.map((item) => rewriteItem(item, depth + 1))
2240
+ };
2241
+ case "table": return {
2242
+ ...node,
2243
+ header: node.header.map((cell) => cell.map((inline) => rewriteInline(inline, depth + 1))),
2244
+ rows: node.rows.map((row) => row.map((cell) => cell.map((inline) => rewriteInline(inline, depth + 1))))
2245
+ };
2246
+ case "codeBlock":
2247
+ case "thematicBreak": return node;
2248
+ }
2249
+ }
2250
+ return {
2251
+ element: "document",
2252
+ children: document.children.map((child) => rewriteBlock(child, 0))
2253
+ };
2254
+ }
2255
+ /**
2256
+ * Concatenate the `value` / `code` content of every descendant text / code-span /
2257
+ * code-block node under `node`, in walk order - the plain-text projection of an AST
2258
+ * (search indexing, word counts, a text-only preview).
2259
+ *
2260
+ * @remarks
2261
+ * Total: never throws. Descent stops at {@link MAX_DEPTH} (contributes `''` past the
2262
+ * cap instead of recursing further).
2263
+ *
2264
+ * @param node - The AST node to flatten (a full document, or any sub-node)
2265
+ * @returns The concatenated text content
2266
+ *
2267
+ * @example
2268
+ * ```ts
2269
+ * flattenText({ element: 'paragraph', children: [
2270
+ * { element: 'text', value: 'a ' },
2271
+ * { element: 'codeSpan', value: 'b' },
2272
+ * ] })
2273
+ * // 'a b'
2274
+ * ```
2275
+ */
2276
+ function flattenText(node) {
2277
+ function flatten(current, depth) {
2278
+ if (depth >= 64) return "";
2279
+ switch (current.element) {
2280
+ case "text": return current.value;
2281
+ case "codeSpan": return current.value;
2282
+ case "codeBlock": return current.code;
2283
+ case "document":
2284
+ case "heading":
2285
+ case "paragraph":
2286
+ case "blockquote":
2287
+ case "listItem":
2288
+ case "emphasis":
2289
+ case "link": return current.children.map((child) => flatten(child, depth + 1)).join("");
2290
+ case "list": return current.items.map((item) => flatten(item, depth + 1)).join("");
2291
+ case "table": return current.header.map((cell) => cell.map((inline) => flatten(inline, depth + 1)).join("")).join("") + current.rows.map((row) => row.map((cell) => cell.map((inline) => flatten(inline, depth + 1)).join("")).join("")).join("");
2292
+ case "thematicBreak": return "";
2293
+ default: return "";
2294
+ }
2295
+ }
2296
+ return flatten(node, 0);
2297
+ }
2298
+ //#endregion
2299
+ //#region src/core/parsers.ts
2300
+ /**
2301
+ * Parses a run of markdown lines into a block AST, recursing into nested
2302
+ * blockquotes, list items, and depth-capped degrade paragraphs.
2303
+ *
2304
+ * @param lines - The markdown lines to parse.
2305
+ * @param depth - The current recursion depth (blockquotes/lists increment it).
2306
+ * @returns The parsed block nodes.
2307
+ */
2308
+ function parseBlocks(lines, depth) {
2309
+ if (depth >= 64) return lines.length > 0 ? [{
2310
+ element: "paragraph",
2311
+ children: [{
2312
+ element: "text",
2313
+ value: lines.join("\n")
2314
+ }]
2315
+ }] : [];
2316
+ const blocks = [];
2317
+ let index = 0;
2318
+ while (index < lines.length) {
2319
+ const line = lines[index] ?? "";
2320
+ if (isBlankLine(line)) {
2321
+ index += 1;
2322
+ continue;
2323
+ }
2324
+ const fence = extractFence(line);
2325
+ if (fence) {
2326
+ const body = [];
2327
+ index += 1;
2328
+ while (index < lines.length && !isFenceClose(lines[index] ?? "", fence.marker)) {
2329
+ body.push(lines[index] ?? "");
2330
+ index += 1;
2331
+ }
2332
+ index += 1;
2333
+ blocks.push({
2334
+ element: "codeBlock",
2335
+ ...fence.lang === void 0 ? {} : { lang: fence.lang },
2336
+ code: body.join("\n")
2337
+ });
2338
+ continue;
2339
+ }
2340
+ if (isThematicBreak(line)) {
2341
+ blocks.push({ element: "thematicBreak" });
2342
+ index += 1;
2343
+ continue;
2344
+ }
2345
+ const heading = extractHeading(line);
2346
+ if (heading) {
2347
+ blocks.push({
2348
+ element: "heading",
2349
+ level: heading.level,
2350
+ children: parseInline(heading.text)
2351
+ });
2352
+ index += 1;
2353
+ continue;
2354
+ }
2355
+ if (isQuote(line)) {
2356
+ const quoted = [];
2357
+ while (index < lines.length && isQuote(lines[index] ?? "")) {
2358
+ quoted.push(stripQuote(lines[index] ?? ""));
2359
+ index += 1;
2360
+ }
2361
+ blocks.push({
2362
+ element: "blockquote",
2363
+ children: parseBlocks(quoted, depth + 1)
2364
+ });
2365
+ continue;
2366
+ }
2367
+ if (isTableStart(line, lines[index + 1])) {
2368
+ const table = collectTable(lines, index);
2369
+ blocks.push(table.node);
2370
+ index = table.next;
2371
+ continue;
2372
+ }
2373
+ if (extractListItem(line)) {
2374
+ const list = collectList(lines, index, depth);
2375
+ blocks.push(list.node);
2376
+ index = list.next;
2377
+ continue;
2378
+ }
2379
+ const paragraph = [];
2380
+ while (index < lines.length && !isBlankLine(lines[index] ?? "") && !(isNonEmptyArray(paragraph) && startsBlock(lines, index))) {
2381
+ paragraph.push((lines[index] ?? "").trim());
2382
+ index += 1;
2383
+ }
2384
+ blocks.push({
2385
+ element: "paragraph",
2386
+ children: parseInline(paragraph.join("\n"))
2387
+ });
2388
+ }
2389
+ return blocks;
2390
+ }
2391
+ /**
2392
+ * Collects a GFM table starting at a header row, parsing the header, the
2393
+ * alignment row, and every contiguous body row that follows.
2394
+ *
2395
+ * @param lines - The markdown lines to scan.
2396
+ * @param start - The index of the header row.
2397
+ * @returns The parsed table node and the index of the first line after it.
2398
+ */
2399
+ function collectTable(lines, start) {
2400
+ const headerCells = splitTableRow(lines[start] ?? "");
2401
+ const columns = headerCells.length;
2402
+ const header = headerCells.map((cell) => parseInline(cell.trim()));
2403
+ const align = tableAlignments(lines[start + 1] ?? "");
2404
+ const padded = [];
2405
+ for (let column = 0; column < columns; column += 1) padded.push(align[column] ?? "none");
2406
+ const rows = [];
2407
+ let index = start + 2;
2408
+ while (index < lines.length && !isBlankLine(lines[index] ?? "") && (lines[index] ?? "").includes("|")) {
2409
+ const cells = splitTableRow(lines[index] ?? "");
2410
+ const row = [];
2411
+ for (let column = 0; column < columns; column += 1) row.push(parseInline((cells[column] ?? "").trim()));
2412
+ rows.push(row);
2413
+ index += 1;
2414
+ }
2415
+ return {
2416
+ node: {
2417
+ element: "table",
2418
+ header,
2419
+ rows,
2420
+ align: padded
2421
+ },
2422
+ next: index
2423
+ };
2424
+ }
2425
+ /**
2426
+ * Collects a list starting at the first item, gathering sibling items at the
2427
+ * same indent/ordering and recursing into each item's own block content.
2428
+ *
2429
+ * @param lines - The markdown lines to scan.
2430
+ * @param start - The index of the first list item.
2431
+ * @param depth - The current recursion depth (each item recurses at `depth + 1`).
2432
+ * @returns The parsed list node and the index of the first line after it.
2433
+ */
2434
+ function collectList(lines, start, depth) {
2435
+ const first = extractListItem(lines[start] ?? "");
2436
+ const ordered = first?.ordered ?? false;
2437
+ const startOrdinal = first?.start ?? 1;
2438
+ const topIndent = first?.indent ?? 0;
2439
+ const items = [];
2440
+ let index = start;
2441
+ while (index < lines.length) {
2442
+ const parsed = extractListItem(lines[index] ?? "");
2443
+ if (!parsed || parsed.indent > topIndent || parsed.ordered !== ordered) break;
2444
+ const itemLines = [parsed.content];
2445
+ const continuation = parsed.marker;
2446
+ index += 1;
2447
+ while (index < lines.length) {
2448
+ const next = lines[index] ?? "";
2449
+ if (isBlankLine(next)) {
2450
+ const after = lines[index + 1] ?? "";
2451
+ if (index + 1 < lines.length && !isBlankLine(after) && leadingIndent(after) >= continuation) {
2452
+ itemLines.push("");
2453
+ index += 1;
2454
+ continue;
2455
+ }
2456
+ break;
2457
+ }
2458
+ if (leadingIndent(next) >= continuation) {
2459
+ itemLines.push(next.slice(continuation));
2460
+ index += 1;
2461
+ continue;
2462
+ }
2463
+ if (extractListItem(next) || startsBlock(lines, index)) break;
2464
+ itemLines.push(next.trim());
2465
+ index += 1;
2466
+ }
2467
+ items.push({
2468
+ element: "listItem",
2469
+ children: parseBlocks(itemLines, depth + 1)
2470
+ });
2471
+ }
2472
+ return {
2473
+ node: {
2474
+ element: "list",
2475
+ ordered,
2476
+ start: startOrdinal,
2477
+ items
2478
+ },
2479
+ next: index
2480
+ };
2481
+ }
2482
+ /**
2483
+ * Parses a markdown string into a typed {@link MarkdownDocument} AST via the
2484
+ * block phase.
2485
+ *
2486
+ * @param markdown - The markdown source to parse.
2487
+ * @returns The parsed document.
2488
+ */
2489
+ function parseDocument(markdown) {
2490
+ return {
2491
+ element: "document",
2492
+ children: parseBlocks(splitLines(markdown), 0)
2493
+ };
2494
+ }
2495
+ /**
2496
+ * Parses inline markdown text (emphasis, code spans, links) into inline AST
2497
+ * nodes, coalescing adjacent text runs.
2498
+ *
2499
+ * @param text - The inline markdown text to parse.
2500
+ * @returns The parsed inline nodes.
2501
+ */
2502
+ function parseInline(text) {
2503
+ return coalesceText(scanInline(text, 0, text.length));
2504
+ }
2505
+ //#endregion
2506
+ //#region src/core/shapers.ts
2507
+ /**
2508
+ * The shape of a {@link TextNode} - a plain-text leaf inline run.
2509
+ *
2510
+ * @example
2511
+ * ```ts
2512
+ * import { createContract } from '@orkestrel/contract'
2513
+ * import { textShape } from '@src/core'
2514
+ *
2515
+ * const text = createContract(textShape)
2516
+ * text.is({ element: 'text', value: 'hi' }) // true
2517
+ * ```
2518
+ */
2519
+ var textShape = objectShape({
2520
+ element: literalShape(["text"]),
2521
+ value: stringShape()
2522
+ });
2523
+ /**
2524
+ * The shape of a {@link CodeSpanNode} - an inline code span (`` `code` ``).
2525
+ *
2526
+ * @example
2527
+ * ```ts
2528
+ * import { createContract } from '@orkestrel/contract'
2529
+ * import { codeSpanShape } from '@src/core'
2530
+ *
2531
+ * const codeSpan = createContract(codeSpanShape)
2532
+ * codeSpan.is({ element: 'codeSpan', value: 'const x = 1' }) // true
2533
+ * ```
2534
+ */
2535
+ var codeSpanShape = objectShape({
2536
+ element: literalShape(["codeSpan"]),
2537
+ value: stringShape()
2538
+ });
2539
+ /**
2540
+ * The shape of a {@link CodeBlockNode} - a fenced code block. `lang` is
2541
+ * optional (absent when the opening fence carries no info-string).
2542
+ *
2543
+ * @example
2544
+ * ```ts
2545
+ * import { createContract } from '@orkestrel/contract'
2546
+ * import { codeBlockShape } from '@src/core'
2547
+ *
2548
+ * const codeBlock = createContract(codeBlockShape)
2549
+ * codeBlock.is({ element: 'codeBlock', code: 'x' }) // true
2550
+ * codeBlock.is({ element: 'codeBlock', code: 'x', lang: 'ts' }) // true
2551
+ * ```
2552
+ */
2553
+ var codeBlockShape = objectShape({
2554
+ element: literalShape(["codeBlock"]),
2555
+ lang: optionalShape(stringShape()),
2556
+ code: stringShape()
2557
+ });
2558
+ /**
2559
+ * The shape of a {@link ThematicBreakNode} - a horizontal rule. Carries no
2560
+ * fields beyond its `element` discriminant.
2561
+ *
2562
+ * @example
2563
+ * ```ts
2564
+ * import { createContract } from '@orkestrel/contract'
2565
+ * import { thematicBreakShape } from '@src/core'
2566
+ *
2567
+ * const thematicBreak = createContract(thematicBreakShape)
2568
+ * thematicBreak.is({ element: 'thematicBreak' }) // true
2569
+ * ```
2570
+ */
2571
+ var thematicBreakShape = objectShape({ element: literalShape(["thematicBreak"]) });
2572
+ /**
2573
+ * The shape of a {@link TableAlign} - the per-column GFM table alignment
2574
+ * literal.
2575
+ *
2576
+ * @example
2577
+ * ```ts
2578
+ * import { createContract } from '@orkestrel/contract'
2579
+ * import { tableAlignShape } from '@src/core'
2580
+ *
2581
+ * const tableAlign = createContract(tableAlignShape)
2582
+ * tableAlign.is('left') // true
2583
+ * tableAlign.is('center') // true
2584
+ * tableAlign.is('top') // false
2585
+ * ```
2586
+ */
2587
+ var tableAlignShape = literalShape([
2588
+ "none",
2589
+ "left",
2590
+ "right",
2591
+ "center"
2592
+ ]);
2593
+ /**
2594
+ * The shape of {@link ListItemParts} - the parsed parts of a single list-item
2595
+ * line the block phase's list detector returns. Fully non-recursive (no
2596
+ * nested node fields), so every field shapes directly.
2597
+ *
2598
+ * @example
2599
+ * ```ts
2600
+ * import { createContract } from '@orkestrel/contract'
2601
+ * import { listItemPartsShape } from '@src/core'
2602
+ *
2603
+ * const listItemParts = createContract(listItemPartsShape)
2604
+ * listItemParts.is({ ordered: false, start: 1, content: 'hi', indent: 0, marker: 2 }) // true
2605
+ * ```
2606
+ */
2607
+ var listItemPartsShape = objectShape({
2608
+ ordered: booleanShape(),
2609
+ start: integerShape(),
2610
+ content: stringShape(),
2611
+ indent: integerShape(),
2612
+ marker: integerShape()
2613
+ });
2614
+ //#endregion
2615
+ //#region src/core/Markdown.ts
2616
+ /**
2617
+ * A stateful, parsed markdown document - wraps a typed {@link MarkdownDocument} AST
2618
+ * with the query (`find` / `filter` / `reduce` / iteration), rewrite (`map`), fold, and
2619
+ * streaming operations {@link MarkdownInterface} declares.
2620
+ *
2621
+ * @remarks
2622
+ * - **Construction.** Given a `string`, the constructor runs {@link parseDocument} (the
2623
+ * block phase then the inline phase) to build the AST. Given a {@link MarkdownDocument},
2624
+ * the document is adopted AS-IS and is NOT re-validated - a caller adopting an
2625
+ * untrusted value should gate it with `isMarkdownDocument` first.
2626
+ * - **Immutable.** {@link map} never mutates the stored AST - it returns a NEW `Markdown`
2627
+ * instance; the document root invariant (`element: 'document'`) always holds.
2628
+ * - **Traversal order.** {@link walk} and the `find` / `filter` / `reduce` queries built
2629
+ * on it walk the AST depth-first, pre-order, root-inclusive (via {@link walkNodes});
2630
+ * `stream` is shallow - only the document's direct block children.
2631
+ *
2632
+ * @example
2633
+ * ```ts
2634
+ * import { Markdown, isHeadingNode, renderMarkdown } from '@src/core'
2635
+ *
2636
+ * const markdown = new Markdown('# Title\n\nA **bold** [link](https://x.dev).')
2637
+ * const heading = markdown.find(isHeadingNode) // the HeadingNode, or undefined
2638
+ * const shouted = markdown.map((node) =>
2639
+ * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node,
2640
+ * )
2641
+ * renderMarkdown(shouted.document) // '# TITLE\n\nA **BOLD** [LINK](https://x.dev).'
2642
+ * ```
2643
+ */
2644
+ var Markdown = class Markdown {
2645
+ #document;
2646
+ constructor(input) {
2647
+ this.#document = typeof input === "string" ? parseDocument(input) : input;
2648
+ }
2649
+ /** The stored {@link MarkdownDocument} AST root. */
2650
+ get document() {
2651
+ return this.#document;
2652
+ }
2653
+ /**
2654
+ * THE deep traversal - a lazy, depth-first, pre-order, root-inclusive generator
2655
+ * over every {@link MarkdownNode} in the document. `find` / `filter` / `reduce`
2656
+ * all iterate this single traversal.
2657
+ *
2658
+ * @example
2659
+ * ```ts
2660
+ * for (const node of markdown.walk()) {
2661
+ * // every node, depth-first, pre-order, root-inclusive
2662
+ * }
2663
+ *
2664
+ * // also consumable by for-await - JS accepts a sync iterable in for-await
2665
+ * for await (const node of markdown.walk()) {
2666
+ * // same sequence, no separate async iterator needed
2667
+ * }
2668
+ * ```
2669
+ */
2670
+ *walk() {
2671
+ yield* walkNodes(this.#document);
2672
+ }
2673
+ find(predicate) {
2674
+ for (const node of this.walk()) if (predicate(node)) return node;
2675
+ }
2676
+ filter(predicate) {
2677
+ const out = [];
2678
+ for (const node of this.walk()) if (predicate(node)) out.push(node);
2679
+ return out;
2680
+ }
2681
+ /** Rewrites the AST bottom-up (copy-on-write) and returns a new {@link Markdown}. */
2682
+ map(rewrite) {
2683
+ return new Markdown(rewriteDocument(this.#document, rewrite));
2684
+ }
2685
+ /** Folds the AST depth-first, pre-order into an accumulator. */
2686
+ reduce(callback, initial) {
2687
+ let accumulator = initial;
2688
+ for (const node of this.walk()) accumulator = callback(accumulator, node);
2689
+ return accumulator;
2690
+ }
2691
+ /** Runs a total catamorphism over the document using a {@link MarkdownHandlers} table. */
2692
+ fold(handlers) {
2693
+ return foldNode(this.#document, handlers, 0);
2694
+ }
2695
+ /**
2696
+ * A web-standard {@link ReadableStream} over the document's top-level block nodes
2697
+ * (shallow, source order) - a fresh, pull-based source per call: one block is
2698
+ * enqueued per `pull`, so a slow reader's backpressure is respected. Cancellable,
2699
+ * async-iterable wherever the platform supports it (Node, Deno), and pipeable
2700
+ * through any {@link TransformStream} / {@link WritableStream}.
2701
+ *
2702
+ * @example
2703
+ * ```ts
2704
+ * // universal - works in every ReadableStream-supporting environment
2705
+ * const reader = markdown.stream().getReader()
2706
+ * for (let result = await reader.read(); !result.done; result = await reader.read()) {
2707
+ * console.log(result.value) // one BlockNode
2708
+ * }
2709
+ *
2710
+ * // Node / Deno / Firefox support async iteration of ReadableStream natively;
2711
+ * // other environments should use the reader loop above instead.
2712
+ * for await (const block of markdown.stream()) {
2713
+ * console.log(block)
2714
+ * }
2715
+ * ```
2716
+ */
2717
+ stream() {
2718
+ const blocks = this.#document.children;
2719
+ let index = 0;
2720
+ return new ReadableStream({ pull(controller) {
2721
+ if (index < blocks.length) {
2722
+ controller.enqueue(blocks[index]);
2723
+ index += 1;
2724
+ } else controller.close();
2725
+ } });
2726
+ }
2727
+ };
2728
+ //#endregion
2729
+ //#region src/core/factories.ts
2730
+ /**
2731
+ * Create a stateful markdown handle from a markdown string or an already-parsed
2732
+ * {@link MarkdownDocument} - a typed AST plus the query, rewrite, and fold operations
2733
+ * {@link MarkdownInterface} exposes.
2734
+ *
2735
+ * @remarks
2736
+ * Given a `string`, runs a block phase (headings / paragraphs / lists / GFM tables /
2737
+ * fenced code / blockquotes / thematic breaks) then an inline phase (emphasis /
2738
+ * inline code / links) to build a render-agnostic {@link MarkdownDocument}. Given a
2739
+ * {@link MarkdownDocument}, adopts it AS-IS without re-validation - gate an untrusted
2740
+ * value with `isMarkdownDocument` first. Pure + total parse (malformed markdown
2741
+ * degrades to text, never throws) and zero-dependency - a hand-written scanner, no
2742
+ * regex-only structural parse, linear-time (no ReDoS).
2743
+ *
2744
+ * @param input - A markdown string to parse, or an already-parsed {@link MarkdownDocument}
2745
+ * @returns A working {@link MarkdownInterface}
2746
+ *
2747
+ * @example
2748
+ * ```ts
2749
+ * import { createMarkdown } from '@src/core'
2750
+ *
2751
+ * const markdown = createMarkdown('# Hi\n\nRead the [guide](./guide.md).')
2752
+ * markdown.document.children[0] // { element: 'heading', ... }
2753
+ * ```
2754
+ */
2755
+ function createMarkdown(input) {
2756
+ return new Markdown(input);
2757
+ }
2758
+ /**
2759
+ * Compile the {@link textShape} into a {@link ContractInterface} for
2760
+ * {@link TextNode} - a guard, coercing parser, JSON Schema, and seeded
2761
+ * generator from one shape declaration (AGENTS §14).
2762
+ *
2763
+ * @returns A `TextNode` contract bundling `schema` / `is` / `parse` / `generate`
2764
+ *
2765
+ * @example
2766
+ * ```ts
2767
+ * import { createTextContract } from '@src/core'
2768
+ *
2769
+ * const text = createTextContract()
2770
+ * text.is({ element: 'text', value: 'hi' }) // true
2771
+ * ```
2772
+ */
2773
+ function createTextContract() {
2774
+ return createContract(textShape);
2775
+ }
2776
+ /**
2777
+ * Compile the {@link codeSpanShape} into a {@link ContractInterface} for
2778
+ * {@link CodeSpanNode} - a guard, coercing parser, JSON Schema, and seeded
2779
+ * generator from one shape declaration (AGENTS §14).
2780
+ *
2781
+ * @returns A `CodeSpanNode` contract bundling `schema` / `is` / `parse` / `generate`
2782
+ *
2783
+ * @example
2784
+ * ```ts
2785
+ * import { createCodeSpanContract } from '@src/core'
2786
+ *
2787
+ * const codeSpan = createCodeSpanContract()
2788
+ * codeSpan.is({ element: 'codeSpan', value: 'const x = 1' }) // true
2789
+ * ```
2790
+ */
2791
+ function createCodeSpanContract() {
2792
+ return createContract(codeSpanShape);
2793
+ }
2794
+ /**
2795
+ * Compile the {@link codeBlockShape} into a {@link ContractInterface} for
2796
+ * {@link CodeBlockNode} - a guard, coercing parser, JSON Schema, and seeded
2797
+ * generator from one shape declaration (AGENTS §14).
2798
+ *
2799
+ * @returns A `CodeBlockNode` contract bundling `schema` / `is` / `parse` / `generate`
2800
+ *
2801
+ * @example
2802
+ * ```ts
2803
+ * import { createCodeBlockContract } from '@src/core'
2804
+ *
2805
+ * const codeBlock = createCodeBlockContract()
2806
+ * codeBlock.is({ element: 'codeBlock', code: 'x' }) // true
2807
+ * ```
2808
+ */
2809
+ function createCodeBlockContract() {
2810
+ return createContract(codeBlockShape);
2811
+ }
2812
+ /**
2813
+ * Compile the {@link thematicBreakShape} into a {@link ContractInterface} for
2814
+ * {@link ThematicBreakNode} - a guard, coercing parser, JSON Schema, and
2815
+ * seeded generator from one shape declaration (AGENTS §14).
2816
+ *
2817
+ * @returns A `ThematicBreakNode` contract bundling `schema` / `is` / `parse` / `generate`
2818
+ *
2819
+ * @example
2820
+ * ```ts
2821
+ * import { createThematicBreakContract } from '@src/core'
2822
+ *
2823
+ * const thematicBreak = createThematicBreakContract()
2824
+ * thematicBreak.is({ element: 'thematicBreak' }) // true
2825
+ * ```
2826
+ */
2827
+ function createThematicBreakContract() {
2828
+ return createContract(thematicBreakShape);
2829
+ }
2830
+ //#endregion
2831
+ export { MAX_DEPTH, Markdown, SAFE_URL_SCHEMES, coalesceText, codeBlockShape, codeSpanShape, collectList, collectTable, createCodeBlockContract, createCodeSpanContract, createMarkdown, createTextContract, createThematicBreakContract, escapeHtml, extractFence, extractHeading, extractListItem, flattenText, foldNode, isBlankLine, isBlockNode, isBlockquoteNode, isCodeBlockNode, isCodeSpanNode, isEmphasisNode, isEscapable, isFenceClose, isFenceWhitespace, isHeadingNode, isInlineNode, isLinkNode, isListNode, isMarkdownDocument, isMarkdownNode, isParagraphNode, isQuote, isTableNode, isTableStart, isTextNode, isThematicBreak, isThematicBreakNode, isWhitespace, leadingIndent, listItemPartsShape, parseBlocks, parseDocument, parseInline, renderHTML, renderMarkdown, rewriteDocument, sanitizeUrl, scanCode, scanEmphasis, scanInline, scanLink, splitLines, splitTableRow, startsBlock, stripQuote, tableAlignShape, tableAlignments, textShape, thematicBreakShape, unescapeText, walkNodes };
2832
+
2833
+ //# sourceMappingURL=index.js.map