diff --git a/.gitignore b/.gitignore index e14356f..de22e11 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ coverage/ *.log cjs/ .gen/ -src/parser/parser.mjs diff --git a/.nycrc b/.nycrc index 6a6e425..822bb0f 100644 --- a/.nycrc +++ b/.nycrc @@ -5,8 +5,7 @@ "src/**/*.mjs" ], "exclude": [ - "**/__*__/**", - "src/parser/parser.mjs" + "**/__*__/**" ], "reporter": [ "lcovonly", diff --git a/eslint.config.js b/eslint.config.js index ac7f1ee..0392c5a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,7 +8,6 @@ import globals from 'globals'; export default [ { files: ['**/*.mjs', '**/*.js'], - ignores: ['src/parser/parser.mjs'], languageOptions: { ecmaVersion: 2023, parser: babelParser, diff --git a/package-lock.json b/package-lock.json index aa143f0..0ed1ddb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,6 @@ "lodash-es": "^4.17.21", "mocha": "^10.2.0", "mocha-each": "^2.0.1", - "peggy": "^3.0.2", "prettier": "^3.2.5", "rollup": "^4.9.6" }, @@ -2546,15 +2545,6 @@ "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "engines": { - "node": ">=14" - } - }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -4509,22 +4499,6 @@ "node": ">= 14.16" } }, - "node_modules/peggy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/peggy/-/peggy-3.0.2.tgz", - "integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==", - "dev": true, - "dependencies": { - "commander": "^10.0.0", - "source-map-generator": "0.8.0" - }, - "bin": { - "peggy": "bin/peggy.js" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/picocolors": { "version": "1.0.0", "dev": true, @@ -4984,15 +4958,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-generator": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/source-map-generator/-/source-map-generator-0.8.0.tgz", - "integrity": "sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "dev": true, diff --git a/package.json b/package.json index 8e05d4a..a827b0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nimma", - "version": "0.4.2", + "version": "0.5.0", "description": "Scalable JSONPath engine.", "keywords": [ "json", @@ -28,8 +28,8 @@ "require": "./cjs/index.cjs" }, "./parser": { - "import": "./src/parser/parser.mjs", - "require": "./cjs/parser/parser.cjs" + "import": "./src/parser/index.mjs", + "require": "./cjs/parser/index.cjs" }, "./parser/jsep": { "import": "./src/parser/jsep.mjs", @@ -53,7 +53,6 @@ "url": "https://github.com/P0lip/nimma" }, "scripts": { - "prebuild": "peggy --format es -o src/parser/parser.mjs src/parser/parser.peg", "build": "rollup -c", "lint": "ls-lint && eslint --cache --cache-location .cache/ src && prettier --log-level error --ignore-path .gitignore --check --cache --cache-location .cache/.prettier src", "test": "c8 mocha --config .mocharc ./src/**/__tests__/**/*.test.mjs && karma start karma.conf.cjs --log-level=error", @@ -86,7 +85,6 @@ "lodash-es": "^4.17.21", "mocha": "^10.2.0", "mocha-each": "^2.0.1", - "peggy": "^3.0.2", "prettier": "^3.2.5", "rollup": "^4.9.6" }, diff --git a/src/codegen/baseline/__tests__/parse-filter-expression.test.mjs b/src/codegen/baseline/__tests__/parse-filter-expression.test.mjs index 3e201ae..cb1a529 100644 --- a/src/codegen/baseline/__tests__/parse-filter-expression.test.mjs +++ b/src/codegen/baseline/__tests__/parse-filter-expression.test.mjs @@ -128,8 +128,7 @@ describe('parseFilterExpression', () => { it('throws upon unknown shorthand', () => { expect(print.bind(null, `?(@foo())`)).to.throw( - SyntaxError, - `Unsupported shorthand '@foo'`, + `Unsupported shorthand "@foo"`, ); }); }); diff --git a/src/codegen/baseline/generators.mjs b/src/codegen/baseline/generators.mjs index 5100b52..68bd5a3 100644 --- a/src/codegen/baseline/generators.mjs +++ b/src/codegen/baseline/generators.mjs @@ -431,7 +431,7 @@ function processAtIdentifier(tree, name) { ); } - throw new SyntaxError(`Unsupported shorthand '${name}'`); + throw Error(`Unsupported shorthand "${name}"`); } } diff --git a/src/core/__tests__/index.test.mjs b/src/core/__tests__/index.test.mjs index a85ca9a..a3322e4 100644 --- a/src/core/__tests__/index.test.mjs +++ b/src/core/__tests__/index.test.mjs @@ -1,7 +1,6 @@ /* eslint-disable no-undef */ import { expect } from 'chai'; -import { ParserError } from '../../runtime/errors/index.mjs'; import Nimma from '../index.mjs'; describe('Core', () => { @@ -13,10 +12,9 @@ describe('Core', () => { try { fn(); } catch (e) { - expect(e.errors[0]).to.be.instanceof(ParserError); - expect(e.errors[0].cause.name).to.eq('SyntaxError'); + expect(e.errors[0]).to.be.instanceof(SyntaxError); expect(e.errors[0].message).to.eq( - 'Expected "^", "~", or end of input but "." found.', + 'Expected "^", "~", or end of input but "." found at 4', ); } }); diff --git a/src/parser/__tests__/parser.test.mjs b/src/parser/__tests__/parser.test.mjs index d9eb2f1..7d42cb1 100644 --- a/src/parser/__tests__/parser.test.mjs +++ b/src/parser/__tests__/parser.test.mjs @@ -1,7 +1,7 @@ import { expect } from 'chai'; import forEach from 'mocha-each'; -import { parse } from '../parser.mjs'; +import parse from '../index.mjs'; describe('Parser', () => { it('goessner samples', () => { @@ -200,6 +200,37 @@ describe('Parser', () => { }); it('filter expressions', () => { + expect(parse('$[(@.length-1)]')).to.deep.equal([ + { + type: 'SliceExpression', + value: [-1, Infinity, 1], + deep: false, + }, + ]); + expect(parse('$[( @.length - 2 )]')).to.deep.equal([ + { + type: 'SliceExpression', + value: [-2, Infinity, 1], + deep: false, + }, + ]); + expect(parse('$[( @[ "length" ] - 10 )]')).to.deep.equal([ + { + type: 'SliceExpression', + value: [-10, Infinity, 1], + deep: false, + }, + ]); + expect(parse('$[( @["length"] - 5 )]')).to.deep.equal([ + { + type: 'SliceExpression', + value: [-5, Infinity, 1], + deep: false, + }, + ]); + }); + + it('script filter expressions', () => { expect(parse('$[?(@property === "@.schema")]')).to.deep.equal([ { type: 'ScriptFilterExpression', @@ -346,6 +377,36 @@ describe('Parser', () => { }, ); + it('skips whitespaces', () => { + expect(parse('$.[ name ] [?( @.abc )]\t ..@@test( )')).to.deep.equal([ + { + type: 'MemberExpression', + value: 'name', + deep: true, + }, + { + type: 'ScriptFilterExpression', + value: ' @.abc ', + deep: false, + }, + { + type: 'ScriptFilterExpression', + value: '@@test( )', + deep: true, + }, + ]); + }); + + it.skip('handles escapable', () => { + expect(parse(`$["'name\\"'","test\\\\",'"a']`)).to.deep.equal([ + { + type: 'MultipleMemberExpression', + value: ['name"', 'test\\'], + deep: false, + }, + ]); + }); + describe('invalid expressions', () => { it('empty expression or does not start with $', () => { expect(() => parse('')).to.throw('Expected "$" but end of input found.'); @@ -355,56 +416,85 @@ describe('Parser', () => { it('invalid member expression', () => { expect(() => parse('$info')).to.throw( - 'Expected ".", "..", "^", "~", or end of input but "i" found.', + 'Expected ".", "..", "^", "~", or end of input but "i" found at 1.', ); expect(() => parse('$.')).to.throw( - 'Expected "*", "@", "[", [$_\\-], [0-9], or [A-Za-z] but end of input found.', + 'Expected valid name but end of input found at 2.', ); }); it('key expression used in the wrong place', () => { expect(() => parse('$.name~.a')).to.throw( - 'Expected "^", "~", or end of input but "." found.', + 'Expected "^", "~", or end of input but "." found at 7.', ); }); it('unclosed quotes', () => { expect(() => parse('$.name["a]')).to.throw( - `Expected "\\"" or [^"] but end of input found.`, + `Expected """ but end of input found at 10.`, ); expect(() => parse('$.name["\']')).to.throw( - `Expected "\\"" or [^"] but end of input found.`, + `Expected """ but end of input found at 10.`, ); }); it('invalid step in slice expressions', () => { expect(() => parse('$.name[::test]')).to.throw( - 'Expected "-" or [0-9] but "t" found.', + 'Expected "-" or [0-9] but "t" found at 9.', + ); + expect(() => parse('$.name[::-]')).to.throw( + 'Expected [0-9] but "]" found at 10.', ); }); it('invalid shorthands', () => { - expect(() => parse('$..@@()')).to.throw('Expected [a-z] but "(" found.'); + expect(() => parse('$..@@()')).to.throw( + 'Expected [a-z] but "(" found at 5.', + ); expect(() => parse('$..@@test)')).to.throw( - 'Expected "()" or [a-z] but ")" found.', + 'Expected "(" but ")" found at 9.', ); expect(() => parse('$..@@test(')).to.throw( - 'Expected "()" or [a-z] but "(" found.', - ); - expect(() => parse('$..@@test)')).to.throw( - 'Expected "()" or [a-z] but ")" found.', + 'Expected ")" but end of input found at 10.', ); expect(() => parse('$..@')).to.throw( - 'Expected "@" or [a-z] but end of input found.', + 'Expected [a-z] but end of input found at 4.', + ); + }); + + it('invalid filter expressions', () => { + expect(() => parse('$[(')).to.throw( + 'Expected "@" but end of input found at 3.', + ); + expect(() => parse('$[(@')).to.throw( + 'Expected "." or "[" but end of input found at 4.', + ); + expect(() => parse('$[(@.len - 1)]')).to.throw( + 'Expected "length" but "len - " found at 11.', + ); + expect(() => parse('$[(@length - 1)]')).to.throw( + 'Expected "." or "[" but "l" found at 4.', + ); + expect(() => parse('$[(@[length]-2)]')).to.throw( + `Expected """ or "'" at 5.`, + ); + expect(() => parse('$[(@.length + 1))')).to.throw( + 'Expected "-" but "+" found at 12.', + ); + expect(() => parse('$[(@.length - -5))')).to.throw( + 'Expected positive number but "-5" found at 14.', + ); + expect(() => parse('$[(@.length - 0))')).to.throw( + 'Expected positive number but "0" found at 14.', ); }); it('unclosed brackets', () => { expect(() => parse('$.name[0')).to.throw( - 'Expected "\'", ",", ":", "\\"", "]", [$_\\-], [0-9], or [A-Za-z] but end of input found.', + 'Expected "]" but end of input found at 8.', ); expect(() => parse('$.store["[name]"')).to.throw( - 'Expected "\'", ",", "\\"", "]", [$_\\-], [0-9], or [A-Za-z] but end of input found.', + 'Expected "]" but end of input found at 16.', ); }); }); diff --git a/src/parser/index.mjs b/src/parser/index.mjs index 1a8d06d..2140732 100644 --- a/src/parser/index.mjs +++ b/src/parser/index.mjs @@ -1,12 +1 @@ -import { ParserError } from '../runtime/errors/index.mjs'; -import * as parser from './parser.mjs'; - -const { parse } = parser; - -export default function (input) { - try { - return parse(input); - } catch (e) { - throw new ParserError(e.message, input, { cause: e }); - } -} +export { parser as default } from './parser.mjs'; diff --git a/src/parser/parser.mjs b/src/parser/parser.mjs new file mode 100644 index 0000000..7e94919 --- /dev/null +++ b/src/parser/parser.mjs @@ -0,0 +1,509 @@ +/* eslint-disable sort-keys */ +export function parser(expr) { + if (expr.length === 0) { + throw SyntaxError('Expected "$" but end of input found.'); + } + + if (expr.charCodeAt(0) !== 0x24 /* "$" */) { + throw SyntaxError(`Expected "$" but "${expr[0]}" found.`); + } + + const nodes = []; + let descendant = false; + + let i = 1; + while (i < expr.length) { + skipWhitespace(); + parseNode(); + } + + return nodes; + + function parseNode() { + descendant = false; + + switch (true) { + case expr.charCodeAt(i) === 0x2e /* "." */ && + expr.charCodeAt(i + 1) === 0x2e /* "." */: + i += 2; + descendant = true; + + if (expr.charCodeAt(i) === 0x5b /* "[" */) { + i++; + nodes.push(parseBracket()); + } else if (expr.charCodeAt(i) === 0x40 /* "@" */) { + nodes.push(parseCustomShorthand()); + } else if (i === expr.length) { + nodes.push({ type: 'AllParentExpression' }); + } else if ( + expr.charCodeAt(i) !== 0x7e /* "~" */ && + expr.charCodeAt(i) !== 0x5e /* "^" */ + ) { + nodes.push(parseNamed()); + } else { + nodes.push({ type: 'AllParentExpression' }); + } + + break; + case expr.charCodeAt(i) === 0x2e /* "." */ && + expr.charCodeAt(i + 1) === 0x5b /* "[" */: + // jsonpath-plus compatibility + descendant = true; + i += 2; + nodes.push(parseBracket()); + break; + case expr.charCodeAt(i) === 0x2e /* "." */: + i++; + if (expr.charCodeAt(i) === 0x40 /* "@" */) { + nodes.push(parseCustomShorthand()); + } else { + nodes.push(parseNamed()); + } + + break; + case expr.charCodeAt(i) === 0x5b /* "[" */: + i++; + nodes.push(parseBracket()); + break; + case expr.charCodeAt(i) === 0x5e /* "^" */: + do { + nodes.push({ type: 'ParentExpression' }); + } while (++i < expr.length && expr.charCodeAt(i) === 0x5e /* "^ */); + + if (expr.charCodeAt(i) === 0x7e /* "~" */ || i === expr.length) { + break; + } + + throw SyntaxError( + `Expected "^", "~", or end of input but "${expr[i]}" found at ${i}`, + ); + case expr.charCodeAt(i) === 0x7e /* "~" */: + nodes.push({ type: 'KeyExpression' }); + while (++i < expr.length && expr.charCodeAt(i) === 0x5e /* "^ */); + + if (i !== expr.length) { + throw SyntaxError( + `Expected "^", "~", or end of input but "${expr[i]}" found at ${i}.`, + ); + } + + break; + case expr.charCodeAt(i) === 0x40 /* "@" */: + nodes.push(parseCustomShorthand()); + break; + default: + throw SyntaxError( + `Expected ".", "..", "^", "~", or end of input but "${expr[i]}" found at ${i}.`, + ); + } + } + + function parseNamed() { + if (expr.charCodeAt(i) === 0x2a /* "*" */) { + i++; + return { type: 'WildcardExpression', deep: descendant }; + } else { + return { + type: 'MemberExpression', + value: parseMember(), + deep: descendant, + }; + } + } + + function parseBracket() { + assertNotEndOfInput(); + const code = expr.charCodeAt(i); + const start = i; + + skipWhitespace(); + + if (code === 0x2a /* "*" */) { + i++; + skipWhitespace(); + eat(0x5d /* "]" */); + return { type: 'WildcardExpression', deep: descendant }; + } else if (code === 0x3f /* "?" */) { + i++; + eat(0x28 /* "(" */); + return parseScriptFilterExpression(); + } else if (code === 0x28 /* "(" */) { + i++; + return parseFilterExpression(); + } + + const members = []; + while (i < expr.length) { + const code = expr.charCodeAt(i); + if (code === 0x3a /* ":" */ || code === 0x2d /* "-" */) { + i = start; + return parseSliceExpression(); + } + + if (isQuote(code)) { + members.push(parseString().slice(1, -1)); + } else { + members.push(parseMember()); + } + + if (expr.charCodeAt(i) === 0x3a /* ":" */) { + i = start; + return parseSliceExpression(); + } + + skipWhitespace(); + if (expr.charCodeAt(i) !== 0x2c /* "," */) { + break; + } else { + i++; + } + } + + eat(0x5d /* "]" */); + if (members.length === 1) { + return { + type: 'MemberExpression', + value: members[0], + deep: descendant, + }; + } else { + return { + type: 'MultipleMemberExpression', + value: members, + deep: descendant, + }; + } + } + + function parseSliceExpression() { + const ranges = [0, Infinity, 1]; + let index = 0; + while (i < expr.length && index < 3) { + const code = expr.charCodeAt(i); + if (code === 0x5d /* "]" */) { + break; + } else if (code === 0x3a /* ":" */) { + index++; + i++; + } else { + ranges[index] = parseNumber(); + } + } + + eat(0x5d /* "]" */); + return { type: 'SliceExpression', value: ranges, deep: descendant }; + } + + function parseScriptFilterExpression() { + let expression = ''; + while (i < expr.length) { + const code = expr.charCodeAt(i); + i++; + if (isQuote(code)) { + i--; + expression += parseString(); + } else if (code === 0x28 /* "(" */) { + expression += parseJsFnCall(); + } else if (code === 0x29 /* ")" */) { + break; + } else { + expression += expr[i - 1]; + } + } + + eat(0x5d /* "]" */); + return { + type: 'ScriptFilterExpression', + value: expression, + deep: descendant, + }; + } + + function parseFilterExpression() { + skipWhitespace(); + eat(0x40 /* "@" */); + skipWhitespace(); + + assertNotEndOfInput(`"." or "["`); + + let member; + + switch (expr.charCodeAt(i)) { + case 0x2e /* "." */: + member = expr.slice(i + 1, i + 7); + i += 7; + break; + case 0x5b /* "[" */: + i++; + skipWhitespace(); + member = parseString().slice(1, -1); + skipWhitespace(); + eat(0x5d /* "]" */); + break; + default: + throw SyntaxError( + `Expected "." or "[" but "${expr[i]}" found at ${i}.`, + ); + } + + if (member !== 'length') { + throw Error(`Expected "length" but "${member}" found at ${i}.`); + } + + skipWhitespace(); + + eat(0x2d /* "-" */); + skipWhitespace(); + + const start = i; + const number = parseNumber(); + + if (number <= 0) { + throw SyntaxError( + `Expected positive number but "${number}" found at ${start}.`, + ); + } + + skipWhitespace(); + eat(0x29 /* ")" */); + skipWhitespace(); + eat(0x5d /* "]" */); + + return { + type: 'SliceExpression', + value: [-number, Infinity, 1], + deep: descendant, + }; + } + + function eatEscapable() { + while (i < expr.length) { + const code = expr.charCodeAt(i); + if ( + code === 0x5c /* "\\" */ || + code === 0x2f /* "/" */ || // backslash + code === 0x62 || // backspace + code === 0x66 || // form feed + code === 0x6e || // line feed + code === 0x72 || // carriage return + code === 0x74 // horizontal tab + ) { + i++; + } else { + break; + } + } + } + + function parseString() { + const leftQuoteCode = expr.charCodeAt(i); + if (!isQuote(leftQuoteCode)) { + throw SyntaxError(`Expected """ or "'" at ${i}.`); + } + + const start = i; + i++; + + while (i < expr.length) { + eatUnescaped(); + + const code = expr.charCodeAt(i); + if (code === leftQuoteCode) { + break; + } + + if (code === 0x5c /* "\\" */) { + i++; + + if (expr.charCodeAt(i) === leftQuoteCode) { + i++; + } else { + eatEscapable(); + } + } else if ( + (code === 0x22 && leftQuoteCode === 0x27) || + (code === 0x27 && leftQuoteCode === 0x22) + ) { + i++; + } else { + break; + } + } + + assertNotEndOfInput(`"${expr[start]}"`); + eat(leftQuoteCode); + + return expr.slice(start, i); + } + + function eatUnescaped() { + while (i < expr.length) { + const code = expr.charCodeAt(i); + if ( + (code >= 0x20 && code <= 0x21) || // omit 0x22 "\"" + (code >= 0x23 && code <= 0x26) || // omit 0x27 "'" + (code >= 0x28 && code <= 0x5b) || // omit 0x5c "\" + (code >= 0x5d && code <= 0xd7ff) || // skip surrogate code points + (code >= 0xe000 && code <= 0x10ffff) // skip surrogate code points + ) { + i++; + } else { + break; + } + } + } + + function parseJsFnCall() { + const start = i; + while (i < expr.length && expr.charCodeAt(i) !== 0x29 /* ")" */) { + i++; + } + + eat(0x29 /* ")" */); + return expr.slice(start - 1, i); + } + + function parseNumber() { + const start = i; + + if (expr.charCodeAt(i) === 0x2d /* "-" */) { + i++; + + if (!isDigit(expr.charCodeAt(i))) { + throw SyntaxError(`Expected [0-9] but "${expr[i]}" found at ${i}.`); + } + } + + while (i < expr.length && isDigit(expr.charCodeAt(i))) { + i++; + } + + if (start === i) { + assertNotEndOfInput('"-" or [0-9]'); + throw SyntaxError( + `Expected "-" or [0-9] but "${expr[i]}" found at ${i}.`, + ); + } + + return Number.parseInt(expr.slice(start, i), 10); + } + + function parseMember() { + const start = i; + let hasOnlyDigits = true; + + while (i < expr.length) { + const code = expr.charCodeAt(i); + if ( + code === 0x24 /* "$" */ || + code === 0x5f /* "_" */ || + code === 0x2d /* "-" ; for compat with JSONPath-plus */ || + code === 0x2f /* "/" ; for compat with JSONPath-plus */ + ) { + i++; + hasOnlyDigits &&= false; + } else if (isChar(code)) { + i++; + hasOnlyDigits &&= false; + } else if (isDigit(code)) { + i++; + } else { + break; + } + } + + if (start === i) { + assertNotEndOfInput('valid name'); + throw SyntaxError(`Expected valid name but "${expr[i]}" found at ${i}.`); + } + + const member = expr.slice(start, i); + return hasOnlyDigits ? Number.parseInt(member, 10) : member; + } + + function parseCustomShorthand() { + const start = i; + + i++; + + if (i < expr.length && expr.charCodeAt(i) === 0x40 /* "@" */) { + i++; + assertNotEndOfInput('[a-z]'); + } + + assertNotEndOfInput('[a-z]'); + + if (!isChar(expr.charCodeAt(i))) { + throw SyntaxError(`Expected [a-z] but "${expr[i]}" found at ${i}.`); + } + + while (++i < expr.length && isChar(expr.charCodeAt(i))); + + eat(0x28 /* "(" */); + skipWhitespace(); + eat(0x29 /* ")" */); + + return { + type: 'ScriptFilterExpression', + value: expr.slice(start, i), + deep: descendant, + }; + } + + function skipWhitespace() { + while (i < expr.length) { + const code = expr.charCodeAt(i); + if ( + code === 0x20 /* " " ; Space */ || + code === 0x09 /* "\t" ; H Tab */ || + code === 0x0a /* "\n" ; LF */ || + code === 0x0d /* "\r" ; CR */ + ) { + i++; + } else { + break; + } + } + } + + function eat(code) { + if (i === expr.length) { + throw SyntaxError( + `Expected "${String.fromCharCode(code)}" but end of input found at ${i}.`, + ); + } + + if (expr.charCodeAt(i) !== code) { + throw SyntaxError( + `Expected "${String.fromCharCode(code)}" but "${expr[i]}" found at ${i}.`, + ); + } + + i++; + } + + function assertNotEndOfInput(expected) { + if (i === expr.length) { + throw SyntaxError( + expected === void 0 + ? `Unexpected end of input at ${i}` + : `Expected ${expected} but end of input found at ${i}.`, + ); + } + } +} + +function isQuote(code) { + return code === 0x22 /* "\"" */ || code === 0x27 /* "'" */; +} + +function isChar(code) { + return ( + (code >= 0x41 /* "A" */ && code <= 0x5a) /* "Z" */ || + (code >= 0x61 /* "a" */ && code <= 0x7a) /* "z" */ + ); +} + +function isDigit(code) { + return code >= 0x30 /* "0" */ && code <= 0x39 /* "9" */; +} diff --git a/src/parser/parser.peg b/src/parser/parser.peg deleted file mode 100644 index 5b35693..0000000 --- a/src/parser/parser.peg +++ /dev/null @@ -1,56 +0,0 @@ -JSONPath = Root - nodes:(AllParentExpression / deep:Descendant step:(Node) { return { ...step, deep } })* - modifiers:(Modifier+)? - { return nodes.concat(Array.isArray(modifiers) ? modifiers : modifiers === null ? [] : modifiers) } - -Root = "$" - -Node = MemberExpression - / (Wildcard / "[" Wildcard "]") { return { type: "WildcardExpression" } } - / "[" expression:(ScriptExpression) "]" { return expression } - / "[" expression:(ScriptFilterExpression) "]" { return expression } - / expression:(JsonPathPlusFilterFunction / CustomScriptFilterExpression) { return expression } - / "[" value:(value:MemberIdentifier ","? { return value })* "]" { return { type: "MultipleMemberExpression", value: [...new Set(value)] } } - / "[" expression:SliceExpression "]" { return expression } - -AllParentExpression = & { return /^\$\.{2}[~^]*$/.test(input) } '..' { return { type: 'AllParentExpression' } } -MemberExpression = value:((value:Identifier) / "[" value:(MemberIdentifier) "]" { return value }) { return { type: "MemberExpression", value } } -ScriptExpression = "(" value:EvalExpression ")" { return value } -ScriptFilterExpression = "?(" value:JSScript ")" { return { type: "ScriptFilterExpression", value } } -SliceExpression = value:$((((Number ":" Number?) / (":" Number?) / Number) (":" Number)?)) { return { - type: "SliceExpression", - value: value.split(':').reduce((values, val, i) => { - if (val !== '') values[i] = Number(val); - return values; - }, [0, Infinity, 1]) - } -} - -JsonPathPlusFilterFunction = value:$("@" [a-z]+ "()") { return { type: 'ScriptFilterExpression', value } } -CustomScriptFilterExpression = value:$("@" node:JsonPathPlusFilterFunction { return node.value }) { return { type: 'ScriptFilterExpression', value } } - -KeyExpression = "~" { return { type: "KeyExpression" } } -ParentExpression = "^" { return { type: "ParentExpression" } } -Modifier = KeyExpression / ParentExpression - -Descendant = ".." { return true } / "." & "[" { return true } / "." { return false } / & [@[] { return false } - -Identifier = $([$_-] / Char / Digit)+ -MemberIdentifier = value:Identifier { return value.length > 0 && Number.isSafeInteger(Number(value)) ? Number(value) : value } / (("\"" $([^"]*) "\"") / ("'" $([^']*) "'")) { return text().slice(1, -1) } -Number = "-"? Digit+ { return Number(text()); } - -Quote = "'" / "\"" -Wildcard = "*" -Char = [A-Za-z] -Digit = [0-9] -Space = [ \t] - -JSScript = $(Char / Digit / Space / JSToken / JSString / JSScriptElementAccess / JSFnCall)+ -JSScriptElementAccess = ("[" (Digit / Char / JSString / JSFnCall)* "]") -JSString = (["] [^"]* ["]) / (['] [^']* [']) -JSToken = [ $@.,_=<>!|&+~%^*/;\-[\]] -JSFnCall = ("(" (JSString / Char / Digit / JSScriptElementAccess / JSToken / Space / JSFnCall)* ")") - -// EvalExpression = "@" value:((LengthEvalExpression) / ("."? value:MemberExpression { return value })) { return value } -EvalExpression = "@" value:(LengthEvalExpression) { return value } -LengthEvalExpression = ".length" Space* "-" Space* value:$(Digit+) { return { type: "SliceExpression", value: [-value, Infinity, 1] } } diff --git a/src/runtime/errors/index.d.ts b/src/runtime/errors/index.d.ts index fbe449e..d1606ab 100644 --- a/src/runtime/errors/index.d.ts +++ b/src/runtime/errors/index.d.ts @@ -1,6 +1 @@ -class ErrorWithCause extends Error { - public readonly cause?: unknown; -} - -export class ParserError extends ErrorWithCause {} -export class RuntimeError extends ErrorWithCause {} +export class RuntimeError extends Error {} diff --git a/src/runtime/errors/index.mjs b/src/runtime/errors/index.mjs index e45d15c..aa4fb40 100644 --- a/src/runtime/errors/index.mjs +++ b/src/runtime/errors/index.mjs @@ -1,2 +1 @@ -export { default as ParserError } from './parser-error.mjs'; export { default as RuntimeError } from './runtime-error.mjs'; diff --git a/src/runtime/errors/parser-error.mjs b/src/runtime/errors/parser-error.mjs deleted file mode 100644 index f54cd53..0000000 --- a/src/runtime/errors/parser-error.mjs +++ /dev/null @@ -1,6 +0,0 @@ -export default class ParserError extends Error { - constructor(message, expression, extra) { - super(message, extra); - this.input = expression; - } -}