@jarenjs/core 0.9.2 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +229 -26
- package/README.md +64 -10
- package/dist/types/array.d.ts +13 -0
- package/dist/types/cache.d.ts +166 -0
- package/dist/types/chunk.d.ts +102 -0
- package/dist/types/color.d.ts +64 -0
- package/dist/types/convert/convert.d.ts +33 -0
- package/dist/types/convert/currency.d.ts +47 -0
- package/dist/types/convert/index.d.ts +9 -0
- package/dist/types/convert/registry.d.ts +55 -0
- package/dist/types/dates/civil.d.ts +146 -0
- package/dist/types/dates/duration.d.ts +83 -0
- package/dist/types/dates/format.d.ts +59 -0
- package/dist/types/dates/index.d.ts +4 -0
- package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
- package/dist/types/errors.d.ts +77 -0
- package/dist/types/finance/amortization.d.ts +51 -0
- package/dist/types/finance/bond.d.ts +45 -0
- package/dist/types/finance/cashflow.d.ts +44 -0
- package/dist/types/finance/depreciation.d.ts +26 -0
- package/dist/types/finance/index.d.ts +16 -0
- package/dist/types/finance/indicators.d.ts +76 -0
- package/dist/types/finance/interest.d.ts +44 -0
- package/dist/types/finance/returns.d.ts +43 -0
- package/dist/types/finance/tvm.d.ts +50 -0
- package/dist/types/geo/angle.d.ts +4 -0
- package/dist/types/geo/bbox.d.ts +32 -0
- package/dist/types/geo/distance.d.ts +67 -0
- package/dist/types/geo/geohash.d.ts +63 -0
- package/dist/types/geo/geojson.d.ts +108 -0
- package/dist/types/geo/index-tree.d.ts +52 -0
- package/dist/types/geo/index.d.ts +11 -0
- package/dist/types/geo/mercator.d.ts +57 -0
- package/dist/types/geo/predicates.d.ts +44 -0
- package/dist/types/geo/ring.d.ts +80 -0
- package/dist/types/geo/simplify.d.ts +31 -0
- package/dist/types/geo/valid.d.ts +24 -0
- package/dist/types/geo/wkt.d.ts +16 -0
- package/dist/types/math/float64.d.ts +126 -3
- package/dist/types/math/format.d.ts +58 -0
- package/dist/types/math/index.d.ts +5 -0
- package/dist/types/math/mat4.d.ts +60 -0
- package/dist/types/math/project.d.ts +54 -0
- package/dist/types/math/solve.d.ts +67 -0
- package/dist/types/math/word.d.ts +82 -0
- package/dist/types/message.d.ts +52 -0
- package/dist/types/object.d.ts +151 -0
- package/dist/types/scan.d.ts +30 -0
- package/dist/types/schema.d.ts +24 -0
- package/dist/types/string.d.ts +96 -0
- package/dist/types/text/base64.d.ts +6 -4
- package/dist/types/text/email.d.ts +0 -1
- package/dist/types/text/host.d.ts +39 -8
- package/dist/types/text/i18n.d.ts +13 -1
- package/dist/types/text/index.d.ts +1 -0
- package/dist/types/text/misc.d.ts +15 -1
- package/dist/types/text/punycode.d.ts +45 -85
- package/docs/CONVERT.md +45 -0
- package/docs/DATES.md +78 -0
- package/docs/FINANCE.md +59 -0
- package/docs/GEO.md +114 -0
- package/docs/MATH.md +75 -0
- package/package.json +57 -4
- package/src/array.js +16 -0
- package/src/cache.js +206 -0
- package/src/chunk.js +159 -0
- package/src/color.js +125 -0
- package/src/convert/convert.js +59 -0
- package/src/convert/currency.js +74 -0
- package/src/convert/index.js +11 -0
- package/src/convert/registry.js +213 -0
- package/src/dates/civil.js +360 -0
- package/src/dates/duration.js +225 -0
- package/src/dates/format.js +238 -0
- package/src/dates/index.js +30 -0
- package/src/dates/rfc3339.js +621 -0
- package/src/errors.js +102 -0
- package/src/finance/amortization.js +50 -0
- package/src/finance/bond.js +100 -0
- package/src/finance/cashflow.js +122 -0
- package/src/finance/depreciation.js +54 -0
- package/src/finance/index.js +27 -0
- package/src/finance/indicators.js +207 -0
- package/src/finance/interest.js +63 -0
- package/src/finance/returns.js +90 -0
- package/src/finance/tvm.js +94 -0
- package/src/function.js +0 -2
- package/src/geo/angle.js +12 -0
- package/src/geo/bbox.js +80 -0
- package/src/geo/distance.js +155 -0
- package/src/geo/geohash.js +224 -0
- package/src/geo/geojson.js +361 -0
- package/src/geo/index-tree.js +294 -0
- package/src/geo/index.js +52 -0
- package/src/geo/mercator.js +124 -0
- package/src/geo/predicates.js +313 -0
- package/src/geo/ring.js +182 -0
- package/src/geo/simplify.js +130 -0
- package/src/geo/valid.js +142 -0
- package/src/geo/wkt.js +262 -0
- package/src/math/float64.js +228 -11
- package/src/math/format.js +157 -0
- package/src/math/index.js +5 -0
- package/src/math/mat4.js +131 -0
- package/src/math/project.js +49 -0
- package/src/math/solve.js +112 -0
- package/src/math/word.js +177 -0
- package/src/message.js +119 -0
- package/src/object.js +329 -0
- package/src/scan.js +42 -0
- package/src/schema.js +37 -0
- package/src/string.js +189 -0
- package/src/text/base64.js +15 -52
- package/src/text/email.js +0 -5
- package/src/text/host.js +515 -140
- package/src/text/i18n.js +13 -6
- package/src/text/identifiers.js +1 -1
- package/src/text/index.js +1 -0
- package/src/text/misc.js +81 -5
- package/src/text/punycode.js +235 -323
- 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
|
+
]);
|