@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.
- package/dist/src/core/Markdown.d.ts +1 -1
- package/dist/src/core/factories.d.ts +2 -2
- package/dist/src/core/helpers.d.ts +90 -1
- package/dist/src/core/index.cjs +2056 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +8 -0
- package/dist/src/core/index.js +209 -1051
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/core/parsers.d.ts +16 -1
- package/dist/src/core/shapers.d.ts +25 -24
- package/dist/src/core/validators.d.ts +106 -9
- package/package.json +25 -15
package/dist/src/core/index.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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>&"\'') // '<a>&"''
|
|
928
|
+
* ```
|
|
1792
929
|
*/
|
|
1793
930
|
function escapeHtml(text) {
|
|
1794
931
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -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] ?? "");
|