Skip to content

Commit d5fd1de

Browse files
committed
Use JSONPath-like syntax for new jsonl- scriptlets
1 parent 5936451 commit d5fd1de

File tree

7 files changed

+435
-144
lines changed

7 files changed

+435
-144
lines changed

src/js/jsonpath.js

+332
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
/*******************************************************************************
2+
3+
uBlock Origin - a comprehensive, efficient content blocker
4+
Copyright (C) 2025-present Raymond Hill
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program. If not, see {http://www.gnu.org/licenses/}.
18+
19+
Home: https://github.com/gorhill/uBlock
20+
21+
*/
22+
23+
/******************************************************************************/
24+
25+
export class JSONPath {
26+
static create(query) {
27+
const jsonp = new JSONPath();
28+
jsonp.compile(query);
29+
return jsonp;
30+
}
31+
compile(query) {
32+
this.#compiled = this.#compile(query, 0);
33+
return this.#compiled ? this.#compiled.i : 0;
34+
}
35+
evaluate(root) {
36+
if ( this.#compiled === undefined ) { return []; }
37+
this.root = root;
38+
return this.#evaluate(this.#compiled.steps, []);
39+
}
40+
resolvePath(path) {
41+
if ( path.length === 0 ) { return { value: this.root }; }
42+
const key = path.at(-1);
43+
let obj = this.root
44+
for ( let i = 0, n = path.length-1; i < n; i++ ) {
45+
obj = obj[path[i]];
46+
}
47+
return { obj, key, value: obj[key] };
48+
}
49+
toString() {
50+
return JSON.stringify(this.#compiled);
51+
}
52+
#UNDEFINED = 0;
53+
#ROOT = 1;
54+
#CURRENT = 2;
55+
#CHILDREN = 3;
56+
#DESCENDANTS = 4;
57+
#reUnquotedIdentifier = /^[A-Za-z_][\w]*|^\*/;
58+
#reExpr = /^([!=^$*]=|[<>]=?)(.+?)\)\]/;
59+
#reIndice = /^\[-?\d+\]/;
60+
#compiled;
61+
#compile(query, i) {
62+
if ( query.length === 0 ) { return; }
63+
const steps = [];
64+
let c = query.charCodeAt(i);
65+
steps.push({ mv: c === 0x24 /* $ */ ? this.#ROOT : this.#CURRENT });
66+
if ( c === 0x24 /* $ */ || c === 0x40 /* @ */ ) { i += 1; }
67+
let mv = this.#UNDEFINED;
68+
for (;;) {
69+
if ( i === query.length ) { break; }
70+
c = query.charCodeAt(i);
71+
if ( c === 0x20 /* whitespace */ ) {
72+
i += 1;
73+
continue;
74+
}
75+
// Dot accessor syntax
76+
if ( c === 0x2E /* . */ ) {
77+
if ( mv !== this.#UNDEFINED ) { return; }
78+
if ( query.startsWith('..', i) ) {
79+
mv = this.#DESCENDANTS;
80+
i += 2;
81+
} else {
82+
mv = this.#CHILDREN;
83+
i += 1;
84+
}
85+
continue;
86+
}
87+
if ( c !== 0x5B /* [ */ ) {
88+
if ( mv === this.#UNDEFINED ) {
89+
const step = steps.at(-1);
90+
if ( step === undefined ) { return; }
91+
i = this.#compileExpr(step, query, i);
92+
break;
93+
}
94+
const s = this.#consumeUnquotedIdentifier(query, i);
95+
if ( s === undefined ) { return; }
96+
steps.push({ mv, k: s });
97+
i += s.length;
98+
mv = this.#UNDEFINED;
99+
continue;
100+
}
101+
// Bracket accessor syntax
102+
if ( query.startsWith('[*]', i) ) {
103+
mv ||= this.#CHILDREN;
104+
steps.push({ mv, k: '*' });
105+
i += 3;
106+
mv = this.#UNDEFINED;
107+
continue;
108+
}
109+
if ( query.startsWith("['", i) ) {
110+
const r = this.#consumeQuotedIdentifier(query, i+2);
111+
if ( r === undefined ) { return; }
112+
mv ||= this.#CHILDREN;
113+
steps.push({ mv, k: r.s });
114+
i = r.i;
115+
mv = this.#UNDEFINED;
116+
continue;
117+
}
118+
if ( query.startsWith('[?(', i) ) {
119+
const not = query.charCodeAt(i+3) === 0x21 /* ! */;
120+
const j = i + 3 + (not ? 1 : 0);
121+
const r = this.#compile(query, j);
122+
if ( r === undefined ) { return; }
123+
if ( query.startsWith(')]', r.i) === false ) { return; }
124+
if ( not ) { r.steps.at(-1).not = true; }
125+
steps.push({ mv: mv || this.#CHILDREN, steps: r.steps });
126+
i = r.i + 2;
127+
mv = this.#UNDEFINED;
128+
continue;
129+
}
130+
if ( this.#reIndice.test(query.slice(i)) ) {
131+
const match = this.#reIndice.exec(query.slice(i));
132+
const indice = parseInt(query.slice(i+1), 10);
133+
mv ||= this.CHILDREN;
134+
steps.push({ mv, k: indice });
135+
i += match[0].length;
136+
mv = this.#UNDEFINED;
137+
continue;
138+
}
139+
return;
140+
}
141+
if ( steps.length <= 1 ) { return; }
142+
return { steps, i };
143+
}
144+
#evaluate(steps, pathin) {
145+
let resultset = [];
146+
if ( Array.isArray(steps) === false ) { return resultset; }
147+
for ( const step of steps ) {
148+
switch ( step.mv ) {
149+
case this.#ROOT:
150+
resultset = [ [] ];
151+
break;
152+
case this.#CURRENT:
153+
resultset = [ pathin ];
154+
break;
155+
case this.#CHILDREN:
156+
case this.#DESCENDANTS:
157+
resultset = this.#getMatches(resultset, step);
158+
break;
159+
default:
160+
break;
161+
}
162+
}
163+
return resultset;
164+
}
165+
#getMatches(listin, step) {
166+
const listout = [];
167+
const recursive = step.mv === this.#DESCENDANTS;
168+
for ( const pathin of listin ) {
169+
const { value: v } = this.resolvePath(pathin);
170+
if ( v === null ) { continue; }
171+
if ( v === undefined ) { continue; }
172+
const { steps, k } = step;
173+
if ( k ) {
174+
if ( k === '*' ) {
175+
const entries = Array.from(this.#getDescendants(v, recursive));
176+
for ( const { path } of entries ) {
177+
this.#evaluateExpr(step, [ ...pathin, ...path ], listout);
178+
}
179+
continue;
180+
}
181+
if ( typeof k === 'number' ) {
182+
if ( Array.isArray(v) === false ) { continue; }
183+
const n = v.length;
184+
const i = k >= 0 ? k : n + k;
185+
if ( i < 0 ) { continue; }
186+
if ( i >= n ) { continue; }
187+
this.#evaluateExpr(step, [ ...pathin, i ], listout);
188+
} else if ( Array.isArray(k) ) {
189+
for ( const l of k ) {
190+
this.#evaluateExpr(step, [ ...pathin, l ], listout);
191+
}
192+
} else {
193+
this.#evaluateExpr(step, [ ...pathin, k ], listout);
194+
}
195+
if ( recursive !== true ) { continue; }
196+
for ( const { obj, key, path } of this.#getDescendants(v, recursive) ) {
197+
const w = obj[key];
198+
if ( w instanceof Object === false ) { continue; }
199+
if ( Object.hasOwn(w, k) === false ) { continue; }
200+
this.#evaluateExpr(step, [ ...pathin, ...path, k ], listout);
201+
}
202+
continue;
203+
}
204+
if ( steps ) {
205+
const isArray = Array.isArray(v);
206+
if ( isArray === false ) {
207+
const r = this.#evaluate(steps, pathin);
208+
if ( r.length !== 0 ) {
209+
listout.push(pathin);
210+
}
211+
if ( recursive !== true ) { continue; }
212+
}
213+
for ( const { obj, key, path } of this.#getDescendants(v, recursive) ) {
214+
const w = obj[key];
215+
if ( Array.isArray(w) ) { continue; }
216+
const x = [ ...pathin, ...path ];
217+
const r = this.#evaluate(steps, x);
218+
if ( r.length !== 0 ) {
219+
listout.push(x);
220+
}
221+
}
222+
}
223+
}
224+
return listout;
225+
}
226+
#getDescendants(v, recursive) {
227+
const iterator = {
228+
next() {
229+
const n = this.stack.length;
230+
if ( n === 0 ) {
231+
this.value = undefined;
232+
this.done = true;
233+
return this;
234+
}
235+
const details = this.stack[n-1];
236+
const entry = details.keys.next();
237+
if ( entry.done ) {
238+
this.stack.pop();
239+
this.path.pop();
240+
return this.next();
241+
}
242+
this.path[n-1] = entry.value;
243+
this.value = {
244+
obj: details.obj,
245+
key: entry.value,
246+
path: this.path.slice(),
247+
};
248+
const v = this.value.obj[this.value.key];
249+
if ( recursive ) {
250+
if ( Array.isArray(v) ) {
251+
this.stack.push({ obj: v, keys: v.keys() });
252+
} else if ( typeof v === 'object' && v !== null ) {
253+
this.stack.push({ obj: v, keys: Object.keys(v).values() });
254+
}
255+
}
256+
return this;
257+
},
258+
path: [],
259+
value: undefined,
260+
done: false,
261+
stack: [],
262+
[Symbol.iterator]() { return this; },
263+
};
264+
if ( Array.isArray(v) ) {
265+
iterator.stack.push({ obj: v, keys: v.keys() });
266+
} else if ( typeof v === 'object' && v !== null ) {
267+
iterator.stack.push({ obj: v, keys: Object.keys(v).values() });
268+
}
269+
return iterator;
270+
}
271+
#consumeQuotedIdentifier(query, i) {
272+
const len = query.length;
273+
const parts = [];
274+
let beg = i, end = i;
275+
for (;;) {
276+
if ( end === len ) { return; }
277+
const c = query.charCodeAt(end);
278+
if ( c === 0x27 /* ' */ ) {
279+
if ( query.startsWith("']", end) === false ) { return; }
280+
parts.push(query.slice(beg, end));
281+
end += 2;
282+
break;
283+
}
284+
if ( c === 0x5C /* \ */ && (end+1) < len ) {
285+
parts.push(query.slice(beg, end));
286+
const d = query.chatCodeAt(end+1);
287+
if ( d === 0x27 || d === 0x5C ) {
288+
end += 1;
289+
beg = end;
290+
}
291+
}
292+
end += 1;
293+
}
294+
return { s: parts.join(''), i: end };
295+
}
296+
#consumeUnquotedIdentifier(query, i) {
297+
const match = this.#reUnquotedIdentifier.exec(query.slice(i));
298+
if ( match === null ) { return; }
299+
return match[0];
300+
}
301+
#compileExpr(step, query, i) {
302+
const match = this.#reExpr.exec(query.slice(i));
303+
if ( match === null ) { return i; }
304+
try {
305+
step.rval = JSON.parse(match[2]);
306+
step.op = match[1];
307+
} catch {
308+
}
309+
return i + match[1].length + match[2].length;
310+
}
311+
#evaluateExpr(step, path, out) {
312+
const { obj: o, key: k } = this.resolvePath(path);
313+
const hasOwn = o instanceof Object && Object.hasOwn(o, k);
314+
const v = o[k];
315+
let outcome = true;
316+
if ( step.op !== undefined && hasOwn === false ) { return; }
317+
switch ( step.op ) {
318+
case '==': outcome = v === step.rval; break;
319+
case '!=': outcome = v !== step.rval; break;
320+
case '<': outcome = v < step.rval; break;
321+
case '<=': outcome = v <= step.rval; break;
322+
case '>': outcome = v > step.rval; break;
323+
case '>=': outcome = v >= step.rval; break;
324+
case '^=': outcome = `${v}`.startsWith(step.rval); break;
325+
case '$=': outcome = `${v}`.endsWith(step.rval); break;
326+
case '*=': outcome = `${v}`.includes(step.rval); break;
327+
default: outcome = hasOwn; break;
328+
}
329+
if ( outcome === (step.not === true) ) { return; }
330+
out.push(path);
331+
}
332+
}

0 commit comments

Comments
 (0)