@jarenjs/json 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 (74) hide show
  1. package/ARCHITECTURE.md +86 -13
  2. package/README.md +248 -23
  3. package/dist/types/canonical.d.ts +37 -0
  4. package/dist/types/cow.d.ts +28 -0
  5. package/dist/types/errors.d.ts +45 -0
  6. package/dist/types/index.d.ts +3 -0
  7. package/dist/types/jslt/errors.d.ts +15 -8
  8. package/dist/types/jslt/index.d.ts +22 -0
  9. package/dist/types/jslt/packs/finance.d.ts +119 -0
  10. package/dist/types/jslt/packs/index.d.ts +310 -0
  11. package/dist/types/jslt/packs/math.d.ts +159 -0
  12. package/dist/types/jslt/packs/stats.d.ts +48 -0
  13. package/dist/types/jslt/registry.d.ts +65 -0
  14. package/dist/types/jtlt/errors.d.ts +3 -6
  15. package/dist/types/option-variants.d.ts +29 -0
  16. package/dist/types/patch.d.ts +214 -0
  17. package/dist/types/path.d.ts +139 -9
  18. package/dist/types/pointer.d.ts +100 -9
  19. package/dist/types/query/compile.d.ts +12 -0
  20. package/dist/types/query/errors.d.ts +72 -8
  21. package/dist/types/query/index.d.ts +317 -25
  22. package/dist/types/query/normalize.d.ts +24 -0
  23. package/dist/types/query/operators.d.ts +241 -1
  24. package/dist/types/query/runtime.d.ts +5 -8
  25. package/dist/types/query/types.d.ts +34 -0
  26. package/dist/types/segments.d.ts +31 -0
  27. package/dist/types/write.d.ts +204 -0
  28. package/dist/types/xquery/parse.d.ts +2 -3
  29. package/docs/JSLT-FORMAT.md +74 -3
  30. package/docs/JSLT-PRELUDE.md +1 -1
  31. package/docs/QUERY-FORMAT.md +695 -33
  32. package/package.json +18 -4
  33. package/schemas/geojson.draft-07.schema.json +323 -0
  34. package/schemas/geojson.jaren.schema.json +863 -0
  35. package/schemas/geojson.schema.json +172 -0
  36. package/schemas/jaren-jslt.authoring.schema.json +142 -0
  37. package/schemas/jaren-jslt.draft-07.schema.json +152 -11
  38. package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
  39. package/schemas/jaren-jslt.schema.json +152 -11
  40. package/schemas/jaren-query.draft-07.schema.json +152 -11
  41. package/schemas/jaren-query.llm-profile.schema.json +619 -0
  42. package/schemas/jaren-query.schema.json +82 -15
  43. package/src/basic.js +1 -1
  44. package/src/canonical.js +170 -0
  45. package/src/cow.js +106 -0
  46. package/src/errors.js +68 -0
  47. package/src/index.js +3 -0
  48. package/src/jslt/dispatch.js +178 -28
  49. package/src/jslt/errors.js +19 -14
  50. package/src/jslt/index.js +37 -29
  51. package/src/jslt/packs/finance.js +49 -0
  52. package/src/jslt/packs/index.js +18 -0
  53. package/src/jslt/packs/math.js +46 -0
  54. package/src/jslt/packs/stats.js +65 -0
  55. package/src/jslt/registry.js +200 -0
  56. package/src/jslt/stylesheet.js +14 -23
  57. package/src/jtlt/desugar.js +2 -3
  58. package/src/jtlt/errors.js +6 -12
  59. package/src/jtlt/index.js +12 -29
  60. package/src/jtlt/template.js +9 -18
  61. package/src/option-variants.js +54 -0
  62. package/src/patch.js +1052 -0
  63. package/src/path.js +319 -52
  64. package/src/pointer.js +225 -44
  65. package/src/query/compile.js +790 -75
  66. package/src/query/errors.js +72 -12
  67. package/src/query/index.js +274 -42
  68. package/src/query/normalize.js +489 -78
  69. package/src/query/operators.js +620 -23
  70. package/src/query/runtime.js +5 -19
  71. package/src/query/types.js +213 -0
  72. package/src/segments.js +409 -64
  73. package/src/write.js +660 -0
  74. package/src/xquery/parse.js +37 -53
@@ -64,9 +64,10 @@
64
64
  "format": "json-path"
65
65
  },
66
66
  "variablePathString": {
67
- "description": "A variable-rooted path: '$name' with name matching [A-Za-z_][A-Za-z0-9_]*, optionally followed by RFC 9535 segments. Only the head is pattern-checked; the full segment grammar is enforced by the compiler (JQ0004).",
67
+ "description": "A variable-rooted path: '$name' with name matching [A-Za-z_][A-Za-z0-9_]*, optionally followed by RFC 9535 segments. The pattern checks the head; the json-path-segments format asserts the full segment grammar where format assertion is enabled (the compiler enforces it either way, JQ0004).",
68
68
  "type": "string",
69
- "pattern": "^\\$[A-Za-z_][A-Za-z0-9_]*($|[.\\[])"
69
+ "pattern": "^\\$[A-Za-z_][A-Za-z0-9_]*($|[.\\[])",
70
+ "format": "json-path-segments"
70
71
  },
71
72
  "arrayConstructor": {
72
73
  "description": "An array in expression position constructs an array; element results are concatenated per XQuery sequence flattening.",
@@ -79,6 +80,7 @@
79
80
  { "$ref": "#/$defs/mapConstructor" },
80
81
  { "$ref": "#/$defs/constPhrase" },
81
82
  { "$ref": "#/$defs/mapPhrase" },
83
+ { "$ref": "#/$defs/callPhrase" },
82
84
  { "$ref": "#/$defs/flworPhrase" },
83
85
  { "$ref": "#/$defs/somePhrase" },
84
86
  { "$ref": "#/$defs/everyPhrase" },
@@ -88,6 +90,8 @@
88
90
  { "$ref": "#/$defs/nonEmptyVariadicOperatorPhrase" },
89
91
  { "$ref": "#/$defs/conditionalArityOperatorPhrase" },
90
92
  { "$ref": "#/$defs/stringJoinPhrase" },
93
+ { "$ref": "#/$defs/geohashPhrase" },
94
+ { "$ref": "#/$defs/ternaryOperatorPhrase" },
91
95
  { "$ref": "#/$defs/replacePhrase" },
92
96
  { "$ref": "#/$defs/schemaOperatorPhrase" }
93
97
  ]
@@ -128,10 +132,25 @@
128
132
  "minItems": 2,
129
133
  "maxItems": 2
130
134
  },
135
+ "callPhrase": {
136
+ "description": "A registered trusted host function call: ['name', ...argument expressions]. The name must be registered at compile time (options.functions, JQ0010); sequences cross the boundary as arrays, a returned undefined is the empty sequence, a throwing function is JQ2010.",
137
+ "type": "object",
138
+ "properties": {
139
+ "$call": {
140
+ "type": "array",
141
+ "minItems": 1,
142
+ "prefixItems": [{ "type": "string", "minLength": 1 }],
143
+ "items": { "$ref": "#/$defs/expression" }
144
+ }
145
+ },
146
+ "required": ["$call"],
147
+ "additionalProperties": false
148
+ },
131
149
  "flworPhrase": {
132
- "description": "FLWOR phrase. Clauses apply in the fixed semantic order $for, $let, $as, $where, $groupby, $orderby, $count, $return regardless of JSON key order (D7); interleavings are expressed by nesting phrases. $return is required, plus at least one of $for/$let.",
150
+ "description": "FLWOR phrase. Clauses apply in the fixed semantic order $fold, $for, $let, $as, $where, $groupby, $orderby, $count, $return regardless of JSON key order (D7); interleavings are expressed by nesting phrases. $return is required, plus at least one of $fold/$for/$let. With $fold the phrase evaluates to the final accumulator instead of the collected $return sequence.",
133
151
  "type": "object",
134
152
  "properties": {
153
+ "$fold": { "$ref": "#/$defs/foldBinding" },
135
154
  "$for": { "$ref": "#/$defs/forBindings" },
136
155
  "$let": { "$ref": "#/$defs/bindingMap" },
137
156
  "$as": { "$ref": "#/$defs/asClause" },
@@ -144,16 +163,25 @@
144
163
  "required": ["$return"],
145
164
  "anyOf": [
146
165
  { "required": ["$for"] },
147
- { "required": ["$let"] }
166
+ { "required": ["$let"] },
167
+ { "required": ["$fold"] }
148
168
  ],
149
169
  "additionalProperties": false
150
170
  },
171
+ "foldBinding": {
172
+ "description": "The $fold accumulator: exactly one variable name mapped to the accumulator's initial value, evaluated once in the enclosing scope before the tuple stream starts. $return then names the accumulator's next value per surviving tuple.",
173
+ "type": "object",
174
+ "minProperties": 1,
175
+ "maxProperties": 1,
176
+ "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
177
+ "additionalProperties": { "$ref": "#/$defs/expression" }
178
+ },
151
179
  "variableName": {
152
180
  "type": "string",
153
181
  "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
154
182
  },
155
183
  "forBindings": {
156
- "description": "Iteration bindings: variable name to source expression or extended {$in, $at} binding. Bindings nest left-to-right in document key order and may be correlated; key-order-hostile producers should nest phrases instead.",
184
+ "description": "Iteration bindings: variable name to source expression or extended $in binding. Bindings nest left-to-right in document key order and may be correlated; key-order-hostile producers should nest phrases instead.",
157
185
  "type": "object",
158
186
  "minProperties": 1,
159
187
  "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
@@ -166,13 +194,17 @@
166
194
  ]
167
195
  },
168
196
  "extendedForBinding": {
169
- "description": "Extended $for binding: iterates $in and binds $at to the 0-based (D6) position variable.",
197
+ "description": "Extended $for binding. $in is the source; $at binds the 0-based (D6) position variable; $allowing-empty emits one tuple with the variable bound to the empty sequence (position -1) when the source yields no tuple; $window with $size, and optionally $step, iterates tumbling or sliding windows instead of items. Only $in is structurally required: the compiler rejects $size/$step without $window, and $window without $size (JQ0003).",
170
198
  "type": "object",
171
199
  "properties": {
172
200
  "$in": { "$ref": "#/$defs/expression" },
173
- "$at": { "$ref": "#/$defs/variableName" }
201
+ "$at": { "$ref": "#/$defs/variableName" },
202
+ "$allowing-empty": { "type": "boolean" },
203
+ "$window": { "enum": ["tumbling", "sliding"] },
204
+ "$size": { "type": "integer", "minimum": 1 },
205
+ "$step": { "type": "integer", "minimum": 1 }
174
206
  },
175
- "required": ["$in", "$at"],
207
+ "required": ["$in"],
176
208
  "additionalProperties": false
177
209
  },
178
210
  "bindingMap": {
@@ -209,12 +241,13 @@
209
241
  ]
210
242
  },
211
243
  "orderByKeySpec": {
212
- "description": "Explicit order key: direction defaults to 'asc', empty-sequence placement to 'least'.",
244
+ "description": "Explicit order key: direction defaults to 'asc', empty-sequence placement to 'least'. A $collation names a registered pure compare function (options.collations, JQ0010) applied to string keys; the default stays code-point order.",
213
245
  "type": "object",
214
246
  "properties": {
215
247
  "$key": { "$ref": "#/$defs/expression" },
216
248
  "$dir": { "enum": ["asc", "desc"] },
217
- "$empty": { "enum": ["least", "greatest"] }
249
+ "$empty": { "enum": ["least", "greatest"] },
250
+ "$collation": { "type": "string", "minLength": 1 }
218
251
  },
219
252
  "required": ["$key"],
220
253
  "additionalProperties": false
@@ -248,10 +281,15 @@
248
281
  "enum": [
249
282
  "$exists", "$empty", "$not", "$neg",
250
283
  "$count", "$sum", "$avg", "$min", "$max",
251
- "$distinct", "$reverse", "$sort", "$head", "$tail",
284
+ "$distinct", "$reverse", "$sort", "$head", "$tail", "$entries", "$from-entries",
252
285
  "$upper", "$lower", "$string-length", "$normalize-space",
253
286
  "$string", "$number", "$boolean",
254
- "$is-string", "$is-number", "$is-boolean", "$is-null", "$is-array", "$is-object"
287
+ "$is-string", "$is-number", "$is-boolean", "$is-null", "$is-array", "$is-object",
288
+ "$is-date", "$is-time", "$is-datetime", "$is-duration",
289
+ "$year", "$month", "$day", "$hours", "$minutes", "$seconds",
290
+ "$offset", "$epoch", "$datetime",
291
+ "$week", "$week-year", "$quarter", "$weekday",
292
+ "$bbox", "$area", "$length", "$centroid"
255
293
  ]
256
294
  },
257
295
  "additionalProperties": { "$ref": "#/$defs/expression" }
@@ -266,7 +304,9 @@
266
304
  "$eq", "$ne", "$lt", "$le", "$gt", "$ge",
267
305
  "$add", "$sub", "$mul", "$div", "$idiv", "$mod",
268
306
  "$contains", "$starts-with", "$ends-with", "$match", "$search",
269
- "$index-of", "$range", "$get", "$default"
307
+ "$index-of", "$range", "$get",
308
+ "$start-of", "$end-of", "$date-format",
309
+ "$distance", "$within", "$bbox-intersects", "$default"
270
310
  ]
271
311
  },
272
312
  "additionalProperties": {
@@ -304,12 +344,12 @@
304
344
  }
305
345
  },
306
346
  "conditionalArityOperatorPhrase": {
307
- "description": "Operators taking two or three operand expressions: $if [cond, then, else?], $substring [str, start, len?], $subsequence [seq, start, len?].",
347
+ "description": "Operators taking two or three operand expressions: $if [cond, then, else?], $substring [str, start, len?], $subsequence [seq, start, len?], $date-add / $date-sub [date, duration] or [date, amount, unit].",
308
348
  "type": "object",
309
349
  "minProperties": 1,
310
350
  "maxProperties": 1,
311
351
  "propertyNames": {
312
- "enum": ["$if", "$substring", "$subsequence"]
352
+ "enum": ["$if", "$substring", "$subsequence", "$date-add", "$date-sub"]
313
353
  },
314
354
  "additionalProperties": {
315
355
  "type": "array",
@@ -318,6 +358,20 @@
318
358
  "maxItems": 3
319
359
  }
320
360
  },
361
+ "geohashPhrase": {
362
+ "description": "$geohash [value] or [value, precision]: a GeoJSON value as a base-32 cell string. Precision is an integer from 1 to 12 (default 9); a value that is not a bare position is represented by its centroid.",
363
+ "type": "object",
364
+ "properties": {
365
+ "$geohash": {
366
+ "type": "array",
367
+ "items": { "$ref": "#/$defs/expression" },
368
+ "minItems": 1,
369
+ "maxItems": 2
370
+ }
371
+ },
372
+ "required": ["$geohash"],
373
+ "additionalProperties": false
374
+ },
321
375
  "stringJoinPhrase": {
322
376
  "description": "$string-join [seq, separator?]: one or two operand expressions.",
323
377
  "type": "object",
@@ -332,6 +386,19 @@
332
386
  "required": ["$string-join"],
333
387
  "additionalProperties": false
334
388
  },
389
+ "ternaryOperatorPhrase": {
390
+ "description": "Operators whose value is an array of exactly three operand expressions: $date-diff [from, to, unit].",
391
+ "type": "object",
392
+ "minProperties": 1,
393
+ "maxProperties": 1,
394
+ "propertyNames": { "enum": ["$date-diff"] },
395
+ "additionalProperties": {
396
+ "type": "array",
397
+ "items": { "$ref": "#/$defs/expression" },
398
+ "minItems": 3,
399
+ "maxItems": 3
400
+ }
401
+ },
335
402
  "replacePhrase": {
336
403
  "description": "$replace [input, pattern, replacement]: exactly three operand expressions (I-Regexp has no flags argument, D5).",
337
404
  "type": "object",
package/src/basic.js CHANGED
@@ -199,7 +199,7 @@ export function isValidRelativeJSONPointer(str) {
199
199
  // \.[a-zA-Z_][a-zA-Z0-9_]*| - dot notation (e.g., $.store)
200
200
  // \.[*]| - dot wildcard (e.g., $.*)
201
201
  // \[\s*(?:'[^']*'|"[^"]*"|\d+|\*|\?[^\]]*|\d*:\d*(?::\d*)?)\s*\] - bracket notation
202
- const CONST_REGEXP_JSONPATH = /^(\$|@)(?:\.\.[a-zA-Z_][a-zA-Z0-9_]*|\.\.|\.[a-zA-Z_][a-zA-Z0-9_]*|\.[*]|\[\s*(?:'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\d+|\*|\?[^\]]*|\d*:\d*(?::\d*)?|\d+(?:\s*,\s*\d+)*|'(?:[^'\\]|\\.)*'(?:\s*,\s*'(?:[^'\\]|\\.)*')*|\"(?:[^"\\]|\\.)*\"(?:\s*,\s*\"(?:[^"\\]|\\.)*\")*)\s*\])*$/;
202
+ const CONST_REGEXP_JSONPATH = /^(\$|@)(?:\.\.[a-zA-Z_][a-zA-Z0-9_]*|\.\.|\.[a-zA-Z_][a-zA-Z0-9_]*|\.[*]|\[\s*(?:'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\d+|\*|\?[^\]]*|\d*:\d*(?::\d*)?|\d+(?:\s*,\s*\d+)*|'(?:[^'\\]|\\.)*'(?:\s*,\s*'(?:[^'\\]|\\.)*')*|"(?:[^"\\]|\\.)*"(?:\s*,\s*"(?:[^"\\]|\\.)*")*)\s*\])*$/;
203
203
 
204
204
  /**
205
205
  * Validates a JSONPath expression string per RFC 9535.
@@ -0,0 +1,170 @@
1
+ //#region Canonical JSON (RFC 8785 / JCS)
2
+ // JSON Canonicalization Scheme
3
+ // https://www.rfc-editor.org/rfc/rfc8785.html
4
+ //
5
+ // Deterministic serialization for hashing and signing: two structurally
6
+ // equal JSON values always produce byte-identical output, so a hash or
7
+ // signature over the text is a hash or signature over the value.
8
+ //
9
+ // The package already had two stable serializers before this one, and
10
+ // neither is JCS. Both stay as they are, because both are load-bearing
11
+ // where they live:
12
+ //
13
+ // - `stableKeyString` (query/runtime.js) is a grouping/ordering KEY.
14
+ // It emits `NaN` and `Infinity` by name so that NaN groups with NaN
15
+ // per the XQuery grouping rule - which JCS forbids outright, since
16
+ // neither is a JSON number.
17
+ // - `stableStringify` (@jarenjs/core/object) is a cache/memo
18
+ // fingerprint. It follows `JSON.stringify` conventions: `NaN`
19
+ // becomes `null`, `undefined` members are dropped.
20
+ //
21
+ // Silently dropping or coercing a value is exactly right for a memo key
22
+ // and exactly wrong for a signature, so this module rejects instead:
23
+ // every input that is not JSON data is an error, never a guess.
24
+
25
+ import { encodeJSONPointerSegment } from './pointer.js';
26
+
27
+ const hasOwn = Object.hasOwn;
28
+
29
+ // In a Unicode-mode pattern a well-formed surrogate pair is one code
30
+ // point, so this matches only UNPAIRED surrogates.
31
+ const RE_LONE_SURROGATE = /\p{Surrogate}/u;
32
+
33
+ /**
34
+ * Error thrown when a value cannot be canonicalized because it is not
35
+ * JSON data (RFC 8785 section 3.2.1). `dataPath` is an RFC 6901 JSON
36
+ * Pointer to the offending value, so a rejected document says which
37
+ * member was at fault rather than only that one was.
38
+ *
39
+ * Extends `TypeError`: passing a non-JSON value to a canonicalizer is a
40
+ * programming error, the same class `JSON.stringify` raises for a
41
+ * BigInt or a cycle.
42
+ */
43
+ export class JsonCanonicalizeError extends TypeError {
44
+ constructor(message, dataPath) {
45
+ super(`${message} at ${dataPath === '' ? 'the document root' : dataPath}`);
46
+ this.name = 'JsonCanonicalizeError';
47
+ this.dataPath = dataPath;
48
+ }
49
+ }
50
+
51
+ function fail(message, dataPath) {
52
+ throw new JsonCanonicalizeError(message, dataPath);
53
+ }
54
+
55
+ // RFC 6901 escaping for the diagnostic pointer only.
56
+ function appendPath(path, token) {
57
+ return path + '/' + encodeJSONPointerSegment(token);
58
+ }
59
+
60
+ function serializeString(value, path) {
61
+ if (RE_LONE_SURROGATE.test(value))
62
+ fail('a string with an unpaired surrogate is not valid Unicode', path);
63
+ // Section 3.2.2.2 is the JSON string escape discipline with the
64
+ // shortest possible escapes and no escaping of non-ASCII - which is
65
+ // precisely what ECMAScript's JSON.stringify emits for a string.
66
+ return JSON.stringify(value);
67
+ }
68
+
69
+ function serializeNumber(value, path) {
70
+ if (!Number.isFinite(value))
71
+ fail(`${value === value ? String(value) : 'NaN'} is not a JSON number`, path);
72
+ // Section 3.2.2.3 mandates ECMAScript's Number::toString, which is
73
+ // what String() is - including `-0` serializing as `0`.
74
+ return String(value);
75
+ }
76
+
77
+ function serializeValue(value, path, stack) {
78
+ if (value === null)
79
+ return 'null';
80
+
81
+ switch (typeof value) {
82
+ case 'boolean':
83
+ return value ? 'true' : 'false';
84
+ case 'number':
85
+ return serializeNumber(value, path);
86
+ case 'string':
87
+ return serializeString(value, path);
88
+ case 'object':
89
+ break;
90
+ case 'undefined':
91
+ return fail('undefined is not a JSON value', path);
92
+ case 'bigint':
93
+ return fail('a BigInt is not a JSON number', path);
94
+ default: // 'function', 'symbol'
95
+ return fail(`a ${typeof value} is not a JSON value`, path);
96
+ }
97
+
98
+ if (stack.has(value))
99
+ fail('a circular reference cannot be canonicalized', path);
100
+ stack.add(value);
101
+
102
+ let out;
103
+ if (Array.isArray(value)) {
104
+ out = '[';
105
+ for (let i = 0; i < value.length; i++) {
106
+ if (i > 0)
107
+ out += ',';
108
+ // an array hole reads as undefined, which is not a JSON value
109
+ out += serializeValue(value[i], path + '/' + i, stack);
110
+ }
111
+ out += ']';
112
+ }
113
+ else {
114
+ // A signing primitive must not guess. A class instance would walk
115
+ // as its own enumerable fields (a Date as `{}`, a Map as `{}`),
116
+ // which is a silently wrong signature rather than a loud error, so
117
+ // only plain objects are JSON objects here. `toJSON` is
118
+ // deliberately not consulted for the same reason: the value that
119
+ // gets signed is the value that was passed in.
120
+ const proto = Object.getPrototypeOf(value);
121
+ if (proto !== null && proto !== Object.prototype)
122
+ fail(`a ${value.constructor?.name ?? 'non-plain'} instance is not a JSON object`, path);
123
+ // Section 3.2.3: members sorted by the UTF-16 code units of their
124
+ // names, which is what the default string sort compares.
125
+ const keys = Object.keys(value).sort();
126
+ out = '{';
127
+ for (let i = 0; i < keys.length; i++) {
128
+ const key = keys[i];
129
+ /* c8 ignore next 2 -- Object.keys only yields own keys */
130
+ if (!hasOwn(value, key))
131
+ continue;
132
+ if (i > 0)
133
+ out += ',';
134
+ out += serializeString(key, path) + ':'
135
+ + serializeValue(value[key], appendPath(path, key), stack);
136
+ }
137
+ out += '}';
138
+ }
139
+
140
+ stack.delete(value);
141
+ return out;
142
+ }
143
+
144
+ /**
145
+ * Serialize a JSON value to its canonical form (RFC 8785 / JCS): object
146
+ * members sorted by the UTF-16 code units of their names, numbers in
147
+ * the ECMAScript `Number::toString` form, strings with the shortest
148
+ * JSON escapes and no escaping of non-ASCII, and no insignificant
149
+ * whitespace. Two structurally equal values always produce identical
150
+ * text, which is what makes the result safe to hash or sign.
151
+ *
152
+ * Unlike `JSON.stringify`, nothing is dropped or coerced: `undefined`,
153
+ * a function, a symbol, a BigInt, `NaN`, `Infinity`, an unpaired
154
+ * surrogate, a circular reference and a class instance are all errors,
155
+ * because a canonicalizer that quietly rewrote its input would produce
156
+ * a signature over a document nobody sent.
157
+ *
158
+ * @param {any} value - The JSON value to canonicalize
159
+ * @returns {string} The canonical JSON text
160
+ * @throws {JsonCanonicalizeError} When the value is not JSON data
161
+ * @example
162
+ * canonicalizeJson({ b: 1, a: [2, 3] }); // '{"a":[2,3],"b":1}'
163
+ * canonicalizeJson({ 'ä': 1, 'a': 2 }); // '{"a":2,"ä":1}'
164
+ * canonicalizeJson(1e21); // '1e+21'
165
+ */
166
+ export function canonicalizeJson(value) {
167
+ return serializeValue(value, '', new Set());
168
+ }
169
+
170
+ //#endregion
package/src/cow.js ADDED
@@ -0,0 +1,106 @@
1
+ //#region copy-on-write machinery (package-internal)
2
+ // The shared copy-on-write core of the JSON Patch engine (patch.js) and
3
+ // the standalone write operations (write.js). Like segments.js, this
4
+ // module is package-internal and is deliberately not listed in the
5
+ // package exports. The value-level primitives it builds on
6
+ // (isJsonContainer, shallowCloneJson, cloneJson) are pure JSON helpers
7
+ // and live in @jarenjs/core/object.
8
+ //
9
+ // An application of writes carries a state `{ root, owned }` where
10
+ // `owned` is the set of nodes this application created and may mutate
11
+ // freely (`null` = in-place mode: every node is owned). The first write
12
+ // along a path shallow-clones the spine from the root down; later writes
13
+ // find the spine in the owned set and mutate the clones in place. The
14
+ // input document is never touched.
15
+ //
16
+ // Step encoding shared by the walk/read helpers: a location is a pair of
17
+ // parallel arrays `names`/`indexes`.
18
+ // - RFC 6901 form (patch.js, pointer targets): `names[i]` is the
19
+ // decoded token, `indexes[i]` its pre-scanned array-index form
20
+ // (-1 = not a valid index). One token, two forms.
21
+ // - Typed form (JSONPath-derived targets): a name selector stores
22
+ // `names[i] = name, indexes[i] = -1`; an index selector stores
23
+ // `names[i] = null, indexes[i] = index` (negative = from the end).
24
+ // `readSteps` resolves either form; the callers own their walk loops so
25
+ // each module raises its own error types.
26
+
27
+ import { isJsonContainer, shallowCloneJson } from '@jarenjs/core/object';
28
+
29
+ const hasOwn = Object.hasOwn;
30
+
31
+ /**
32
+ * The mutable state of one write application. `owned` is the set of
33
+ * nodes this application created and may mutate freely; `null` means
34
+ * in-place mode (every node is owned).
35
+ */
36
+ export function makeState(root, owned) {
37
+ return { root, owned };
38
+ }
39
+
40
+ /** Ensure the root is owned before the first write into it. */
41
+ export function ownedRoot(state) {
42
+ const root = state.root;
43
+ const owned = state.owned;
44
+ if (owned === null || !isJsonContainer(root) || owned.has(root))
45
+ return root;
46
+ const clone = shallowCloneJson(root);
47
+ owned.add(clone);
48
+ state.root = clone;
49
+ return clone;
50
+ }
51
+
52
+ /**
53
+ * Return an owned version of `child`, writing the clone back into the
54
+ * (already owned) parent slot when one is taken.
55
+ */
56
+ export function ownedChild(state, parent, child, key) {
57
+ const owned = state.owned;
58
+ if (owned === null || !isJsonContainer(child) || owned.has(child))
59
+ return child;
60
+ const clone = shallowCloneJson(child);
61
+ owned.add(clone);
62
+ // the slot was just read through hasOwn/index, so plain assignment
63
+ // never reaches a prototype '__proto__' setter
64
+ parent[key] = clone;
65
+ return clone;
66
+ }
67
+
68
+ /**
69
+ * The concrete array index a step addresses in `parent`, or -1 when the
70
+ * step cannot address an array element. Typed index steps resolve
71
+ * negative indexes from the end; the RFC 6901 form never does.
72
+ */
73
+ export function stepArrayIndex(parent, name, index) {
74
+ if (name === null)
75
+ return index < 0 ? parent.length + index : index;
76
+ return index;
77
+ }
78
+
79
+ /**
80
+ * Read the location `steps[0..len)` in `root` without cloning anything.
81
+ * Returns the shared NOTHING-style `miss` sentinel value passed in when
82
+ * the location does not exist (callers pick their own sentinel).
83
+ */
84
+ export function readSteps(root, names, indexes, len, miss) {
85
+ let v = root;
86
+ for (let i = 0; i < len; i++) {
87
+ if (Array.isArray(v)) {
88
+ const idx = stepArrayIndex(v, names[i], indexes[i]);
89
+ if (idx < 0 || idx >= v.length)
90
+ return miss;
91
+ v = v[idx];
92
+ }
93
+ else if (typeof v === 'object' && v !== null) {
94
+ const name = names[i];
95
+ if (name === null || !hasOwn(v, name))
96
+ return miss;
97
+ v = v[name];
98
+ }
99
+ else {
100
+ return miss;
101
+ }
102
+ }
103
+ return v;
104
+ }
105
+
106
+ //#endregion
package/src/errors.js ADDED
@@ -0,0 +1,68 @@
1
+ //#region Jaren shared error bases
2
+ // Shared constructors for the coded error classes of the json package.
3
+ // Each engine keeps its own exported class (the `name` and `message`
4
+ // shapes are public API); these bases only centralize the constructor
5
+ // bodies that were byte-identical across engines. The `name` is passed
6
+ // as a string literal because the bundle is minified and a mangled
7
+ // class name must not leak into `error.name`.
8
+
9
+ import { CodedError } from '@jarenjs/core/errors';
10
+
11
+ /**
12
+ * Base for errors that carry a stable `code` and a `docPath`, an RFC
13
+ * 6901 JSON Pointer into the offending document. A wrapped error is
14
+ * exposed through the native `cause` option when one is given. This is
15
+ * the positional adapter over `@jarenjs/core`'s {@link CodedError}: a
16
+ * trailing `undefined` cause means "no cause" (no own property), per
17
+ * the base's contract.
18
+ */
19
+ export class CodedDocPathError extends CodedError {
20
+ /**
21
+ * @param {string} name - The public class name for `error.name`
22
+ * @param {string} code - Stable diagnosis code
23
+ * @param {string} reason - What is wrong (bare; the message is composed)
24
+ * @param {string} docPath - JSON Pointer into the offending document
25
+ * @param {unknown} [cause] - Wrapped error, when there is one
26
+ */
27
+ constructor(name, code, reason, docPath, cause = undefined) {
28
+ super(name, code, reason, docPath,
29
+ cause === undefined ? undefined : { cause });
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Base for syntax errors over a source string: `source` and `position`
35
+ * locate the offending token, and the message is prefixed with a
36
+ * human-readable language label. Deliberately NOT a {@link CodedError}:
37
+ * this family carries no stable code and locates by source offset, not
38
+ * by document pointer.
39
+ */
40
+ export class LabeledSyntaxError extends SyntaxError {
41
+ /**
42
+ * @param {string} name - The public class name for `error.name`
43
+ * @param {string} label - Language label for the message prefix
44
+ * @param {string} message - What is wrong
45
+ * @param {string} source - The offending source text
46
+ * @param {number} position - 0-based index of the offending token
47
+ */
48
+ constructor(name, label, message, source, position) {
49
+ super(`Invalid ${label}: ${message} at position ${position} in '${source}'`);
50
+ this.name = name;
51
+ this.source = source;
52
+ this.position = position;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Build a `fail(code, message, docPath)` throw-adapter for a coded
58
+ * error class; compilers use it to keep rejection one-liners readable.
59
+ * @param {new (code: string, message: string, docPath: string) => Error} ErrorClass
60
+ * @returns {(code: string, message: string, docPath: string) => never}
61
+ */
62
+ export function failerFor(ErrorClass) {
63
+ return function fail(code, message, docPath) {
64
+ throw new ErrorClass(code, message, docPath);
65
+ };
66
+ }
67
+
68
+ //#endregion
package/src/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from './basic.js';
2
+ export * from './canonical.js';
2
3
  export * from './pointer.js';
4
+ export * from './patch.js';
3
5
  export * from './path.js';
6
+ export * from './write.js';
4
7
  export * from './query/index.js';