diff --git a/src/jsonpath.js b/src/jsonpath.js index 4d036ee..2eed181 100644 --- a/src/jsonpath.js +++ b/src/jsonpath.js @@ -1251,6 +1251,22 @@ JSONPath.toPathArray = function (expr) { return '[#' + (subx.push($1) - 1) + ']'; } ) + // Quoted property names containing characters which would otherwise + // be read as path syntax (e.g., `['a[b]']`), kept intact with a + // leading backtick escape so they are looked up as they are + .replaceAll( + /\[\s*(?:'([^']*)'|"([^"]*)")\s*\]/gv, + function ($0, single, double) { + const name = single ?? double; + if (!(/(?:^[@"])|[\[\];^']|#\d/v).test(name)) { + return $0; + } + const escaped = name[0] === '`' ? name : '`' + name; + // eslint-disable-next-line @stylistic/max-len -- Long + // eslint-disable-next-line unicorn/no-return-array-push -- Optimization + return '[#' + (subx.push(escaped) - 1) + ']'; + } + ) // Escape periods and tildes within properties .replaceAll(/\[['"]([^'\]]*)['"]\]/gv, function ($0, prop) { return "['" + prop diff --git a/test/test.properties.js b/test/test.properties.js index 66febf1..03fb2b9 100644 --- a/test/test.properties.js +++ b/test/test.properties.js @@ -91,6 +91,55 @@ checkBuiltInVMAndNodeVM(function (vmType, setBuiltInState) { } }); + it('Quoted property names with path syntax characters', () => { + const names = [ + 'cd[DataLayer]', '[', ']', 'phoneNumbers [1]', 'x;y', 'a^b', + "it's", "a'", "'lastName'", '"lastName"', 'say "hi"', '@id' + ]; + for (const name of names) { + const data = {[name]: {y: 'Y'}, x: {[name]: 'N'}, z: 1}; + /** + * @param {string|string[]} path + * @param {"value"|"path"} [resultType] + * @param {boolean} [wrap] + * @returns {unknown} + */ + const query = (path, resultType, wrap = true) => jsonpath({ + json: data, path, resultType, wrap + }); + const quotedNames = /** @type {string[]} */ ([]); + if (!name.includes("'")) { + quotedNames.push(`'${name}'`); + } + if (!name.includes('"')) { + quotedNames.push(`"${name}"`); + } + for (const quoted of quotedNames) { + const msg = quoted; + assert.deepEqual(query(`$[${quoted}]`), [{y: 'Y'}], msg); + assert.deepEqual(query(`$[${quoted}].y`), ['Y'], msg); + assert.deepEqual(query(`$.x[${quoted}]`), ['N'], msg); + assert.deepEqual( + query(`$..[${quoted}]`), [{y: 'Y'}, 'N'], msg + ); + // A single name is not an array expression + assert.strictEqual( + query(`$[${quoted}].y`, 'value', false), 'Y', msg + ); + } + // Result paths and array paths find the name again + const [path] = /** @type {string[]} */ ( + query('$.*.y', 'path') + ); + assert.deepEqual(query(path), ['Y'], path); + assert.deepEqual(query(['$', name, 'y']), ['Y'], name); + } + // A leading backtick still escapes the rest of the name + assert.deepEqual(jsonpath({ + json: {'a[b]': 1, '`a[b]': 2}, path: "$['`a[b]']" + }), [1]); + }); + it('At signs within properties', () => { let result = jsonpath({json, path: "$.datafield[?(@.tag=='035')]", wrap: false}); assert.deepEqual(result, [json.datafield[0]]);