@orkestrel/markdown 0.0.1 → 0.0.3

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.
@@ -1,3 +1,4 @@
1
+ import { arrayOf, booleanShape, createContract, integerShape, isBoolean, isEmptyString, isNonEmptyArray, isNonEmptyString, isNumber, isString, lazyOf, literalOf, literalShape, objectShape, optionalShape, parseInteger, recordOf, stringShape, unionOf } from "@orkestrel/contract";
1
2
  //#region src/core/constants.ts
2
3
  /**
3
4
  * The URL schemes `renderHTML` permits on a link `href` - anything else (notably
@@ -21,1050 +22,6 @@ var SAFE_URL_SCHEMES = /* @__PURE__ */ new Set([
21
22
  * parser treats the remaining content as literal text instead of recursing further.
22
23
  */
23
24
  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
25
  //#endregion
1069
26
  //#region src/core/validators.ts
1070
27
  /**
@@ -1073,6 +30,12 @@ function optionalShape(inner) {
1073
30
  *
1074
31
  * @param character - The character to test
1075
32
  * @returns `true` when it is inline whitespace
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * isWhitespace(' ') // true
37
+ * isWhitespace('a') // false
38
+ * ```
1076
39
  */
1077
40
  function isWhitespace(character) {
1078
41
  return character === " " || character === " " || character === "\n";
@@ -1083,6 +46,12 @@ function isWhitespace(character) {
1083
46
  *
1084
47
  * @param character - The single character after a backslash
1085
48
  * @returns `true` when a backslash before it is an escape
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * isEscapable('*') // true
53
+ * isEscapable('a') // false
54
+ * ```
1086
55
  */
1087
56
  function isEscapable(character) {
1088
57
  return /[\\`*_{}[\]()#+\-.!>~|]/.test(character);
@@ -1094,6 +63,11 @@ function isEscapable(character) {
1094
63
  *
1095
64
  * @param line - The candidate line
1096
65
  * @returns `true` when the line is blank
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * isBlankLine(' ') // true
70
+ * ```
1097
71
  */
1098
72
  function isBlankLine(line) {
1099
73
  return isEmptyString(line.trim());
@@ -1104,6 +78,11 @@ function isBlankLine(line) {
1104
78
  *
1105
79
  * @param line - The candidate line
1106
80
  * @returns `true` when the line begins a blockquote
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * isQuote('> quoted') // true
85
+ * ```
1107
86
  */
1108
87
  function isQuote(line) {
1109
88
  return /^\s{0,3}>/.test(line);
@@ -1115,6 +94,11 @@ function isQuote(line) {
1115
94
  * @param line - The candidate closing line
1116
95
  * @param marker - The opening fence's marker run (from {@link extractFence})
1117
96
  * @returns `true` when `line` closes the fence
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * isFenceClose('```', '```') // true
101
+ * ```
1118
102
  */
1119
103
  function isFenceClose(line, marker) {
1120
104
  const character = marker[0] === "~" ? "~" : "`";
@@ -1135,6 +119,12 @@ function isFenceClose(line, marker) {
1135
119
  *
1136
120
  * @param character - The single character to test, or `undefined` past the end of a line
1137
121
  * @returns `true` when it is whitespace
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * isFenceWhitespace(' ') // true
126
+ * isFenceWhitespace(undefined) // false
127
+ * ```
1138
128
  */
1139
129
  function isFenceWhitespace(character) {
1140
130
  return character === " " || character === " " || character === "\n" || character === "\r" || character === "\f" || character === "\v";
@@ -1146,6 +136,11 @@ function isFenceWhitespace(character) {
1146
136
  *
1147
137
  * @param line - The candidate line
1148
138
  * @returns `true` when the line is a thematic break
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * isThematicBreak('---') // true
143
+ * ```
1149
144
  */
1150
145
  function isThematicBreak(line) {
1151
146
  const stripped = line.trim().replace(/\s+/g, "");
@@ -1162,6 +157,11 @@ function isThematicBreak(line) {
1162
157
  * @param header - The candidate header line
1163
158
  * @param delimiter - The line after it (the candidate delimiter)
1164
159
  * @returns `true` when the two lines open a table
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * isTableStart('| a |', '| - |') // true
164
+ * ```
1165
165
  */
1166
166
  function isTableStart(header, delimiter) {
1167
167
  if (delimiter === void 0 || !header.includes("|")) return false;
@@ -1173,11 +173,25 @@ function isTableStart(header, delimiter) {
1173
173
  function isHeadingNode(node) {
1174
174
  return node.element === "heading";
1175
175
  }
1176
- /** Determine whether a node is a paragraph block. */
176
+ /**
177
+ * Determine whether a node is a paragraph block.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * isParagraphNode({ element: 'paragraph', children: [] }) // true
182
+ * ```
183
+ */
1177
184
  function isParagraphNode(node) {
1178
185
  return node.element === "paragraph";
1179
186
  }
1180
- /** Determine whether a node is a list block. */
187
+ /**
188
+ * Determine whether a node is a list block.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * isListNode({ element: 'list', ordered: false, start: 1, items: [] }) // true
193
+ * ```
194
+ */
1181
195
  function isListNode(node) {
1182
196
  return node.element === "list";
1183
197
  }
@@ -1185,23 +199,58 @@ function isListNode(node) {
1185
199
  function isTableNode(node) {
1186
200
  return node.element === "table";
1187
201
  }
1188
- /** Determine whether a node is a fenced code block. */
202
+ /**
203
+ * Determine whether a node is a fenced code block.
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * isCodeBlockNode({ element: 'codeBlock', code: 'x' }) // true
208
+ * ```
209
+ */
1189
210
  function isCodeBlockNode(node) {
1190
211
  return node.element === "codeBlock";
1191
212
  }
1192
- /** Determine whether a node is a blockquote block. */
213
+ /**
214
+ * Determine whether a node is a blockquote block.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * isBlockquoteNode({ element: 'blockquote', children: [] }) // true
219
+ * ```
220
+ */
1193
221
  function isBlockquoteNode(node) {
1194
222
  return node.element === "blockquote";
1195
223
  }
1196
- /** Determine whether a node is a thematic break (horizontal rule) block. */
224
+ /**
225
+ * Determine whether a node is a thematic break (horizontal rule) block.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * isThematicBreakNode({ element: 'thematicBreak' }) // true
230
+ * ```
231
+ */
1197
232
  function isThematicBreakNode(node) {
1198
233
  return node.element === "thematicBreak";
1199
234
  }
1200
- /** Determine whether a node is a plain text run. */
235
+ /**
236
+ * Determine whether a node is a plain text run.
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * isTextNode({ element: 'text', value: 'hi' }) // true
241
+ * ```
242
+ */
1201
243
  function isTextNode(node) {
1202
244
  return node.element === "text";
1203
245
  }
1204
- /** Determine whether a node is an emphasis run (`*em*` / `**strong**`). */
246
+ /**
247
+ * Determine whether a node is an emphasis run (`*em*` / `**strong**`).
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * isEmphasisNode({ element: 'emphasis', strong: false, children: [] }) // true
252
+ * ```
253
+ */
1205
254
  function isEmphasisNode(node) {
1206
255
  return node.element === "emphasis";
1207
256
  }
@@ -1211,6 +260,11 @@ function isEmphasisNode(node) {
1211
260
  * @remarks
1212
261
  * Narrows to {@link CodeSpanNode} - the node whose `element` discriminant is
1213
262
  * `'codeSpan'`.
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * isCodeSpanNode({ element: 'codeSpan', value: 'x' }) // true
267
+ * ```
1214
268
  */
1215
269
  function isCodeSpanNode(node) {
1216
270
  return node.element === "codeSpan";
@@ -1367,6 +421,11 @@ var isMarkdownDocument = recordOf({
1367
421
  *
1368
422
  * @param markdown - The raw markdown source
1369
423
  * @returns The document's lines, line-terminators stripped
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * splitLines('a\r\nb\nc') // ['a', 'b', 'c']
428
+ * ```
1370
429
  */
1371
430
  function splitLines(markdown) {
1372
431
  const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
@@ -1379,6 +438,11 @@ function splitLines(markdown) {
1379
438
  *
1380
439
  * @param line - The line to measure
1381
440
  * @returns The number of leading space / tab characters
441
+ *
442
+ * @example
443
+ * ```ts
444
+ * leadingIndent(' text') // 2
445
+ * ```
1382
446
  */
1383
447
  function leadingIndent(line) {
1384
448
  let count = 0;
@@ -1394,6 +458,11 @@ function leadingIndent(line) {
1394
458
  *
1395
459
  * @param line - The candidate line
1396
460
  * @returns The heading level (1–6) and its raw inline text, or `undefined`
461
+ *
462
+ * @example
463
+ * ```ts
464
+ * extractHeading('## Title') // { level: 2, text: 'Title' }
465
+ * ```
1397
466
  */
1398
467
  function extractHeading(line) {
1399
468
  const match = /^(#{1,6})(?:\s+(.*))?$/.exec(line.trimStart());
@@ -1411,6 +480,11 @@ function extractHeading(line) {
1411
480
  *
1412
481
  * @param line - The candidate line
1413
482
  * @returns The fence marker run and its language tag, or `undefined`
483
+ *
484
+ * @example
485
+ * ```ts
486
+ * extractFence('```ts') // { marker: '```', lang: 'ts' }
487
+ * ```
1414
488
  */
1415
489
  function extractFence(line) {
1416
490
  const match = /^\s*(`{3,}|~{3,})\s*(.*)$/.exec(line);
@@ -1431,6 +505,11 @@ function extractFence(line) {
1431
505
  *
1432
506
  * @param line - The candidate line
1433
507
  * @returns The list-item parts, or `undefined` when not a list item
508
+ *
509
+ * @example
510
+ * ```ts
511
+ * extractListItem('- item') // { ordered: false, start: 1, content: 'item', indent: 0, marker: 2 }
512
+ * ```
1434
513
  */
1435
514
  function extractListItem(line) {
1436
515
  const unordered = /^(\s*)([-*+])\s+(.*)$/.exec(line);
@@ -1464,6 +543,11 @@ function extractListItem(line) {
1464
543
  *
1465
544
  * @param line - A blockquote line (per {@link isQuote})
1466
545
  * @returns The line with its leading `>` (and one space) removed
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * stripQuote('> text') // 'text'
550
+ * ```
1467
551
  */
1468
552
  function stripQuote(line) {
1469
553
  return line.replace(/^\s{0,3}>\s?/, "");
@@ -1475,6 +559,11 @@ function stripQuote(line) {
1475
559
  *
1476
560
  * @param row - The raw table row line
1477
561
  * @returns The row's cells, in column order
562
+ *
563
+ * @example
564
+ * ```ts
565
+ * splitTableRow('|a|b|') // ['a', 'b']
566
+ * ```
1478
567
  */
1479
568
  function splitTableRow(row) {
1480
569
  const cells = [];
@@ -1501,6 +590,11 @@ function splitTableRow(row) {
1501
590
  *
1502
591
  * @param delimiter - The table's delimiter row
1503
592
  * @returns One alignment per column, in column order
593
+ *
594
+ * @example
595
+ * ```ts
596
+ * tableAlignments('| :--- | ---: |') // ['left', 'right']
597
+ * ```
1504
598
  */
1505
599
  function tableAlignments(delimiter) {
1506
600
  return splitTableRow(delimiter).map((cell) => {
@@ -1523,6 +617,11 @@ function tableAlignments(delimiter) {
1523
617
  * @param lines - The document's lines
1524
618
  * @param index - The line index to test
1525
619
  * @returns `true` when the line begins a different block
620
+ *
621
+ * @example
622
+ * ```ts
623
+ * startsBlock(['text', '## Heading'], 1) // true
624
+ * ```
1526
625
  */
1527
626
  function startsBlock(lines, index) {
1528
627
  const line = lines[index] ?? "";
@@ -1534,6 +633,11 @@ function startsBlock(lines, index) {
1534
633
  *
1535
634
  * @param text - The raw text possibly carrying `\x` escapes
1536
635
  * @returns The text with escapable `\x` reduced to `x`
636
+ *
637
+ * @example
638
+ * ```ts
639
+ * unescapeText('\\*hi\\*') // '*hi*'
640
+ * ```
1537
641
  */
1538
642
  function unescapeText(text) {
1539
643
  let out = "";
@@ -1552,6 +656,12 @@ function unescapeText(text) {
1552
656
  *
1553
657
  * @param nodes - The inline nodes (possibly with adjacent text runs)
1554
658
  * @returns The nodes with consecutive text nodes concatenated
659
+ *
660
+ * @example
661
+ * ```ts
662
+ * coalesceText([{ element: 'text', value: 'a' }, { element: 'text', value: 'b' }])
663
+ * // [{ element: 'text', value: 'ab' }]
664
+ * ```
1555
665
  */
1556
666
  function coalesceText(nodes) {
1557
667
  const out = [];
@@ -1575,6 +685,11 @@ function coalesceText(nodes) {
1575
685
  * @param start - The index of the opening backtick
1576
686
  * @param to - The exclusive end of the scan window
1577
687
  * @returns The span text + end index, or `undefined`
688
+ *
689
+ * @example
690
+ * ```ts
691
+ * scanCode('`code`', 0, 6) // { value: 'code', end: 6 }
692
+ * ```
1578
693
  */
1579
694
  function scanCode(source, start, to) {
1580
695
  let run = 0;
@@ -1608,6 +723,12 @@ function scanCode(source, start, to) {
1608
723
  * at {@link MAX_DEPTH} the link's text children degrade to literal text instead of
1609
724
  * recursing further
1610
725
  * @returns The parsed {@link LinkNode} + end index, or `undefined`
726
+ *
727
+ * @example
728
+ * ```ts
729
+ * scanLink('[text](url)', 0, 11)
730
+ * // { node: { element: 'link', href: 'url', children: [...] }, end: 11 }
731
+ * ```
1611
732
  */
1612
733
  function scanLink(source, start, to, depth = 0) {
1613
734
  let bracketDepth = 0;
@@ -1669,6 +790,12 @@ function scanLink(source, start, to, depth = 0) {
1669
790
  * at {@link MAX_DEPTH} the emphasis's children degrade to literal text instead of
1670
791
  * recursing further
1671
792
  * @returns The parsed {@link EmphasisNode} + end index, or `undefined`
793
+ *
794
+ * @example
795
+ * ```ts
796
+ * scanEmphasis('*em*', 0, 4)
797
+ * // { node: { element: 'emphasis', strong: false, children: [...] }, end: 4 }
798
+ * ```
1672
799
  */
1673
800
  function scanEmphasis(source, start, to, depth = 0) {
1674
801
  const marker = source[start] ?? "";
@@ -1721,6 +848,11 @@ function scanEmphasis(source, start, to, depth = 0) {
1721
848
  * it emits as a single literal text node - so pathological nesting (`[[[[…`,
1722
849
  * `****…`) cannot exhaust the call stack.
1723
850
  * @returns The parsed inline nodes (NOT yet coalesced)
851
+ *
852
+ * @example
853
+ * ```ts
854
+ * scanInline('hi *there*', 0, 10) // [{ element: 'text', value: 'hi ' }, { element: 'emphasis', ... }]
855
+ * ```
1724
856
  */
1725
857
  function scanInline(source, from, to, depth = 0) {
1726
858
  if (depth >= 64) return from < to ? [{
@@ -1789,6 +921,11 @@ function scanInline(source, from, to, depth = 0) {
1789
921
  *
1790
922
  * @param text - The raw text
1791
923
  * @returns The HTML-escaped text
924
+ *
925
+ * @example
926
+ * ```ts
927
+ * escapeHtml('<a>&"\'') // '&lt;a&gt;&amp;&quot;&#39;'
928
+ * ```
1792
929
  */
1793
930
  function escapeHtml(text) {
1794
931
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -1806,6 +943,12 @@ function escapeHtml(text) {
1806
943
  *
1807
944
  * @param href - The raw link destination
1808
945
  * @returns A safe, escaped `href` (empty when the scheme is unsafe or protocol-relative)
946
+ *
947
+ * @example
948
+ * ```ts
949
+ * sanitizeUrl('javascript:alert(1)') // ''
950
+ * sanitizeUrl('/path') // '/path'
951
+ * ```
1809
952
  */
1810
953
  function sanitizeUrl(href) {
1811
954
  let cleaned = "";
@@ -2304,6 +1447,11 @@ function flattenText(node) {
2304
1447
  * @param lines - The markdown lines to parse.
2305
1448
  * @param depth - The current recursion depth (blockquotes/lists increment it).
2306
1449
  * @returns The parsed block nodes.
1450
+ *
1451
+ * @example
1452
+ * ```ts
1453
+ * parseBlocks(['# Hi'], 0) // [{ element: 'heading', level: 1, children: [...] }]
1454
+ * ```
2307
1455
  */
2308
1456
  function parseBlocks(lines, depth) {
2309
1457
  if (depth >= 64) return lines.length > 0 ? [{
@@ -2395,6 +1543,11 @@ function parseBlocks(lines, depth) {
2395
1543
  * @param lines - The markdown lines to scan.
2396
1544
  * @param start - The index of the header row.
2397
1545
  * @returns The parsed table node and the index of the first line after it.
1546
+ *
1547
+ * @example
1548
+ * ```ts
1549
+ * collectTable(['| a |', '| - |'], 0) // { node: { element: 'table', ... }, next: 2 }
1550
+ * ```
2398
1551
  */
2399
1552
  function collectTable(lines, start) {
2400
1553
  const headerCells = splitTableRow(lines[start] ?? "");
@@ -2430,6 +1583,11 @@ function collectTable(lines, start) {
2430
1583
  * @param start - The index of the first list item.
2431
1584
  * @param depth - The current recursion depth (each item recurses at `depth + 1`).
2432
1585
  * @returns The parsed list node and the index of the first line after it.
1586
+ *
1587
+ * @example
1588
+ * ```ts
1589
+ * collectList(['- item'], 0, 0) // { node: { element: 'list', ... }, next: 1 }
1590
+ * ```
2433
1591
  */
2434
1592
  function collectList(lines, start, depth) {
2435
1593
  const first = extractListItem(lines[start] ?? "");