@jarenjs/json 0.9.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 (57) hide show
  1. package/ARCHITECTURE.md +175 -0
  2. package/LICENSE +21 -0
  3. package/README.md +471 -0
  4. package/dist/types/basic.d.ts +32 -0
  5. package/dist/types/index.d.ts +4 -0
  6. package/dist/types/jslt/dispatch.d.ts +11 -0
  7. package/dist/types/jslt/errors.d.ts +18 -0
  8. package/dist/types/jslt/index.d.ts +53 -0
  9. package/dist/types/jslt/stylesheet.d.ts +8 -0
  10. package/dist/types/jtlt/desugar.d.ts +19 -0
  11. package/dist/types/jtlt/errors.d.ts +18 -0
  12. package/dist/types/jtlt/index.d.ts +57 -0
  13. package/dist/types/jtlt/template.d.ts +8 -0
  14. package/dist/types/jtlt/writer.d.ts +6 -0
  15. package/dist/types/path.d.ts +235 -0
  16. package/dist/types/pointer.d.ts +114 -0
  17. package/dist/types/query/compile.d.ts +21 -0
  18. package/dist/types/query/errors.d.ts +18 -0
  19. package/dist/types/query/index.d.ts +70 -0
  20. package/dist/types/query/normalize.d.ts +68 -0
  21. package/dist/types/query/operators.d.ts +424 -0
  22. package/dist/types/query/runtime.d.ts +93 -0
  23. package/dist/types/segments.d.ts +62 -0
  24. package/dist/types/xquery/index.d.ts +19 -0
  25. package/dist/types/xquery/parse.d.ts +20 -0
  26. package/docs/JSLT-FORMAT.md +861 -0
  27. package/docs/JSLT-PRELUDE.md +159 -0
  28. package/docs/JTLT-FORMAT.md +659 -0
  29. package/docs/QUERY-FORMAT.md +1221 -0
  30. package/docs/XQUERY-FRONTEND.md +321 -0
  31. package/package.json +81 -0
  32. package/schemas/jaren-jslt.draft-07.schema.json +776 -0
  33. package/schemas/jaren-jslt.schema.json +776 -0
  34. package/schemas/jaren-query.draft-07.schema.json +613 -0
  35. package/schemas/jaren-query.schema.json +375 -0
  36. package/src/basic.js +300 -0
  37. package/src/index.js +4 -0
  38. package/src/jslt/dispatch.js +934 -0
  39. package/src/jslt/errors.js +34 -0
  40. package/src/jslt/index.js +121 -0
  41. package/src/jslt/stylesheet.js +234 -0
  42. package/src/jtlt/desugar.js +231 -0
  43. package/src/jtlt/errors.js +34 -0
  44. package/src/jtlt/index.js +155 -0
  45. package/src/jtlt/template.js +130 -0
  46. package/src/jtlt/writer.js +110 -0
  47. package/src/path.js +977 -0
  48. package/src/pointer.js +453 -0
  49. package/src/query/compile.js +817 -0
  50. package/src/query/errors.js +33 -0
  51. package/src/query/index.js +150 -0
  52. package/src/query/normalize.js +1047 -0
  53. package/src/query/operators.js +1253 -0
  54. package/src/query/runtime.js +233 -0
  55. package/src/segments.js +627 -0
  56. package/src/xquery/index.js +35 -0
  57. package/src/xquery/parse.js +1647 -0
@@ -0,0 +1,375 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://jarenjs.dev/schemas/jaren-query/0.1",
4
+ "title": "Jaren JSON Query format 0.1",
5
+ "description": "Structural grammar of Jaren JSON Query documents (see packages/json/docs/QUERY-FORMAT.md). This schema is structural validation only: the compiler remains authoritative for semantic rules it cannot express - FLWOR clause keys apply in the fixed semantic order $for, $let, $as, $where, $groupby, $orderby, $count, $return regardless of JSON key order; variable scoping, duplicate-binding detection, external-parameter collection and $as name binding are compiler rules (JQ0005/JQ0007); variable-rooted path strings are only head-checked here (full RFC 9535 segment grammar is JQ0004); embedded JSON Schema literals are taken verbatim and never meta-validated here (the type-test compiler is authoritative, JQ0008/JQ0009); per-argument runtime typing is JQ2xxx. Authored in a draft-neutral keyword subset; the draft-07 twin is a mechanical derivation.",
6
+ "allOf": [
7
+ { "$ref": "#/$defs/queryDocument" }
8
+ ],
9
+ "$defs": {
10
+ "queryDocument": {
11
+ "description": "A query document is either the version envelope or a bare expression (a bare RFC 9535 JSONPath string is the degenerate query).",
12
+ "oneOf": [
13
+ { "$ref": "#/$defs/versionEnvelope" },
14
+ { "$ref": "#/$defs/expression" }
15
+ ]
16
+ },
17
+ "versionEnvelope": {
18
+ "description": "Top-level version envelope. Only recognized at the top level of a query document.",
19
+ "type": "object",
20
+ "properties": {
21
+ "$query": { "const": "0.1" },
22
+ "$expr": { "$ref": "#/$defs/expression" }
23
+ },
24
+ "required": ["$query", "$expr"],
25
+ "additionalProperties": false
26
+ },
27
+ "expression": {
28
+ "description": "Any expression per the encoding rules: scalars are literals, strings split on the leading '$', arrays are array constructors, objects partition into map constructors (no $-prefixed key) and operator phrases (all keys $-prefixed).",
29
+ "oneOf": [
30
+ { "$ref": "#/$defs/scalarLiteral" },
31
+ { "$ref": "#/$defs/stringExpression" },
32
+ { "$ref": "#/$defs/arrayConstructor" },
33
+ { "$ref": "#/$defs/objectExpression" }
34
+ ]
35
+ },
36
+ "scalarLiteral": {
37
+ "description": "Numbers, booleans and null are literal items.",
38
+ "type": ["null", "boolean", "number"]
39
+ },
40
+ "stringExpression": {
41
+ "description": "Rule 2: a string starting with '$' is a query expression ('$$' escapes a literal); any other string is a literal.",
42
+ "type": "string",
43
+ "anyOf": [
44
+ { "$ref": "#/$defs/literalString" },
45
+ { "$ref": "#/$defs/escapedStringLiteral" },
46
+ { "$ref": "#/$defs/absolutePathString" },
47
+ { "$ref": "#/$defs/variablePathString" }
48
+ ]
49
+ },
50
+ "literalString": {
51
+ "description": "A string that does not start with '$' denotes itself.",
52
+ "type": "string",
53
+ "pattern": "^([^$]|$)"
54
+ },
55
+ "escapedStringLiteral": {
56
+ "description": "A string starting with '$$' denotes the literal string with one leading '$' dropped.",
57
+ "type": "string",
58
+ "pattern": "^\\$\\$"
59
+ },
60
+ "absolutePathString": {
61
+ "description": "An absolute RFC 9535 JSONPath query rooted at the input document: '$' alone or starting '$.', '$[' or '$..'. The json-path format asserts the full RFC 9535 grammar where format assertion is enabled (format is annotation-only by default from draft 2020-12).",
62
+ "type": "string",
63
+ "pattern": "^\\$($|[.\\[])",
64
+ "format": "json-path"
65
+ },
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).",
68
+ "type": "string",
69
+ "pattern": "^\\$[A-Za-z_][A-Za-z0-9_]*($|[.\\[])"
70
+ },
71
+ "arrayConstructor": {
72
+ "description": "An array in expression position constructs an array; element results are concatenated per XQuery sequence flattening.",
73
+ "type": "array",
74
+ "items": { "$ref": "#/$defs/expression" }
75
+ },
76
+ "objectExpression": {
77
+ "description": "Rule 1: objects partition by $-prefixed keys. A mixed object matches neither branch and is invalid (JQ0001).",
78
+ "oneOf": [
79
+ { "$ref": "#/$defs/mapConstructor" },
80
+ { "$ref": "#/$defs/constPhrase" },
81
+ { "$ref": "#/$defs/mapPhrase" },
82
+ { "$ref": "#/$defs/flworPhrase" },
83
+ { "$ref": "#/$defs/somePhrase" },
84
+ { "$ref": "#/$defs/everyPhrase" },
85
+ { "$ref": "#/$defs/unaryOperatorPhrase" },
86
+ { "$ref": "#/$defs/binaryOperatorPhrase" },
87
+ { "$ref": "#/$defs/variadicOperatorPhrase" },
88
+ { "$ref": "#/$defs/nonEmptyVariadicOperatorPhrase" },
89
+ { "$ref": "#/$defs/conditionalArityOperatorPhrase" },
90
+ { "$ref": "#/$defs/stringJoinPhrase" },
91
+ { "$ref": "#/$defs/replacePhrase" },
92
+ { "$ref": "#/$defs/schemaOperatorPhrase" }
93
+ ]
94
+ },
95
+ "mapConstructor": {
96
+ "description": "An object with no $-prefixed key: keys are literal member names, values are expressions. Use $map or $const for member names that start with '$'.",
97
+ "type": "object",
98
+ "patternProperties": {
99
+ "^\\$": false
100
+ },
101
+ "additionalProperties": { "$ref": "#/$defs/expression" }
102
+ },
103
+ "constPhrase": {
104
+ "description": "Quote: the value is returned verbatim, nothing inside is evaluated.",
105
+ "type": "object",
106
+ "properties": {
107
+ "$const": true
108
+ },
109
+ "required": ["$const"],
110
+ "additionalProperties": false
111
+ },
112
+ "mapPhrase": {
113
+ "description": "General map constructor for computed keys or keys starting with '$'. Each entry is a [keyExpr, valueExpr] pair; keyExpr must evaluate to a single string at runtime (JQ2004); later pairs win on duplicate keys.",
114
+ "type": "object",
115
+ "properties": {
116
+ "$map": {
117
+ "type": "array",
118
+ "items": { "$ref": "#/$defs/mapEntry" }
119
+ }
120
+ },
121
+ "required": ["$map"],
122
+ "additionalProperties": false
123
+ },
124
+ "mapEntry": {
125
+ "description": "One [keyExpr, valueExpr] pair of a $map phrase.",
126
+ "type": "array",
127
+ "items": { "$ref": "#/$defs/expression" },
128
+ "minItems": 2,
129
+ "maxItems": 2
130
+ },
131
+ "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.",
133
+ "type": "object",
134
+ "properties": {
135
+ "$for": { "$ref": "#/$defs/forBindings" },
136
+ "$let": { "$ref": "#/$defs/bindingMap" },
137
+ "$as": { "$ref": "#/$defs/asClause" },
138
+ "$where": { "$ref": "#/$defs/expression" },
139
+ "$groupby": { "$ref": "#/$defs/bindingMap" },
140
+ "$orderby": { "$ref": "#/$defs/orderBySpec" },
141
+ "$count": { "$ref": "#/$defs/variableName" },
142
+ "$return": { "$ref": "#/$defs/expression" }
143
+ },
144
+ "required": ["$return"],
145
+ "anyOf": [
146
+ { "required": ["$for"] },
147
+ { "required": ["$let"] }
148
+ ],
149
+ "additionalProperties": false
150
+ },
151
+ "variableName": {
152
+ "type": "string",
153
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
154
+ },
155
+ "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.",
157
+ "type": "object",
158
+ "minProperties": 1,
159
+ "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
160
+ "additionalProperties": { "$ref": "#/$defs/forSource" }
161
+ },
162
+ "forSource": {
163
+ "oneOf": [
164
+ { "$ref": "#/$defs/expression" },
165
+ { "$ref": "#/$defs/extendedForBinding" }
166
+ ]
167
+ },
168
+ "extendedForBinding": {
169
+ "description": "Extended $for binding: iterates $in and binds $at to the 0-based (D6) position variable.",
170
+ "type": "object",
171
+ "properties": {
172
+ "$in": { "$ref": "#/$defs/expression" },
173
+ "$at": { "$ref": "#/$defs/variableName" }
174
+ },
175
+ "required": ["$in", "$at"],
176
+ "additionalProperties": false
177
+ },
178
+ "bindingMap": {
179
+ "description": "Variable name to expression map, used by $let, $groupby and quantifier bindings.",
180
+ "type": "object",
181
+ "minProperties": 1,
182
+ "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
183
+ "additionalProperties": { "$ref": "#/$defs/expression" }
184
+ },
185
+ "orderBySpec": {
186
+ "description": "A single key spec or a major-to-minor list of key specs. An array value is by specification ALWAYS a list of key specs, so the single form excludes arrays; write a single array-constructor key as {\"$key\": [...]}.",
187
+ "oneOf": [
188
+ { "$ref": "#/$defs/singleOrderByKey" },
189
+ {
190
+ "type": "array",
191
+ "items": { "$ref": "#/$defs/orderByKey" },
192
+ "minItems": 1
193
+ }
194
+ ]
195
+ },
196
+ "singleOrderByKey": {
197
+ "description": "A key spec in non-list position: any non-array expression, or the explicit {$key, $dir, $empty} form.",
198
+ "oneOf": [
199
+ { "$ref": "#/$defs/scalarLiteral" },
200
+ { "$ref": "#/$defs/stringExpression" },
201
+ { "$ref": "#/$defs/objectExpression" },
202
+ { "$ref": "#/$defs/orderByKeySpec" }
203
+ ]
204
+ },
205
+ "orderByKey": {
206
+ "oneOf": [
207
+ { "$ref": "#/$defs/expression" },
208
+ { "$ref": "#/$defs/orderByKeySpec" }
209
+ ]
210
+ },
211
+ "orderByKeySpec": {
212
+ "description": "Explicit order key: direction defaults to 'asc', empty-sequence placement to 'least'.",
213
+ "type": "object",
214
+ "properties": {
215
+ "$key": { "$ref": "#/$defs/expression" },
216
+ "$dir": { "enum": ["asc", "desc"] },
217
+ "$empty": { "enum": ["least", "greatest"] }
218
+ },
219
+ "required": ["$key"],
220
+ "additionalProperties": false
221
+ },
222
+ "somePhrase": {
223
+ "description": "Existential quantifier: true iff some binding tuple satisfies the EBV of $satisfies; short-circuits.",
224
+ "type": "object",
225
+ "properties": {
226
+ "$some": { "$ref": "#/$defs/bindingMap" },
227
+ "$satisfies": { "$ref": "#/$defs/expression" }
228
+ },
229
+ "required": ["$some", "$satisfies"],
230
+ "additionalProperties": false
231
+ },
232
+ "everyPhrase": {
233
+ "description": "Universal quantifier: true iff every binding tuple satisfies the EBV of $satisfies; short-circuits.",
234
+ "type": "object",
235
+ "properties": {
236
+ "$every": { "$ref": "#/$defs/bindingMap" },
237
+ "$satisfies": { "$ref": "#/$defs/expression" }
238
+ },
239
+ "required": ["$every", "$satisfies"],
240
+ "additionalProperties": false
241
+ },
242
+ "unaryOperatorPhrase": {
243
+ "description": "Operators whose value is a single operand expression.",
244
+ "type": "object",
245
+ "minProperties": 1,
246
+ "maxProperties": 1,
247
+ "propertyNames": {
248
+ "enum": [
249
+ "$exists", "$empty", "$not", "$neg",
250
+ "$count", "$sum", "$avg", "$min", "$max",
251
+ "$distinct", "$reverse", "$sort", "$head", "$tail",
252
+ "$upper", "$lower", "$string-length", "$normalize-space",
253
+ "$string", "$number", "$boolean",
254
+ "$is-string", "$is-number", "$is-boolean", "$is-null", "$is-array", "$is-object"
255
+ ]
256
+ },
257
+ "additionalProperties": { "$ref": "#/$defs/expression" }
258
+ },
259
+ "binaryOperatorPhrase": {
260
+ "description": "Operators whose value is an array of exactly two operand expressions.",
261
+ "type": "object",
262
+ "minProperties": 1,
263
+ "maxProperties": 1,
264
+ "propertyNames": {
265
+ "enum": [
266
+ "$eq", "$ne", "$lt", "$le", "$gt", "$ge",
267
+ "$add", "$sub", "$mul", "$div", "$idiv", "$mod",
268
+ "$contains", "$starts-with", "$ends-with", "$match", "$search",
269
+ "$index-of", "$range", "$get", "$default"
270
+ ]
271
+ },
272
+ "additionalProperties": {
273
+ "type": "array",
274
+ "items": { "$ref": "#/$defs/expression" },
275
+ "minItems": 2,
276
+ "maxItems": 2
277
+ }
278
+ },
279
+ "variadicOperatorPhrase": {
280
+ "description": "Variadic operators accepting zero or more operand expressions.",
281
+ "type": "object",
282
+ "minProperties": 1,
283
+ "maxProperties": 1,
284
+ "propertyNames": {
285
+ "enum": ["$seq", "$concat"]
286
+ },
287
+ "additionalProperties": {
288
+ "type": "array",
289
+ "items": { "$ref": "#/$defs/expression" }
290
+ }
291
+ },
292
+ "nonEmptyVariadicOperatorPhrase": {
293
+ "description": "Variadic operators requiring at least one operand expression.",
294
+ "type": "object",
295
+ "minProperties": 1,
296
+ "maxProperties": 1,
297
+ "propertyNames": {
298
+ "enum": ["$and", "$or", "$coalesce"]
299
+ },
300
+ "additionalProperties": {
301
+ "type": "array",
302
+ "items": { "$ref": "#/$defs/expression" },
303
+ "minItems": 1
304
+ }
305
+ },
306
+ "conditionalArityOperatorPhrase": {
307
+ "description": "Operators taking two or three operand expressions: $if [cond, then, else?], $substring [str, start, len?], $subsequence [seq, start, len?].",
308
+ "type": "object",
309
+ "minProperties": 1,
310
+ "maxProperties": 1,
311
+ "propertyNames": {
312
+ "enum": ["$if", "$substring", "$subsequence"]
313
+ },
314
+ "additionalProperties": {
315
+ "type": "array",
316
+ "items": { "$ref": "#/$defs/expression" },
317
+ "minItems": 2,
318
+ "maxItems": 3
319
+ }
320
+ },
321
+ "stringJoinPhrase": {
322
+ "description": "$string-join [seq, separator?]: one or two operand expressions.",
323
+ "type": "object",
324
+ "properties": {
325
+ "$string-join": {
326
+ "type": "array",
327
+ "items": { "$ref": "#/$defs/expression" },
328
+ "minItems": 1,
329
+ "maxItems": 2
330
+ }
331
+ },
332
+ "required": ["$string-join"],
333
+ "additionalProperties": false
334
+ },
335
+ "replacePhrase": {
336
+ "description": "$replace [input, pattern, replacement]: exactly three operand expressions (I-Regexp has no flags argument, D5).",
337
+ "type": "object",
338
+ "properties": {
339
+ "$replace": {
340
+ "type": "array",
341
+ "items": { "$ref": "#/$defs/expression" },
342
+ "minItems": 3,
343
+ "maxItems": 3
344
+ }
345
+ },
346
+ "required": ["$replace"],
347
+ "additionalProperties": false
348
+ },
349
+ "schemaLiteral": {
350
+ "description": "A JSON Schema literal, taken verbatim - never interpreted as a query expression, so JSON Schema's $-prefixed keywords ($ref, $defs, ...) do not collide with Rule 1. Deliberately unconstrained (a true schema is draft-neutral by definition): embedded schemas are not meta-validated by this artifact; the consumer's type-test compiler is authoritative (JQ0009)."
351
+ },
352
+ "schemaOperatorPhrase": {
353
+ "description": "Schema operators $valid/$assert: [expr, schema] - exactly two elements, an operand expression and a verbatim JSON Schema literal. Tuple validation is outside the draft-neutral subset, so both positions validate uniformly as schema literals here; the operand position is normalized as an ordinary expression by the compiler (JQ0xxx).",
354
+ "type": "object",
355
+ "minProperties": 1,
356
+ "maxProperties": 1,
357
+ "propertyNames": {
358
+ "enum": ["$valid", "$assert"]
359
+ },
360
+ "additionalProperties": {
361
+ "type": "array",
362
+ "items": { "$ref": "#/$defs/schemaLiteral" },
363
+ "minItems": 2,
364
+ "maxItems": 2
365
+ }
366
+ },
367
+ "asClause": {
368
+ "description": "The $as clause: variable-name to JSON Schema literal members, applied per tuple after $for/$let binding and before $where. Each name must be bound by the same phrase's $for (including $at names) or $let - a compiler rule (JQ0005); each binding validates per item at runtime (JQ2008).",
369
+ "type": "object",
370
+ "minProperties": 1,
371
+ "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
372
+ "additionalProperties": { "$ref": "#/$defs/schemaLiteral" }
373
+ }
374
+ }
375
+ }
package/src/basic.js ADDED
@@ -0,0 +1,300 @@
1
+ import {
2
+ CC_SLASH,
3
+ CC_HASH,
4
+ CC_PERCENT,
5
+ CC_TILDE,
6
+ CC_0,
7
+ CC_1,
8
+ isDigitCode,
9
+ isHexDigitCode,
10
+ } from '@jarenjs/core/scan';
11
+
12
+ //#region JSON validation
13
+ // JSON whitespace characters: space, tab, newline, carriage return
14
+ const JSON_WHITESPACE = new Set([0x20, 0x09, 0x0A, 0x0D]);
15
+
16
+ export function isValidJSONCheap(data) {
17
+ const len = data.length;
18
+ // Too short to be valid JSON (minimum is 2 for {} or [])
19
+ if (len < 2) return true; // Definitely NOT valid JSON
20
+
21
+ // Check first char
22
+ const first = data.charCodeAt(0);
23
+
24
+ // Objects and arrays - check matching brackets
25
+ if (first === 0x7B || first === 0x5B) { // '{' or '['
26
+ // Find last non-whitespace character (JSON allows trailing whitespace)
27
+ let lastIdx = len - 1;
28
+ while (lastIdx >= 0) {
29
+ const code = data.charCodeAt(lastIdx);
30
+ if (!JSON_WHITESPACE.has(code)) break;
31
+ lastIdx--;
32
+ }
33
+
34
+ // Must end with matching bracket
35
+ const last = data.charCodeAt(lastIdx);
36
+ if (first === 0x7B && last !== 0x7D) return true; // {} must match - definitely NOT valid
37
+ if (first === 0x5B && last !== 0x5D) return true; // [] must match - definitely NOT valid
38
+
39
+ // Looks like JSON object/array, might be valid
40
+ return false;
41
+ }
42
+
43
+ // Check if first char is a valid JSON starting character:
44
+ // '"' (0x22) for strings, '-' (0x2D) or digit (0x30-0x39) for numbers,
45
+ // 't' (0x74) for true, 'f' (0x66) for false, 'n' (0x6E) for null
46
+ const isValidStart = (
47
+ first === 0x22 || // '"'
48
+ first === 0x2D || // '-'
49
+ (first >= 0x30 && first <= 0x39) || // '0'-'9'
50
+ first === 0x74 || // 't' (true)
51
+ first === 0x66 || // 'f' (false)
52
+ first === 0x6E // 'n' (null)
53
+ );
54
+
55
+ // If it starts with a valid JSON character, it might be valid JSON
56
+ // Return false to indicate "might be JSON, need to parse"
57
+ if (isValidStart) return false;
58
+
59
+ // Doesn't start with valid JSON character - definitely NOT valid JSON
60
+ return true;
61
+ }
62
+
63
+ export function isValidJSON(data) {
64
+ if (isValidJSONCheap(data)) return false;
65
+
66
+ try {
67
+ JSON.parse(data);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+ //#endregion
74
+
75
+ //#region JPtr Tests
76
+ // Single-pass char-code scanners (no regex, no allocation), shared by the
77
+ // `json-pointer`/`relative-json-pointer` string formats.
78
+
79
+ // scan the pointer body `str[pos..)`: '/'-delimited segments where every
80
+ // '~' must be followed by '0' or '1' (RFC 6901 section 3)
81
+ function isValidPointerBody(str, pos) {
82
+ const len = str.length;
83
+ for (; pos < len; pos++) {
84
+ if (str.charCodeAt(pos) === CC_TILDE) {
85
+ const d = pos + 1 < len ? str.charCodeAt(pos + 1) : -1;
86
+ if (d !== CC_0 && d !== CC_1)
87
+ return false;
88
+ pos++;
89
+ }
90
+ }
91
+ return true;
92
+ }
93
+
94
+ // JSON-pointer: https://tools.ietf.org/html/rfc6901
95
+ export function isValidJSONPointer(str) {
96
+ if (typeof str !== 'string')
97
+ return false;
98
+ if (str.length === 0)
99
+ return true;
100
+ if (str.charCodeAt(0) !== CC_SLASH)
101
+ return false;
102
+ return isValidPointerBody(str, 1);
103
+ }
104
+
105
+ // unreserved / sub-delims / ':' / '@' per RFC 3986 pchar, minus the
106
+ // pointer-significant '~' and '/' handled by the caller
107
+ function isFragmentPointerCode(c) {
108
+ return (c >= 0x61 && c <= 0x7A) // a-z
109
+ || (c >= 0x41 && c <= 0x5A) // A-Z
110
+ || isDigitCode(c)
111
+ || (c >= 0x26 && c <= 0x2E) // & ' ( ) * + , - .
112
+ || c === 0x21 // !
113
+ || c === 0x24 // $
114
+ || c === 0x3A // :
115
+ || c === 0x3B // ;
116
+ || c === 0x3D // =
117
+ || c === 0x40 // @
118
+ || c === 0x5F; // _
119
+ }
120
+
121
+ // uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A
122
+ export function isValidJSONPointerUriFragment(str) {
123
+ if (typeof str !== 'string')
124
+ return false;
125
+ const len = str.length;
126
+ if (len === 0 || str.charCodeAt(0) !== CC_HASH)
127
+ return false;
128
+ if (len > 1 && str.charCodeAt(1) !== CC_SLASH)
129
+ return false;
130
+ for (let pos = 1; pos < len; pos++) {
131
+ const c = str.charCodeAt(pos);
132
+ if (c === CC_SLASH || isFragmentPointerCode(c))
133
+ continue;
134
+ if (c === CC_PERCENT) { // percent-encoded octet
135
+ if (pos + 2 >= len
136
+ || !isHexDigitCode(str.charCodeAt(pos + 1))
137
+ || !isHexDigitCode(str.charCodeAt(pos + 2)))
138
+ return false;
139
+ pos += 2;
140
+ continue;
141
+ }
142
+ if (c === CC_TILDE) {
143
+ const d = pos + 1 < len ? str.charCodeAt(pos + 1) : -1;
144
+ if (d !== CC_0 && d !== CC_1)
145
+ return false;
146
+ pos++;
147
+ continue;
148
+ }
149
+ return false;
150
+ }
151
+ return true;
152
+ }
153
+
154
+ // relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00
155
+ export function isValidRelativeJSONPointer(str) {
156
+ if (typeof str !== 'string')
157
+ return false;
158
+ const len = str.length;
159
+ if (len === 0)
160
+ return false;
161
+ const first = str.charCodeAt(0);
162
+ if (!isDigitCode(first))
163
+ return false;
164
+ let pos = 1;
165
+ if (first !== CC_0) {
166
+ while (pos < len && isDigitCode(str.charCodeAt(pos)))
167
+ pos++;
168
+ }
169
+ if (pos === len)
170
+ return true;
171
+ const c = str.charCodeAt(pos);
172
+ if (c === CC_HASH)
173
+ return pos + 1 === len;
174
+ if (c !== CC_SLASH)
175
+ return false;
176
+ return isValidPointerBody(str, pos + 1);
177
+ }
178
+ //#endregion
179
+
180
+ //#region JSONPath Tests (RFC 9535)
181
+ // JSONPath syntax: https://www.rfc-editor.org/rfc/rfc9535.html
182
+
183
+ // Basic JSONPath structure:
184
+ // $ - root
185
+ // $.store.book[0].title - dot notation with array index
186
+ // $['store']['book'][0] - bracket notation
187
+ // $..name - recursive descent
188
+ // $.* - wildcard
189
+ // $[?(@.price < 10)] - filter expression
190
+ // $[0:5:2] - slice
191
+ // $[0,1,2] - multiple indices
192
+
193
+ // Simplified regex that covers common JSONPath patterns
194
+ // This validates the structure without fully parsing filter expressions
195
+ // Pattern breakdown:
196
+ // (\$|@) - starts with $ or @
197
+ // (?:\.\.[a-zA-Z_][a-zA-Z0-9_]*| - recursive descent with name (e.g., $..name)
198
+ // \.\.| - recursive descent (e.g., $..)
199
+ // \.[a-zA-Z_][a-zA-Z0-9_]*| - dot notation (e.g., $.store)
200
+ // \.[*]| - dot wildcard (e.g., $.*)
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*\])*$/;
203
+
204
+ /**
205
+ * Validates a JSONPath expression string per RFC 9535.
206
+ *
207
+ * JSONPath syntax includes:
208
+ * - $ - root node selector
209
+ * - @ - current node selector (used in filter expressions)
210
+ * - .name - dot notation for child member
211
+ * - ['name'] or ["name"] - bracket notation for child member
212
+ * - [index] - array index
213
+ * - [*] - wildcard selector
214
+ * - [start:end:step] - array slice
215
+ * - [?expression] - filter expression
216
+ * - .. - recursive descent
217
+ *
218
+ * @param {string} str - The JSONPath expression to validate
219
+ * @returns {boolean} - True if the string is a valid JSONPath expression
220
+ * @example
221
+ * isValidJSONPath('$.store.book[0].title'); // true
222
+ * isValidJSONPath('$..name'); // true
223
+ * isValidJSONPath('$[*]'); // true
224
+ * isValidJSONPath('$[?(@.price < 10)]'); // true
225
+ * isValidJSONPath('$.store.book[0:5]'); // true
226
+ * isValidJSONPath('@.name'); // true (current node selector)
227
+ * isValidJSONPath('store'); // false (must start with $ or @)
228
+ * isValidJSONPath(''); // false (empty string)
229
+ */
230
+ export function isValidJSONPath(str) {
231
+ if (typeof str !== 'string')
232
+ return false;
233
+
234
+ // Must start with $ (root) or @ (current node in filters)
235
+ if (!str || (str[0] !== '$' && str[0] !== '@'))
236
+ return false;
237
+
238
+ // Single $ or @ is valid
239
+ if (str.length === 1)
240
+ return true;
241
+
242
+ // Use the regex for basic validation
243
+ if (!CONST_REGEXP_JSONPATH.test(str))
244
+ return false;
245
+
246
+ // Additional validation for bracket contents
247
+ return validateJSONPathBrackets(str);
248
+ }
249
+
250
+ /**
251
+ * Validates that brackets in JSONPath are balanced and well-formed
252
+ * @param {string} str - The JSONPath string to validate
253
+ * @returns {boolean} - True if brackets are valid
254
+ * @private
255
+ */
256
+ function validateJSONPathBrackets(str) {
257
+ let depth = 0;
258
+ let inString = false;
259
+ let stringChar = null;
260
+ let escapeNext = false;
261
+
262
+ for (let i = 0; i < str.length; i++) {
263
+ const char = str[i];
264
+
265
+ if (escapeNext) {
266
+ escapeNext = false;
267
+ continue;
268
+ }
269
+
270
+ if (char === '\\') {
271
+ escapeNext = true;
272
+ continue;
273
+ }
274
+
275
+ if (inString) {
276
+ if (char === stringChar) {
277
+ inString = false;
278
+ stringChar = null;
279
+ }
280
+ continue;
281
+ }
282
+
283
+ if (char === "'" || char === '"') {
284
+ inString = char;
285
+ stringChar = char;
286
+ continue;
287
+ }
288
+
289
+ if (char === '[') {
290
+ depth++;
291
+ } else if (char === ']') {
292
+ depth--;
293
+ if (depth < 0)
294
+ return false; // Unbalanced brackets
295
+ }
296
+ }
297
+
298
+ return depth === 0 && !inString;
299
+ }
300
+ //#endregion
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './basic.js';
2
+ export * from './pointer.js';
3
+ export * from './path.js';
4
+ export * from './query/index.js';