Skip to content

Commit e04fa4e

Browse files
committed
fix: support unions that mix quoted names with other selectors
A union with a quoted name next to any other kind of selector found nothing for the quoted name: $['a',1] returned only the index, $[*,'a'] dropped the name, and $..['io.nest',out] (the last example in #159) returned only `out`. toPathArray only kept unions of quoted names intact, so a mixed one was split at the commas with the quotes still stuck to the pieces. toPathArray now keeps any well formed union that has a quoted member intact, and _trace traces the members in order: quoted ones as literal names, the rest (indexes, slices, wildcards, bare names) as usual. Unions without quotes are unchanged, so $[x, y] still treats the space as part of the name.
1 parent 970bdb2 commit e04fa4e

2 files changed

Lines changed: 73 additions & 9 deletions

File tree

‎src/jsonpath.js‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {toStringTag} from './utils.js';
77

88
const scriptCache = new Map();
99
const pathCache = new Map();
10+
// A union of quoted names and other selectors (e.g., `0,'a'`); only needed
11+
// for unions which don't start with a quote
12+
const mixedUnion = /^(?=[^'"]*['"])(?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|[^,'"\s\[\]]+)(?:\s*,\s*(?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|[^,'"\s\[\]]+))*$/v;
1013

1114
/**
1215
* @typedef {any} AnyInput
@@ -868,11 +871,27 @@ class JSONPathClass {
868871
x, valObj[locProp], push(path, locProp), val, locProp, callback,
869872
hasArrExpr, true
870873
));
871-
} else if (loc[0] === "'" || loc[0] === '"') { // ['name1','name2',...]
872-
// Quoted members are literal property names
873-
for (const [, single, double] of loc.matchAll(
874-
/'((?:\\.|[^'\\])*)'|"((?:\\.|[^"\\])*)"/gv
874+
} else if (
875+
loc[0] === "'" || loc[0] === '"' ||
876+
(loc.includes(',') && mixedUnion.test(loc))
877+
) { // ['name1','name2',...], possibly with other selectors
878+
for (const [, single, double, other] of loc.matchAll(
879+
/'((?:\\.|[^'\\])*)'|"((?:\\.|[^"\\])*)"|([^,'"\s\[\]]+)/gv
875880
)) {
881+
if (other !== undefined) {
882+
// Not a quoted name, so trace it as usual (e.g., an index)
883+
addRet(this._trace(
884+
unshift(other, x),
885+
val,
886+
path,
887+
parent,
888+
parentPropName,
889+
callback,
890+
true
891+
));
892+
continue;
893+
}
894+
// Quoted members are literal property names
876895
const prop = (single ?? double).replaceAll(
877896
/\\(['"\\])/gv, '$1'
878897
);
@@ -1237,13 +1256,14 @@ JSONPath.toPathArray = function (expr) {
12371256
(subx.push($1) - 1) +
12381257
']';
12391258
})
1240-
// Unions of quoted property names (e.g., `['x','y']`) and quoted
1241-
// names with commas or backslash escapes, kept intact (with
1242-
// quotes) so their members survive tokenization
1259+
// Unions of quoted property names (e.g., `['x','y']`), optionally
1260+
// mixed with other selectors (e.g., `['x',0]`), and quoted names
1261+
// with commas or backslash escapes, kept intact (with quotes) so
1262+
// their members survive tokenization
12431263
.replaceAll(
1244-
/\[\s*((?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")(?:\s*,\s*(?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"))*)\s*\]/gv,
1264+
/\[\s*((?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|[^,'"\s\[\]]+)(?:\s*,\s*(?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|[^,'"\s\[\]]+))*)\s*\]/gv,
12451265
function ($0, $1) {
1246-
if (!(/[,\\]/v).test($1)) {
1266+
if (!(/['"]/v).test($1) || !(/[,\\]/v).test($1)) {
12471267
return $0;
12481268
}
12491269
// eslint-disable-next-line @stylistic/max-len -- Long

‎test/test.properties.js‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,50 @@ checkBuiltInVMAndNodeVM(function (vmType, setBuiltInState) {
6060
assert.deepEqual(query('$[x, y]'), [1, 3]);
6161
});
6262

63+
it('Union of quoted property names and other selectors', () => {
64+
/**
65+
* @param {string} path
66+
* @param {unknown} data
67+
* @param {"value"|"path"} [resultType]
68+
* @returns {unknown}
69+
*/
70+
const query = (path, data, resultType) => jsonpath({
71+
json: /** @type {object} */ (data), path, resultType
72+
});
73+
const obj = {a: 1, b: 2};
74+
const list = ['x', 'y', 'z'];
75+
// A quoted name next to an index, in either order
76+
assert.deepEqual(query("$['a',1]", obj), [1]);
77+
assert.deepEqual(query("$[1,'a']", obj), [1]);
78+
assert.deepEqual(query("$['a','b',0]", obj), [1, 2]);
79+
assert.deepEqual(query('$["a",0]', obj), [1]);
80+
assert.deepEqual(query("$['a', 1]", obj), [1]);
81+
assert.deepEqual(query("$['a',1]", obj, 'path'), ["$['a']"]);
82+
assert.deepEqual(query("$[0,'a']", list), ['x']);
83+
assert.deepEqual(query("$['a',2,0]", list), ['z', 'x']);
84+
// A wildcard or a slice next to a name keeps every match
85+
assert.deepEqual(query("$[*,'a']", {a: 'A', b: 'B'}), [
86+
'A', 'B', 'A'
87+
]);
88+
assert.deepEqual(query("$[0:2,'a']", list), ['x', 'y']);
89+
// Commas inside the quotes do not split the name
90+
assert.deepEqual(query("$['a,b',0]", {'a,b': 5, 0: 6}), [5, 6]);
91+
// A quoted name next to a bare name, as in the last example of #159
92+
assert.deepEqual(query("$..['io.nest',out]", {
93+
functions: {foo: {'io.nest': 'foo'}, bar: {out: 'bar'}}
94+
}), ['foo', 'bar']);
95+
// Escapes work as in a union of only quoted names
96+
assert.deepEqual(
97+
query(String.raw`$['it\'s',0]`, {"it's": 7, 0: 8}), [7, 8]
98+
);
99+
// A quote within a bare name does not make it a mixed union
100+
assert.deepEqual(query("$[it's,x]", {"it's": 1, x: 2}), [1, 2]);
101+
// The rest of the path continues from every member
102+
assert.deepEqual(query("$['n',0].v", {n: {v: 1}, 0: {v: 2}}), [
103+
1, 2
104+
]);
105+
});
106+
63107
it('Quoted property names with commas and escapes', () => {
64108
const data = {
65109
'a,b': 1, "it's": 2, 'say "hi"': 3, 'a\\b': 4, a: 5, b: 6

0 commit comments

Comments
 (0)