@jarenjs/core 0.9.2 → 0.34.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/ARCHITECTURE.md +229 -26
  2. package/README.md +64 -10
  3. package/dist/types/array.d.ts +13 -0
  4. package/dist/types/cache.d.ts +166 -0
  5. package/dist/types/chunk.d.ts +102 -0
  6. package/dist/types/color.d.ts +64 -0
  7. package/dist/types/convert/convert.d.ts +33 -0
  8. package/dist/types/convert/currency.d.ts +47 -0
  9. package/dist/types/convert/index.d.ts +9 -0
  10. package/dist/types/convert/registry.d.ts +55 -0
  11. package/dist/types/dates/civil.d.ts +146 -0
  12. package/dist/types/dates/duration.d.ts +83 -0
  13. package/dist/types/dates/format.d.ts +59 -0
  14. package/dist/types/dates/index.d.ts +4 -0
  15. package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
  16. package/dist/types/errors.d.ts +77 -0
  17. package/dist/types/finance/amortization.d.ts +51 -0
  18. package/dist/types/finance/bond.d.ts +45 -0
  19. package/dist/types/finance/cashflow.d.ts +44 -0
  20. package/dist/types/finance/depreciation.d.ts +26 -0
  21. package/dist/types/finance/index.d.ts +16 -0
  22. package/dist/types/finance/indicators.d.ts +76 -0
  23. package/dist/types/finance/interest.d.ts +44 -0
  24. package/dist/types/finance/returns.d.ts +43 -0
  25. package/dist/types/finance/tvm.d.ts +50 -0
  26. package/dist/types/geo/angle.d.ts +4 -0
  27. package/dist/types/geo/bbox.d.ts +32 -0
  28. package/dist/types/geo/distance.d.ts +67 -0
  29. package/dist/types/geo/geohash.d.ts +63 -0
  30. package/dist/types/geo/geojson.d.ts +108 -0
  31. package/dist/types/geo/index-tree.d.ts +52 -0
  32. package/dist/types/geo/index.d.ts +11 -0
  33. package/dist/types/geo/mercator.d.ts +57 -0
  34. package/dist/types/geo/predicates.d.ts +44 -0
  35. package/dist/types/geo/ring.d.ts +80 -0
  36. package/dist/types/geo/simplify.d.ts +31 -0
  37. package/dist/types/geo/valid.d.ts +24 -0
  38. package/dist/types/geo/wkt.d.ts +16 -0
  39. package/dist/types/math/float64.d.ts +126 -3
  40. package/dist/types/math/format.d.ts +58 -0
  41. package/dist/types/math/index.d.ts +5 -0
  42. package/dist/types/math/mat4.d.ts +60 -0
  43. package/dist/types/math/project.d.ts +54 -0
  44. package/dist/types/math/solve.d.ts +67 -0
  45. package/dist/types/math/word.d.ts +82 -0
  46. package/dist/types/message.d.ts +52 -0
  47. package/dist/types/object.d.ts +151 -0
  48. package/dist/types/scan.d.ts +30 -0
  49. package/dist/types/schema.d.ts +24 -0
  50. package/dist/types/string.d.ts +96 -0
  51. package/dist/types/text/base64.d.ts +6 -4
  52. package/dist/types/text/email.d.ts +0 -1
  53. package/dist/types/text/host.d.ts +39 -8
  54. package/dist/types/text/i18n.d.ts +13 -1
  55. package/dist/types/text/index.d.ts +1 -0
  56. package/dist/types/text/misc.d.ts +15 -1
  57. package/dist/types/text/punycode.d.ts +45 -85
  58. package/docs/CONVERT.md +45 -0
  59. package/docs/DATES.md +78 -0
  60. package/docs/FINANCE.md +59 -0
  61. package/docs/GEO.md +114 -0
  62. package/docs/MATH.md +75 -0
  63. package/package.json +57 -4
  64. package/src/array.js +16 -0
  65. package/src/cache.js +206 -0
  66. package/src/chunk.js +159 -0
  67. package/src/color.js +125 -0
  68. package/src/convert/convert.js +59 -0
  69. package/src/convert/currency.js +74 -0
  70. package/src/convert/index.js +11 -0
  71. package/src/convert/registry.js +213 -0
  72. package/src/dates/civil.js +360 -0
  73. package/src/dates/duration.js +225 -0
  74. package/src/dates/format.js +238 -0
  75. package/src/dates/index.js +30 -0
  76. package/src/dates/rfc3339.js +621 -0
  77. package/src/errors.js +102 -0
  78. package/src/finance/amortization.js +50 -0
  79. package/src/finance/bond.js +100 -0
  80. package/src/finance/cashflow.js +122 -0
  81. package/src/finance/depreciation.js +54 -0
  82. package/src/finance/index.js +27 -0
  83. package/src/finance/indicators.js +207 -0
  84. package/src/finance/interest.js +63 -0
  85. package/src/finance/returns.js +90 -0
  86. package/src/finance/tvm.js +94 -0
  87. package/src/function.js +0 -2
  88. package/src/geo/angle.js +12 -0
  89. package/src/geo/bbox.js +80 -0
  90. package/src/geo/distance.js +155 -0
  91. package/src/geo/geohash.js +224 -0
  92. package/src/geo/geojson.js +361 -0
  93. package/src/geo/index-tree.js +294 -0
  94. package/src/geo/index.js +52 -0
  95. package/src/geo/mercator.js +124 -0
  96. package/src/geo/predicates.js +313 -0
  97. package/src/geo/ring.js +182 -0
  98. package/src/geo/simplify.js +130 -0
  99. package/src/geo/valid.js +142 -0
  100. package/src/geo/wkt.js +262 -0
  101. package/src/math/float64.js +228 -11
  102. package/src/math/format.js +157 -0
  103. package/src/math/index.js +5 -0
  104. package/src/math/mat4.js +131 -0
  105. package/src/math/project.js +49 -0
  106. package/src/math/solve.js +112 -0
  107. package/src/math/word.js +177 -0
  108. package/src/message.js +119 -0
  109. package/src/object.js +329 -0
  110. package/src/scan.js +42 -0
  111. package/src/schema.js +37 -0
  112. package/src/string.js +189 -0
  113. package/src/text/base64.js +15 -52
  114. package/src/text/email.js +0 -5
  115. package/src/text/host.js +515 -140
  116. package/src/text/i18n.js +13 -6
  117. package/src/text/identifiers.js +1 -1
  118. package/src/text/index.js +1 -0
  119. package/src/text/misc.js +81 -5
  120. package/src/text/punycode.js +235 -323
  121. package/src/dates.js +0 -371
package/src/message.js ADDED
@@ -0,0 +1,119 @@
1
+ //@ts-check
2
+
3
+ /**
4
+ * Human-message templating: the shared half of every message catalog in
5
+ * the suite.
6
+ *
7
+ * A catalog is a plain flat object keyed by a stable message id, whose
8
+ * entries are either render closures or template strings. Compiling one
9
+ * turns every template string into a closure, so the consumer only ever
10
+ * calls `catalog[msgid](params)` — the two-stage house rule applied to
11
+ * messages: parse the template once, render many times.
12
+ *
13
+ * This lives in `@jarenjs/core` so that packages which must not depend on
14
+ * each other can still speak the identical catalog contract. That is what
15
+ * lets one locale pack service both the validator's document voice ("must
16
+ * have required property 'x'") and the form layer's field voice ("This
17
+ * field is required") without either package importing the other.
18
+ */
19
+
20
+ /**
21
+ * Render one interpolated parameter: `String(v)` for primitives,
22
+ * `JSON.stringify(v)` for objects and arrays.
23
+ *
24
+ * @param {unknown} value - The parameter value
25
+ * @returns {string} The rendered value
26
+ */
27
+ export function formatTemplateParam(value) {
28
+ return (value !== null && typeof value === 'object')
29
+ ? JSON.stringify(value)
30
+ : String(value);
31
+ }
32
+
33
+ /**
34
+ * Render a JSON value for quotation inside a message: strings keep their
35
+ * quotes so an empty or space-padded value is visible, everything else is
36
+ * JSON.
37
+ *
38
+ * @param {unknown} value - The value to render
39
+ * @returns {string}
40
+ */
41
+ export function formatMessageValue(value) {
42
+ return typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
43
+ }
44
+
45
+ /**
46
+ * Compile a message template into a render closure. Template syntax:
47
+ * `{name}` substitutes the params member `name`; an unknown name leaves
48
+ * the placeholder literally (so a catalog typo shows up in the output
49
+ * instead of rendering as `undefined`); `{{` escapes a literal `{`.
50
+ *
51
+ * @param {string} template - The template text
52
+ * @returns {(params: object, error?: object) => string} The compiled render closure
53
+ */
54
+ export function compileMessageTemplate(template) {
55
+ /** @type {string[]} literal parts between placeholders */
56
+ const parts = [];
57
+ /** @type {string[]} placeholder names, one per gap between parts */
58
+ const names = [];
59
+ let literal = '';
60
+ for (let i = 0; i < template.length; ++i) {
61
+ if (template.charCodeAt(i) === 0x7b /* { */) {
62
+ if (template.charCodeAt(i + 1) === 0x7b) {
63
+ literal += '{';
64
+ i += 1;
65
+ continue;
66
+ }
67
+ const end = template.indexOf('}', i + 1);
68
+ if (end === -1) {
69
+ literal += template.slice(i);
70
+ break;
71
+ }
72
+ parts.push(literal);
73
+ literal = '';
74
+ names.push(template.slice(i + 1, end));
75
+ i = end;
76
+ continue;
77
+ }
78
+ literal += template[i];
79
+ }
80
+ parts.push(literal);
81
+
82
+ if (names.length === 0) {
83
+ const text = parts[0];
84
+ return function renderLiteralTemplate() { return text; };
85
+ }
86
+
87
+ return function renderMessageTemplate(params) {
88
+ let out = parts[0];
89
+ for (let i = 0; i < names.length; ++i) {
90
+ const name = names[i];
91
+ out += (params != null && name in params)
92
+ ? formatTemplateParam(params[name])
93
+ : `{${name}}`;
94
+ out += parts[i + 1];
95
+ }
96
+ return out;
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Compile a catalog-like object into a functions-only frozen catalog.
102
+ * Entries may be render closures (kept as-is) or template strings
103
+ * (compiled through {@link compileMessageTemplate}).
104
+ *
105
+ * @param {Record<string, string | ((params: object, error?: object) => string)>} catalogLike - The catalog to compile
106
+ * @returns {Readonly<Record<string, (params: object, error?: object) => string>>} The compiled catalog
107
+ */
108
+ export function compileMessageCatalog(catalogLike) {
109
+ /** @type {Record<string, (params: object, error?: object) => string>} */
110
+ const compiled = {};
111
+ const keys = Object.keys(catalogLike);
112
+ for (let i = 0; i < keys.length; ++i) {
113
+ const entry = catalogLike[keys[i]];
114
+ compiled[keys[i]] = typeof entry === 'function'
115
+ ? entry
116
+ : compileMessageTemplate(String(entry));
117
+ }
118
+ return Object.freeze(compiled);
119
+ }
package/src/object.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  isBooleanType,
7
7
  isTypedArray,
8
8
  } from './index.js';
9
+ import { compareCodePoints, hashContent } from './string.js';
9
10
 
10
11
  const hasOwn = Object.hasOwn;
11
12
 
@@ -155,6 +156,31 @@ export function equalsJson(a, b) {
155
156
  return count === 0;
156
157
  }
157
158
 
159
+ /**
160
+ * Ordering of two JSON values per RFC 9535 section 2.3.5.2.2 — the
161
+ * ordering half of the comparison family whose equality half is
162
+ * `equalsJson`.
163
+ *
164
+ * Only two numbers or two strings order at all: numbers by value,
165
+ * strings by Unicode scalar values (`compareCodePoints`, not the native
166
+ * `<`). Every other pair — mismatched types, objects, arrays, booleans,
167
+ * null — is simply `false` in both directions, never an error.
168
+ *
169
+ * @param {any} a
170
+ * @param {any} b
171
+ * @param {boolean} [orEqual] when true test `<=` instead of `<`
172
+ * @returns {boolean}
173
+ */
174
+ export function compareJsonScalarLt(a, b, orEqual = false) {
175
+ if (typeof a === 'number')
176
+ return typeof b === 'number' && (orEqual ? a <= b : a < b);
177
+ if (typeof a === 'string')
178
+ return typeof b === 'string' && (orEqual
179
+ ? compareCodePoints(a, b) <= 0
180
+ : compareCodePoints(a, b) < 0);
181
+ return false;
182
+ }
183
+
158
184
  /**
159
185
  * Check if all items in an array are unique using deep equality
160
186
  * @param {any[]} arr - The array to check
@@ -213,6 +239,309 @@ export function isUniqueDeepArray(arr) {
213
239
  return true;
214
240
  }
215
241
 
242
+ /**
243
+ * Deterministic JSON text for plain data: like `JSON.stringify`, but
244
+ * object keys are emitted in sorted order at every depth, so two
245
+ * structurally equal values always produce the same string (a stable
246
+ * cache/memo/fingerprint key regardless of key insertion order).
247
+ * Non-JSON values follow `JSON.stringify` semantics (undefined members
248
+ * are dropped, undefined roots return undefined).
249
+ *
250
+ * That leniency is what makes it a memo key and not an interchange
251
+ * format: dropping a member changes the document. For output that is
252
+ * hashed or signed, use `canonicalizeJson` (`@jarenjs/json/canonical`,
253
+ * RFC 8785), which rejects every non-JSON input instead of coercing it.
254
+ * @param {*} value - The value to serialize
255
+ * @returns {string|undefined} Deterministic JSON text
256
+ */
257
+ export function stableStringify(value) {
258
+ if (value === null || typeof value !== 'object')
259
+ return JSON.stringify(value);
260
+ if (Array.isArray(value)) {
261
+ let out = '[';
262
+ for (let i = 0; i < value.length; ++i)
263
+ out += (i === 0 ? '' : ',') + (stableStringify(value[i]) ?? 'null');
264
+ return out + ']';
265
+ }
266
+ const keys = Object.keys(value).sort();
267
+ let out = '{';
268
+ let first = true;
269
+ for (const key of keys) {
270
+ const sv = stableStringify(value[key]);
271
+ if (sv === undefined)
272
+ continue;
273
+ out += (first ? '' : ',') + JSON.stringify(key) + ':' + sv;
274
+ first = false;
275
+ }
276
+ return out + '}';
277
+ }
278
+
279
+ /**
280
+ * The suite's one MEMO-GRADE content key: `hashContent(stableStringify
281
+ * (value) ?? '')`. Two structurally equal plain-JSON values produce the
282
+ * same key regardless of property insertion order — which is exactly
283
+ * what a reconciliation key wants and exactly what `JSON.stringify`-based
284
+ * keys get wrong.
285
+ *
286
+ * **This is a 32-bit FINGERPRINT, never an identity.** Distinct values
287
+ * DO share a key — the birthday bound puts the first collision around
288
+ * 65k documents, and one turns up after ~113k trivially different query
289
+ * documents in practice. So it is sound for a vnode `key`, a DOM id, a
290
+ * bucket index or a diagnostic label, and WRONG as the sole identity of
291
+ * anything whose reuse changes a result: a compiled query, a query plan,
292
+ * a registered SQL function body, a memoized render. For those use
293
+ * {@link semanticKey}, which compares the whole serialization.
294
+ *
295
+ * Two further properties a caller must know, inherited from
296
+ * `stableStringify`: **`undefined` members are dropped** (two values
297
+ * differing only in an `undefined` member share a key) and there is
298
+ * **no cycle guard** (a cyclic value overflows the stack). Both are fine
299
+ * for a fingerprint and wrong for a checksum — for anything hashed,
300
+ * signed or recorded, use `canonicalizeJson`
301
+ * (`@jarenjs/json/canonical`, RFC 8785) instead. Do not conflate the
302
+ * three.
303
+ * @param {*} value - The value to derive a fingerprint for
304
+ * @returns {string} base-36 content hash of the stable serialization
305
+ */
306
+ export function contentKey(value) {
307
+ return hashContent(stableStringify(value) ?? '');
308
+ }
309
+
310
+ /**
311
+ * The raw tokens {@link semanticKey} uses for the values JSON text cannot
312
+ * tell apart. Each is emitted UNQUOTED, which alone is enough that no
313
+ * string can forge one — `JSON.stringify` always puts quotes around a
314
+ * string. The leading NUL is belt and braces: `JSON.stringify` escapes it
315
+ * to `\u0000` inside a string, so a token cannot occur in serialized text
316
+ * at all.
317
+ *
318
+ * Written as `\u0000` escapes on purpose. The same character as a literal
319
+ * byte is invisible in every editor and diff and makes tooling treat the
320
+ * file as binary, so a test pins that it stays an escape.
321
+ */
322
+ const SEMANTIC_TOKENS = {
323
+ undefined: '\u0000undef',
324
+ nan: '\u0000nan',
325
+ posInfinity: '\u0000+inf',
326
+ negInfinity: '\u0000-inf',
327
+ // -0 needs no sentinel: JSON text for the NUMBER -0 is `0`, so this
328
+ // two-character form is already unreachable as a number's key
329
+ negZero: '-0',
330
+ };
331
+
332
+ /**
333
+ * Serialize one node of a semantic key, or throw when the value cannot
334
+ * be keyed injectively.
335
+ * @param {*} value
336
+ * @param {string} path - JSON-Pointer-ish trail, for the error message
337
+ * @param {Set<object>} open - Ancestors on the current path (cycle guard)
338
+ * @returns {string}
339
+ */
340
+ function semanticToken(value, path, open) {
341
+ const refuse = (what) => {
342
+ throw new TypeError(
343
+ `semanticKey: ${what} at ${path === '' ? 'the root' : path} cannot be a cache identity`);
344
+ };
345
+ if (value === null) return 'null';
346
+ switch (typeof value) {
347
+ case 'boolean': return value ? 'true' : 'false';
348
+ case 'string': return JSON.stringify(value);
349
+ case 'number':
350
+ if (Number.isNaN(value)) return SEMANTIC_TOKENS.nan;
351
+ if (value === Infinity) return SEMANTIC_TOKENS.posInfinity;
352
+ if (value === -Infinity) return SEMANTIC_TOKENS.negInfinity;
353
+ // -0 and 0 are one token in JSON text and two values to every
354
+ // arithmetic the engine performs (1/-0 is -Infinity)
355
+ return Object.is(value, -0) ? SEMANTIC_TOKENS.negZero : String(value);
356
+ case 'undefined': return SEMANTIC_TOKENS.undefined;
357
+ case 'bigint': return `\u0000big${value}`;
358
+ case 'function': return refuse('a function');
359
+ case 'symbol': return refuse('a symbol');
360
+ default: break;
361
+ }
362
+ const object = /** @type {object} */ (value);
363
+ if (open.has(object)) refuse('a cycle');
364
+ const proto = Object.getPrototypeOf(object);
365
+ // a symbol-keyed member is data the serialization cannot show, so two
366
+ // values differing only there would share an identity
367
+ if (Object.getOwnPropertySymbols(object).length > 0)
368
+ refuse('a symbol-keyed member');
369
+ open.add(object);
370
+ let out;
371
+ if (Array.isArray(object)) {
372
+ // a subclass carries behavior the key cannot see
373
+ if (proto !== Array.prototype) refuse('an Array subclass instance');
374
+ const items = /** @type {any[]} */ (object);
375
+ // an own property beyond the elements would vanish positionally
376
+ for (const key of Object.keys(items)) {
377
+ const index = Number(key);
378
+ if (!Number.isInteger(index) || index < 0 || index >= items.length)
379
+ refuse(`the extra array property ${JSON.stringify(key)}`);
380
+ }
381
+ out = '[';
382
+ for (let i = 0; i < items.length; i++)
383
+ out += (i === 0 ? '' : ',') + semanticToken(items[i], `${path}/${i}`, open);
384
+ out += ']';
385
+ }
386
+ else {
387
+ // Date, Map, Set, RegExp and every class instance stringify to `{}`
388
+ // through `Object.keys` — a whole family collapsing onto one key
389
+ if (proto !== Object.prototype && proto !== null)
390
+ refuse(`a ${object.constructor?.name ?? 'non-plain'} instance`);
391
+ const keys = Object.keys(object).sort(compareCodePoints);
392
+ out = '{';
393
+ for (let i = 0; i < keys.length; i++) {
394
+ const key = keys[i];
395
+ out += (i === 0 ? '' : ',') + JSON.stringify(key) + ':'
396
+ + semanticToken(/** @type {any} */ (object)[key], `${path}/${key}`, open);
397
+ }
398
+ out += '}';
399
+ }
400
+ open.delete(object);
401
+ return out;
402
+ }
403
+
404
+ /**
405
+ * The suite's one COLLISION-FREE semantic key: the COMPLETE
406
+ * deterministic serialization of a plain-data value. Two values share a
407
+ * key exactly when they are structurally equal, so a cache keyed by it
408
+ * can never serve one document's compiled semantics for another —
409
+ * which a hash-only key inevitably does (see {@link contentKey}).
410
+ *
411
+ * Use it wherever reuse changes a RESULT: compiled queries, query
412
+ * plans, load specifications, safe profiles, registered SQL function
413
+ * bodies, memoized renders. The key is longer than a fingerprint; for a
414
+ * bounded cache of a few hundred entries that cost is a few kilobytes
415
+ * and the alternative is wrong data.
416
+ *
417
+ * Injective over plain data, and STRICT about the rest: it distinguishes
418
+ * `-0` from `0`, `NaN`/`±Infinity` from `null` and from each other, and
419
+ * a present-but-`undefined` member from an absent one — every case
420
+ * `stableStringify` silently folds together. Values that cannot be
421
+ * keyed injectively are REFUSED with a `TypeError` rather than folded:
422
+ * functions, symbols, cycles, and non-plain objects (a `Date`, `Map`,
423
+ * `RegExp` or class instance, all of which serialize to `{}`), plus the
424
+ * two members a serialization cannot show — a symbol key, and an own
425
+ * array property past the last element. A caller that may hold such a
426
+ * value must treat the refusal as "not cacheable" and compute afresh —
427
+ * never as "reuse whatever shares the key".
428
+ *
429
+ * The identity covers OWN ENUMERABLE string-keyed properties, the same
430
+ * surface JSON reads. Two values differing only in a non-enumerable
431
+ * member are one value to this key, as they are to `JSON.stringify`.
432
+ * @param {*} value - The value to derive an identity for
433
+ * @returns {string} the complete deterministic serialization
434
+ * @throws {TypeError} When the value cannot be keyed injectively
435
+ */
436
+ export function semanticKey(value) {
437
+ return semanticToken(value, '', new Set());
438
+ }
439
+
440
+ /**
441
+ * True for a JSON object — a non-null object that is not an array.
442
+ *
443
+ * This is the JSON data-model predicate, deliberately distinct from
444
+ * `isObjectType`: it treats `Map`, `Set`, `Date` and every other class
445
+ * instance as an object too, because at the JSON layer such a value has
446
+ * already been rejected or serialized before it gets here, and the only
447
+ * distinction that matters is object-vs-array.
448
+ *
449
+ * @param {any} value
450
+ * @returns {boolean}
451
+ */
452
+ export function isJsonObject(value) {
453
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
454
+ }
455
+
456
+ /**
457
+ * Assign a member so that a key named `__proto__` becomes an own data
458
+ * property instead of reassigning the object's prototype. Every builder
459
+ * that turns untrusted names into members must go through this — a plain
460
+ * `out[name] = value` is a prototype-pollution hole for that one name.
461
+ *
462
+ * @param {Object} out target object
463
+ * @param {string} name member name, possibly attacker-controlled
464
+ * @param {any} value
465
+ */
466
+ export function setObjectMember(out, name, value) {
467
+ if (name === '__proto__') {
468
+ Object.defineProperty(out, name, {
469
+ value,
470
+ enumerable: true,
471
+ configurable: true,
472
+ writable: true,
473
+ });
474
+ }
475
+ else {
476
+ out[name] = value;
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Whether a value can hold JSON members: an object or an array, not null.
482
+ * The complement of a JSON scalar.
483
+ * @param {any} value
484
+ * @returns {boolean}
485
+ */
486
+ export function isJsonContainer(value) {
487
+ return typeof value === 'object' && value !== null;
488
+ }
489
+
490
+ /**
491
+ * Copy one JSON container one level deep, preserving member order. Object
492
+ * spread copies an own `__proto__` data property as an own property
493
+ * (CreateDataProperty semantics), so this is pollution-safe.
494
+ * @template T
495
+ * @param {T} value - The container to copy
496
+ * @returns {T} A shallow copy; scalars are returned unchanged
497
+ */
498
+ export function shallowCloneJson(value) {
499
+ return Array.isArray(value) ? value.slice() : { ...value };
500
+ }
501
+
502
+ /**
503
+ * Deep-copy a JSON value. Scalars are returned as-is; containers are
504
+ * rebuilt so no part of the result is shared with the input.
505
+ * @template T
506
+ * @param {T} value - The JSON value to copy
507
+ * @returns {T} A deep copy sharing no container with the input
508
+ */
509
+ export function cloneJson(value) {
510
+ if (!isJsonContainer(value))
511
+ return value;
512
+ if (Array.isArray(value)) {
513
+ const len = value.length;
514
+ const out = new Array(len);
515
+ for (let i = 0; i < len; i++)
516
+ out[i] = cloneJson(value[i]);
517
+ return out;
518
+ }
519
+ const out = {};
520
+ for (const key in value) {
521
+ if (Object.hasOwn(value, key))
522
+ setObjectMember(out, key, cloneJson(value[key]));
523
+ }
524
+ return out;
525
+ }
526
+
527
+ /**
528
+ * Recursively `Object.freeze` a value and everything reachable from it,
529
+ * returning the value. Scalars pass through untouched. Assumes an acyclic
530
+ * structure (a JSON value); a cycle would recurse forever.
531
+ *
532
+ * @template T
533
+ * @param {T} value
534
+ * @returns {T} the same value, deeply frozen
535
+ */
536
+ export function deepFreeze(value) {
537
+ if (typeof value !== 'object' || value === null)
538
+ return value;
539
+ const keys = Object.keys(value);
540
+ for (let i = 0; i < keys.length; i++)
541
+ deepFreeze(value[keys[i]]);
542
+ return Object.freeze(value);
543
+ }
544
+
216
545
  /**
217
546
  *
218
547
  * @param {Map<any, any>} map
package/src/scan.js CHANGED
@@ -21,6 +21,7 @@ export const CC_SQUOTE = 0x27;
21
21
  export const CC_LPAREN = 0x28;
22
22
  export const CC_RPAREN = 0x29;
23
23
  export const CC_STAR = 0x2A;
24
+ export const CC_PLUS = 0x2B;
24
25
  export const CC_COMMA = 0x2C;
25
26
  export const CC_MINUS = 0x2D;
26
27
  export const CC_DOT = 0x2E;
@@ -38,8 +39,14 @@ export const CC_LBRACKET = 0x5B;
38
39
  export const CC_BACKSLASH = 0x5C;
39
40
  export const CC_RBRACKET = 0x5D;
40
41
  export const CC_UNDERSCORE = 0x5F;
42
+ export const CC_LOWER_B = 0x62;
43
+ export const CC_LOWER_O = 0x6F;
44
+ export const CC_LOWER_X = 0x78;
45
+ export const CC_LBRACE = 0x7B;
41
46
  export const CC_PIPE = 0x7C;
47
+ export const CC_RBRACE = 0x7D;
42
48
  export const CC_TILDE = 0x7E;
49
+ export const CC_DEL = 0x7F;
43
50
 
44
51
  //#endregion
45
52
 
@@ -65,6 +72,32 @@ export function isHexDigitCode(c) {
65
72
  || (c >= 0x61 && c <= 0x66); // a-f
66
73
  }
67
74
 
75
+ /**
76
+ * Checks if a char code may start an unquoted name: an ASCII letter,
77
+ * `_`, or any non-ASCII code unit. Non-ASCII is admitted wholesale rather
78
+ * than by Unicode category, which is what the RFC 9535 shorthand member
79
+ * name and the XML Name productions both settle for in practice.
80
+ * @param {number} c - The char code
81
+ * @returns {boolean}
82
+ */
83
+ export function isNameStartCode(c) {
84
+ return (c >= 0x41 && c <= 0x5A) // A-Z
85
+ || (c >= 0x61 && c <= 0x7A) // a-z
86
+ || c === CC_UNDERSCORE
87
+ || c >= 0x80; // any non-ASCII code unit
88
+ }
89
+
90
+ /**
91
+ * Checks if a char code may continue an unquoted name: a name start or an
92
+ * ASCII digit. Grammars that also admit `-`/`.` inside names (XML Name,
93
+ * for one) test those separately on top of this.
94
+ * @param {number} c - The char code
95
+ * @returns {boolean}
96
+ */
97
+ export function isNameCharCode(c) {
98
+ return isNameStartCode(c) || isDigitCode(c);
99
+ }
100
+
68
101
  /**
69
102
  * Checks if a char code is blank space per RFC 9535 (space, tab,
70
103
  * line feed or carriage return).
@@ -93,4 +126,13 @@ export function isAsciiUpperCode(c) {
93
126
  return c >= 0x41 && c <= 0x5A;
94
127
  }
95
128
 
129
+ /**
130
+ * Checks if a char code is an ASCII letter (A-Z or a-z).
131
+ * @param {number} c - The char code
132
+ * @returns {boolean}
133
+ */
134
+ export function isAsciiLetterCode(c) {
135
+ return isAsciiUpperCode(c) || isAsciiLowerCode(c);
136
+ }
137
+
96
138
  //#endregion
package/src/schema.js ADDED
@@ -0,0 +1,37 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The JSON Schema constraint-keyword vocabulary, grouped by the
4
+ * value family each keyword constrains. Before this file the same
5
+ * keyword lists were spelled four times (forms' constraint extraction,
6
+ * emit's dropped-constraint table, the validator's `$data` dispatch
7
+ * order and its `$ref`-sibling detection) — and a keyword added to one
8
+ * list silently missed the others. Each site composes the list it wants
9
+ * from these groups and appends its own extras; the loops stay where
10
+ * they are, because the drift risk was always in the data, not the
11
+ * code.
12
+ *
13
+ * ORDER IS PART OF THE CONTRACT: the validator's `$data` dispatch
14
+ * applies keywords in list order and its error order is observable
15
+ * behaviour, so the internal order of each group is fixed. Membership
16
+ * consumers are order-insensitive by construction.
17
+ */
18
+
19
+ /** Keywords constraining numeric values, in dispatch order. */
20
+ export const NUMERIC_CONSTRAINTS = Object.freeze([
21
+ 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
22
+ ]);
23
+
24
+ /** Keywords constraining string values, in dispatch order. */
25
+ export const STRING_CONSTRAINTS = Object.freeze([
26
+ 'minLength', 'maxLength', 'pattern', 'format',
27
+ ]);
28
+
29
+ /** Keywords constraining array values, in dispatch order. */
30
+ export const ARRAY_CONSTRAINTS = Object.freeze([
31
+ 'minItems', 'maxItems', 'uniqueItems',
32
+ ]);
33
+
34
+ /** Keywords constraining object values, in dispatch order. */
35
+ export const OBJECT_CONSTRAINTS = Object.freeze([
36
+ 'minProperties', 'maxProperties',
37
+ ]);