UNPKG

@swaggerexpert/json-pointer

Version:
1,466 lines (1,379 loc) 89.6 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["JSONPointer"] = factory(); else root["JSONPointer"] = factory(); })(self, () => { return /******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ 58: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var _escape_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(980); /* harmony import */ var _errors_JSONPointerCompileError_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(853); const compile = referenceTokens => { if (!Array.isArray(referenceTokens)) { throw new TypeError('Reference tokens must be a list of strings or numbers'); } try { if (referenceTokens.length === 0) { return ''; } return `/${referenceTokens.map(referenceToken => { if (typeof referenceToken !== 'string' && typeof referenceToken !== 'number') { throw new TypeError('Reference token must be a string or number'); } return (0,_escape_js__WEBPACK_IMPORTED_MODULE_1__["default"])(String(referenceToken)); }).join('/')}`; } catch (error) { throw new _errors_JSONPointerCompileError_js__WEBPACK_IMPORTED_MODULE_0__["default"]('Unexpected error during JSON Pointer compilation', { cause: error, referenceTokens }); } }; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (compile); /***/ }), /***/ 71: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var _JSONPointerEvaluateError_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(369); class JSONPointerKeyError extends _JSONPointerEvaluateError_js__WEBPACK_IMPORTED_MODULE_0__["default"] {} /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (JSONPointerKeyError); /***/ }), /***/ 133: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var _errors_JSONPointerError_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(628); class EvaluationRealm { name = ''; isArray(node) { throw new _errors_JSONPointerError_js__WEBPACK_IMPORTED_MODULE_0__["default"]('Realm.isArray(node) must be implemented in a subclass'); } isObject(node) { throw new _errors_JSONPointerError_js__WEBPACK_IMPORTED_MODULE_0__["default"]('Realm.isObject(node) must be implemented in a subclass'); } sizeOf(node) { throw new _errors_JSONPointerError_js__WEBPACK_IMPORTED_MODULE_0__["default"]('Realm.sizeOf(node) must be implemented in a subclass'); } has(node, referenceToken) { throw new _errors_JSONPointerError_js__WEBPACK_IMPORTED_MODULE_0__["default"]('Realm.has(node) must be implemented in a subclass'); } evaluate(node, referenceToken) { throw new _errors_JSONPointerError_js__WEBPACK_IMPORTED_MODULE_0__["default"]('Realm.evaluate(node) must be implemented in a subclass'); } } /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (EvaluationRealm); /***/ }), /***/ 142: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var _CSTTranslator_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(979); /* harmony import */ var _unescape_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(415); class ASTTranslator extends _CSTTranslator_js__WEBPACK_IMPORTED_MODULE_0__["default"] { getTree() { const { root } = super.getTree(); return root.children.filter(({ type }) => type === 'reference-token').map(({ text }) => (0,_unescape_js__WEBPACK_IMPORTED_MODULE_1__["default"])(text)); } } /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ASTTranslator); /***/ }), /***/ 348: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var apg_lite__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(646); /* harmony import */ var _grammar_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(678); const grammar = new _grammar_js__WEBPACK_IMPORTED_MODULE_1__["default"](); const parser = new apg_lite__WEBPACK_IMPORTED_MODULE_0__.Parser(); const testArrayIndex = referenceToken => { if (typeof referenceToken !== 'string') return false; try { return parser.parse(grammar, 'array-index', referenceToken).success; } catch { return false; } }; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (testArrayIndex); /***/ }), /***/ 353: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); class TraceBuilder { #trace; #path; #realm; constructor(trace, context = {}) { this.#trace = trace; this.#trace.steps = []; this.#trace.failed = false; this.#trace.failedAt = -1; this.#trace.message = `JSON Pointer "${context.jsonPointer}" was successfully evaluated against the provided value`; this.#trace.context = { ...context, realm: context.realm.name }; this.#path = []; this.#realm = context.realm; } step({ referenceToken, input, output, success = true, reason }) { const position = this.#path.length; this.#path.push(referenceToken); const step = { referenceToken, referenceTokenPosition: position, input, inputType: this.#realm.isObject(input) ? 'object' : this.#realm.isArray(input) ? 'array' : 'unrecognized', output, success }; if (reason) { step.reason = reason; } this.#trace.steps.push(step); if (!success) { this.#trace.failed = true; this.#trace.failedAt = position; this.#trace.message = reason; } } } /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (TraceBuilder); /***/ }), /***/ 369: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var _JSONPointerError_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(628); class JSONPointerEvaluateError extends _JSONPointerError_js__WEBPACK_IMPORTED_MODULE_0__["default"] {} /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (JSONPointerEvaluateError); /***/ }), /***/ 415: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); const unescape = referenceToken => { if (typeof referenceToken !== 'string') { throw new TypeError('Reference token must be a string'); } return referenceToken.replace(/~1/g, '/').replace(/~0/g, '~'); }; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (unescape); /***/ }), /***/ 427: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var _parse_index_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(583); /* harmony import */ var _test_array_dash_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(922); /* harmony import */ var _test_array_index_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(348); /* harmony import */ var _trace_TraceBuilder_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(353); /* harmony import */ var _realms_json_index_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(839); /* harmony import */ var _errors_JSONPointerEvaluateError_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(369); /* harmony import */ var _errors_JSONPointerTypeError_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(932); /* harmony import */ var _errors_JSONPointerIndexError_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(874); /* harmony import */ var _errors_JSONPointerKeyError_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(71); const evaluate = (value, jsonPointer, { strictArrays = true, strictObjects = true, realm = new _realms_json_index_js__WEBPACK_IMPORTED_MODULE_3__["default"](), trace = true } = {}) => { const { result: parseResult, tree: referenceTokens, trace: parseTrace } = (0,_parse_index_js__WEBPACK_IMPORTED_MODULE_0__["default"])(jsonPointer, { trace: !!trace }); const tracer = typeof trace === 'object' && trace !== null ? new _trace_TraceBuilder_js__WEBPACK_IMPORTED_MODULE_8__["default"](trace, { jsonPointer, referenceTokens, strictArrays, strictObjects, realm, value }) : null; try { let output; if (!parseResult.success) { let message = `Invalid JSON Pointer: "${jsonPointer}". Syntax error at position ${parseResult.maxMatched}`; message += parseTrace ? `, expected ${parseTrace.inferExpectations()}` : ''; throw new _errors_JSONPointerEvaluateError_js__WEBPACK_IMPORTED_MODULE_4__["default"](message, { jsonPointer, currentValue: value, realm: realm.name }); } return referenceTokens.reduce((current, referenceToken, referenceTokenPosition) => { if (realm.isArray(current)) { if ((0,_test_array_dash_js__WEBPACK_IMPORTED_MODULE_1__["default"])(referenceToken)) { if (strictArrays) { throw new _errors_JSONPointerIndexError_js__WEBPACK_IMPORTED_MODULE_6__["default"](`Invalid array index "-" at position ${referenceTokenPosition} in "${jsonPointer}". The "-" token always refers to a nonexistent element during evaluation`, { jsonPointer, referenceTokens, referenceToken, referenceTokenPosition, currentValue: current, realm: realm.name }); } else { output = realm.evaluate(current, String(realm.sizeOf(current))); tracer?.step({ referenceToken, input: current, output }); return output; } } if (!(0,_test_array_index_js__WEBPACK_IMPORTED_MODULE_2__["default"])(referenceToken)) { throw new _errors_JSONPointerIndexError_js__WEBPACK_IMPORTED_MODULE_6__["default"](`Invalid array index "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": index MUST be "0", or digits without a leading "0"`, { jsonPointer, referenceTokens, referenceToken, referenceTokenPosition, currentValue: current, realm: realm.name }); } const index = Number(referenceToken); if (!Number.isSafeInteger(index)) { throw new _errors_JSONPointerIndexError_js__WEBPACK_IMPORTED_MODULE_6__["default"](`Invalid array index "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": index must be a safe integer`, { jsonPointer, referenceTokens, referenceToken, referenceTokenPosition, currentValue: current, realm: realm.name }); } if (!realm.has(current, referenceToken) && strictArrays) { throw new _errors_JSONPointerIndexError_js__WEBPACK_IMPORTED_MODULE_6__["default"](`Invalid array index "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": index not found in array`, { jsonPointer, referenceTokens, referenceToken, referenceTokenPosition, currentValue: current, realm: realm.name }); } output = realm.evaluate(current, referenceToken); tracer?.step({ referenceToken, input: current, output }); return output; } if (realm.isObject(current)) { if (!realm.has(current, referenceToken) && strictObjects) { throw new _errors_JSONPointerKeyError_js__WEBPACK_IMPORTED_MODULE_7__["default"](`Invalid object key "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": key not found in object`, { jsonPointer, referenceTokens, referenceToken, referenceTokenPosition, currentValue: current, realm: realm.name }); } output = realm.evaluate(current, referenceToken); tracer?.step({ referenceToken, input: current, output }); return output; } throw new _errors_JSONPointerTypeError_js__WEBPACK_IMPORTED_MODULE_5__["default"](`Invalid reference token "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": cannot be applied to a non-object/non-array value`, { jsonPointer, referenceTokens, referenceToken, referenceTokenPosition, currentValue: current, realm: realm.name }); }, value); } catch (error) { tracer?.step({ referenceToken: error.referenceToken, input: error.currentValue, success: false, reason: error.message }); if (error instanceof _errors_JSONPointerEvaluateError_js__WEBPACK_IMPORTED_MODULE_4__["default"]) { throw error; } throw new _errors_JSONPointerEvaluateError_js__WEBPACK_IMPORTED_MODULE_4__["default"]('Unexpected error during JSON Pointer evaluation', { cause: error, jsonPointer, referenceTokens }); } }; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (evaluate); /***/ }), /***/ 525: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var apg_lite__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(646); /* harmony import */ var _grammar_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(678); const grammar = new _grammar_js__WEBPACK_IMPORTED_MODULE_1__["default"](); const parser = new apg_lite__WEBPACK_IMPORTED_MODULE_0__.Parser(); const testArrayLocation = referenceToken => { if (typeof referenceToken !== 'string') return false; try { return parser.parse(grammar, 'array-location', referenceToken).success; } catch { return false; } }; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (testArrayLocation); /***/ }), /***/ 533: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var apg_lite__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(646); /* harmony import */ var _grammar_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(678); const grammar = new _grammar_js__WEBPACK_IMPORTED_MODULE_1__["default"](); const parser = new apg_lite__WEBPACK_IMPORTED_MODULE_0__.Parser(); const testReferenceToken = referenceToken => { if (typeof referenceToken !== 'string') return false; try { return parser.parse(grammar, 'reference-token', referenceToken).success; } catch { return false; } }; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (testReferenceToken); /***/ }), /***/ 544: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var _parse_index_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(583); const testJSONPointer = jsonPointer => { try { const parseResult = (0,_parse_index_js__WEBPACK_IMPORTED_MODULE_0__["default"])(jsonPointer, { translator: null }); return parseResult.result.success; } catch { return false; } }; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (testJSONPointer); /***/ }), /***/ 583: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var apg_lite__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(646); /* harmony import */ var _grammar_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(678); /* harmony import */ var _errors_JSONPointerParseError_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(819); /* harmony import */ var _translators_ASTTranslator_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(142); /* harmony import */ var _trace_Trace_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(992); const grammar = new _grammar_js__WEBPACK_IMPORTED_MODULE_4__["default"](); const parse = (jsonPointer, { translator = new _translators_ASTTranslator_js__WEBPACK_IMPORTED_MODULE_2__["default"](), stats = false, trace = false } = {}) => { if (typeof jsonPointer !== 'string') { throw new TypeError('JSON Pointer must be a string'); } try { const parser = new apg_lite__WEBPACK_IMPORTED_MODULE_0__.Parser(); if (translator) parser.ast = translator; if (stats) parser.stats = new apg_lite__WEBPACK_IMPORTED_MODULE_0__.Stats(); if (trace) parser.trace = new _trace_Trace_js__WEBPACK_IMPORTED_MODULE_3__["default"](); const result = parser.parse(grammar, 'json-pointer', jsonPointer); return { result, tree: result.success && translator ? parser.ast.getTree() : undefined, stats: parser.stats, trace: parser.trace }; } catch (error) { throw new _errors_JSONPointerParseError_js__WEBPACK_IMPORTED_MODULE_1__["default"]('Unexpected error during JSON Pointer parsing', { cause: error, jsonPointer }); } }; /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (parse); /***/ }), /***/ 628: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); class JSONPointerError extends Error { constructor(message, options = undefined) { super(message, options); this.name = this.constructor.name; if (typeof message === 'string') { this.message = message; } if (typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor); } else { this.stack = new Error(message).stack; } /** * This needs to stay here until our minimum supported version of Node.js is >= 16.9.0. * Node.js is >= 16.9.0 supports error causes natively. */ if (options != null && typeof options === 'object' && Object.prototype.hasOwnProperty.call(options, 'cause') && !('cause' in this)) { const { cause } = options; this.cause = cause; if (cause instanceof Error && 'stack' in cause) { this.stack = `${this.stack}\nCAUSE: ${cause.stack}`; } } /** * Allows to assign arbitrary properties to the error object. */ if (options != null && typeof options === 'object') { const { cause, ...causelessOptions } = options; Object.assign(this, causelessOptions); } } } /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (JSONPointerError); /***/ }), /***/ 632: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony import */ var _EvaluationRealm_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(133); /* harmony import */ var _errors_JSONPointerEvaluateError_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(369); class CompositeEvaluationRealm extends _EvaluationRealm_js__WEBPACK_IMPORTED_MODULE_0__["default"] { name = 'composite'; realms = []; constructor(realms) { super(); this.realms = realms; } isArray(node) { return this.#findRealm(node).isArray(node); } isObject(node) { return this.#findRealm(node).isObject(node); } sizeOf(node) { return this.#findRealm(node).sizeOf(node); } has(node, referenceToken) { return this.#findRealm(node).has(node, referenceToken); } evaluate(node, referenceToken) { return this.#findRealm(node).evaluate(node, referenceToken); } #findRealm(node) { for (const realm of this.realms) { if (realm.isArray(node) || realm.isObject(node)) { return realm; } } throw new _errors_JSONPointerEvaluateError_js__WEBPACK_IMPORTED_MODULE_1__["default"]('No suitable evaluation realm found for value', { currentValue: node }); } } const compose = (...realms) => new CompositeEvaluationRealm(realms); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (compose); /***/ }), /***/ 646: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ Ast: () => (/* binding */ Ast), /* harmony export */ Parser: () => (/* binding */ Parser), /* harmony export */ Stats: () => (/* binding */ Stats), /* harmony export */ Trace: () => (/* binding */ Trace), /* harmony export */ identifiers: () => (/* binding */ identifiers), /* harmony export */ utilities: () => (/* binding */ utilities) /* harmony export */ }); /* ************************************************************************************* * copyright: Copyright (c) 2023 Lowell D. Thomas, all rights reserved * license: BSD-2-Clause (https://opensource.org/licenses/BSD-2-Clause) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ********************************************************************************* */ const Parser = function fnparser() { const id = identifiers; const utils = utilities; const p = this; const thisFileName = 'parser.js: Parser(): '; const systemData = function systemData() { this.state = id.ACTIVE; this.phraseLength = 0; this.refresh = () => { this.state = id.ACTIVE; this.phraseLength = 0; }; }; p.ast = undefined; p.stats = undefined; p.trace = undefined; p.callbacks = []; let lookAhead = 0; let treeDepth = 0; let maxTreeDepth = 0; let nodeHits = 0; let maxMatched = 0; let rules = undefined; let udts = undefined; let opcodes = undefined; let chars = undefined; let sysData = new systemData(); let ruleCallbacks = undefined; let udtCallbacks = undefined; let userData = undefined; const clear = () => { lookAhead = 0; treeDepth = 0; maxTreeDepth = 0; nodeHits = 0; maxMatched = 0; rules = undefined; udts = undefined; opcodes = undefined; chars = undefined; sysData.refresh(); ruleCallbacks = undefined; udtCallbacks = undefined; userData = undefined; }; const initializeCallbacks = () => { const functionName = `${thisFileName}initializeCallbacks(): `; let i; ruleCallbacks = []; udtCallbacks = []; for (i = 0; i < rules.length; i += 1) { ruleCallbacks[i] = undefined; } for (i = 0; i < udts.length; i += 1) { udtCallbacks[i] = undefined; } let func; const list = []; for (i = 0; i < rules.length; i += 1) { list.push(rules[i].lower); } for (i = 0; i < udts.length; i += 1) { list.push(udts[i].lower); } for (const index in p.callbacks) { if (p.callbacks.hasOwnProperty(index)) { i = list.indexOf(index.toLowerCase()); if (i < 0) { throw new Error(`${functionName}syntax callback '${index}' not a rule or udt name`); } func = p.callbacks[index] ? p.callbacks[index] : undefined; if (typeof func === 'function' || func === undefined) { if (i < rules.length) { ruleCallbacks[i] = func; } else { udtCallbacks[i - rules.length] = func; } } else { throw new Error(`${functionName}syntax callback[${index}] must be function reference or falsy)`); } } } }; p.parse = (grammar, startName, inputString, callbackData) => { const functionName = `${thisFileName}parse(): `; clear(); chars = utils.stringToChars(inputString); rules = grammar.rules; udts = grammar.udts; const lower = startName.toLowerCase(); let startIndex = undefined; for (const i in rules) { if (rules.hasOwnProperty(i)) { if (lower === rules[i].lower) { startIndex = rules[i].index; break; } } } if (startIndex === undefined) { throw new Error(`${functionName}start rule name '${startRule}' not recognized`); } initializeCallbacks(); if (p.trace) { p.trace.init(rules, udts, chars); } if (p.stats) { p.stats.init(rules, udts); } if (p.ast) { p.ast.init(rules, udts, chars); } userData = callbackData; /* create a dummy opcode for the start rule */ opcodes = [ { type: id.RNM, index: startIndex, }, ]; /* execute the start rule */ opExecute(0, 0); opcodes = undefined; /* test and return the sysData */ let success = false; switch (sysData.state) { case id.ACTIVE: throw new Error(`${functionName}final state should never be 'ACTIVE'`); case id.NOMATCH: success = false; break; case id.EMPTY: case id.MATCH: if (sysData.phraseLength === chars.length) { success = true; } else { success = false; } break; default: throw new Error('unrecognized state'); } return { success, state: sysData.state, stateName: id.idName(sysData.state), length: chars.length, matched: sysData.phraseLength, maxMatched, maxTreeDepth, nodeHits, }; }; // The `ALT` operator.<br> // Executes its child nodes, from left to right, until it finds a match. // Fails if *all* of its child nodes fail. const opALT = (opIndex, phraseIndex) => { const op = opcodes[opIndex]; for (let i = 0; i < op.children.length; i += 1) { opExecute(op.children[i], phraseIndex); if (sysData.state !== id.NOMATCH) { break; } } }; // The `CAT` operator.<br> // Executes all of its child nodes, from left to right, // concatenating the matched phrases. // Fails if *any* child nodes fail. const opCAT = (opIndex, phraseIndex) => { let success; let astLength; let catCharIndex; let catPhrase; const op = opcodes[opIndex]; if (p.ast) { astLength = p.ast.getLength(); } success = true; catCharIndex = phraseIndex; catPhrase = 0; for (let i = 0; i < op.children.length; i += 1) { opExecute(op.children[i], catCharIndex); if (sysData.state === id.NOMATCH) { success = false; break; } else { catCharIndex += sysData.phraseLength; catPhrase += sysData.phraseLength; } } if (success) { sysData.state = catPhrase === 0 ? id.EMPTY : id.MATCH; sysData.phraseLength = catPhrase; } else { sysData.state = id.NOMATCH; sysData.phraseLength = 0; if (p.ast) { p.ast.setLength(astLength); } } }; // The `REP` operator.<br> // Repeatedly executes its single child node, // concatenating each of the matched phrases found. // The number of repetitions executed and its final sysData depends // on its `min` & `max` repetition values. const opREP = (opIndex, phraseIndex) => { let astLength; let repCharIndex; let repPhrase; let repCount; const op = opcodes[opIndex]; if (op.max === 0) { // this is an empty-string acceptor // deprecated: use the TLS empty string operator, "", instead sysData.state = id.EMPTY; sysData.phraseLength = 0; return; } repCharIndex = phraseIndex; repPhrase = 0; repCount = 0; if (p.ast) { astLength = p.ast.getLength(); } while (1) { if (repCharIndex >= chars.length) { /* exit on end of input string */ break; } opExecute(opIndex + 1, repCharIndex); if (sysData.state === id.NOMATCH) { /* always end if the child node fails */ break; } if (sysData.state === id.EMPTY) { /* REP always succeeds when the child node returns an empty phrase */ /* this may not seem obvious, but that's the way it works out */ break; } repCount += 1; repPhrase += sysData.phraseLength; repCharIndex += sysData.phraseLength; if (repCount === op.max) { /* end on maxed out reps */ break; } } /* evaluate the match count according to the min, max values */ if (sysData.state === id.EMPTY) { sysData.state = repPhrase === 0 ? id.EMPTY : id.MATCH; sysData.phraseLength = repPhrase; } else if (repCount >= op.min) { sysData.state = repPhrase === 0 ? id.EMPTY : id.MATCH; sysData.phraseLength = repPhrase; } else { sysData.state = id.NOMATCH; sysData.phraseLength = 0; if (p.ast) { p.ast.setLength(astLength); } } }; // Validate the callback function's returned sysData values. // It's the user's responsibility to get them right // but `RNM` fails if not. const validateRnmCallbackResult = (rule, sysData, charsLeft, down) => { if (sysData.phraseLength > charsLeft) { let str = `${thisFileName}opRNM(${rule.name}): callback function error: `; str += `sysData.phraseLength: ${sysData.phraseLength}`; str += ` must be <= remaining chars: ${charsLeft}`; throw new Error(str); } switch (sysData.state) { case id.ACTIVE: if (!down) { throw new Error( `${thisFileName}opRNM(${rule.name}): callback function return error. ACTIVE state not allowed.` ); } break; case id.EMPTY: sysData.phraseLength = 0; break; case id.MATCH: if (sysData.phraseLength === 0) { sysData.state = id.EMPTY; } break; case id.NOMATCH: sysData.phraseLength = 0; break; default: throw new Error( `${thisFileName}opRNM(${rule.name}): callback function return error. Unrecognized return state: ${sysData.state}` ); } }; // The `RNM` operator.<br> // This operator will acts as a root node for a parse tree branch below and // returns the matched phrase to its parent. // However, its larger responsibility is handling user-defined callback functions and `AST` nodes. // Note that the `AST` is a separate object, but `RNM` calls its functions to create its nodes. const opRNM = (opIndex, phraseIndex) => { let astLength; let astDefined; let savedOpcodes; const op = opcodes[opIndex]; const rule = rules[op.index]; const callback = ruleCallbacks[rule.index]; /* ignore AST in look ahead (AND or NOT operator above) */ if (!lookAhead) { astDefined = p.ast && p.ast.ruleDefined(op.index); if (astDefined) { astLength = p.ast.getLength(); p.ast.down(op.index, rules[op.index].name); } } if (callback) { /* call user's callback going down the parse tree*/ const charsLeft = chars.length - phraseIndex; callback(sysData, chars, phraseIndex, userData); validateRnmCallbackResult(rule, sysData, charsLeft, true); if (sysData.state === id.ACTIVE) { savedOpcodes = opcodes; opcodes = rule.opcodes; opExecute(0, phraseIndex); opcodes = savedOpcodes; /* call user's callback going up the parse tree*/ callback(sysData, chars, phraseIndex, userData); validateRnmCallbackResult(rule, sysData, charsLeft, false); } /* implied else clause: just accept the callback sysData - RNM acting as UDT */ } else { /* no callback - just execute the rule */ savedOpcodes = opcodes; opcodes = rule.opcodes; opExecute(0, phraseIndex, sysData); opcodes = savedOpcodes; } if (!lookAhead) { /* end AST */ if (astDefined) { if (sysData.state === id.NOMATCH) { p.ast.setLength(astLength); } else { p.ast.up(op.index, rule.name, phraseIndex, sysData.phraseLength); } } } }; // The `TRG` operator.<br> // Succeeds if the single first character of the phrase is // within the `min - max` range. const opTRG = (opIndex, phraseIndex) => { const op = opcodes[opIndex]; sysData.state = id.NOMATCH; if (phraseIndex < chars.length) { if (op.min <= chars[phraseIndex] && chars[phraseIndex] <= op.max) { sysData.state = id.MATCH; sysData.phraseLength = 1; } } }; // The `TBS` operator.<br> // Matches its pre-defined phrase against the input string. // All characters must match exactly. // Case-sensitive literal strings (`'string'` & `%s"string"`) are translated to `TBS` // operators by `apg`. // Phrase length of zero is not allowed. // Empty phrases can only be defined with `TLS` operators. const opTBS = (opIndex, phraseIndex) => { const op = opcodes[opIndex]; const len = op.string.length; sysData.state = id.NOMATCH; if (phraseIndex + len <= chars.length) { for (let i = 0; i < len; i += 1) { if (chars[phraseIndex + i] !== op.string[i]) { return; } } sysData.state = id.MATCH; sysData.phraseLength = len; } /* implied else NOMATCH */ }; // The `TLS` operator.<br> // Matches its pre-defined phrase against the input string. // A case-insensitive match is attempted for ASCII alphbetical characters. // `TLS` is the only operator that explicitly allows empty phrases. // `apg` will fail for empty `TBS`, case-sensitive strings (`''`) or // zero repetitions (`0*0RuleName` or `0RuleName`). const opTLS = (opIndex, phraseIndex) => { let code; const op = opcodes[opIndex]; sysData.state = id.NOMATCH; const len = op.string.length; if (len === 0) { /* EMPTY match allowed for TLS */ sysData.state = id.EMPTY; return; } if (phraseIndex + len <= chars.length) { for (let i = 0; i < len; i += 1) { code = chars[phraseIndex + i]; if (code >= 65 && code <= 90) { code += 32; } if (code !== op.string[i]) { return; } } sysData.state = id.MATCH; sysData.phraseLength = len; } /* implied else NOMATCH */ }; // Validate the callback function's returned sysData values. // It's the user's responsibility to get it right but `UDT` fails if not. const validateUdtCallbackResult = (udt, sysData, charsLeft) => { if (sysData.phraseLength > charsLeft) { let str = `${thisFileName}opUDT(${udt.name}): callback function error: `; str += `sysData.phraseLength: ${sysData.phraseLength}`; str += ` must be <= remaining chars: ${charsLeft}`; throw new Error(str); } switch (sysData.state) { case id.ACTIVE: throw new Error(`${thisFileName}opUDT(${udt.name}) ACTIVE state return not allowed.`); case id.EMPTY: if (udt.empty) { sysData.phraseLength = 0; } else { throw new Error(`${thisFileName}opUDT(${udt.name}) may not return EMPTY.`); } break; case id.MATCH: if (sysData.phraseLength === 0) { if (udt.empty) { sysData.state = id.EMPTY; } else { throw new Error(`${thisFileName}opUDT(${udt.name}) may not return EMPTY.`); } } break; case id.NOMATCH: sysData.phraseLength = 0; break; default: throw new Error( `${thisFileName}opUDT(${udt.name}): callback function return error. Unrecognized return state: ${sysData.state}` ); } }; // The `UDT` operator.<br> // Simply calls the user's callback function, but operates like `RNM` with regard to the `AST` // and back referencing. // There is some ambiguity here. `UDT`s act as terminals for phrase recognition but as named rules // for `AST` nodes and back referencing. // See [`ast.js`](./ast.html) for usage. const opUDT = (opIndex, phraseIndex) => { let astLength; let astIndex; let astDefined; const op = opcodes[opIndex]; const udt = udts[op.index]; sysData.UdtIndex = udt.index; /* ignore AST in look ahead */ if (!lookAhead) { astDefined = p.ast && p.ast.udtDefined(op.index); if (astDefined) { astIndex = rules.length + op.index; astLength = p.ast.getLength(); p.ast.down(astIndex, udt.name); } } /* call the UDT */ const charsLeft = chars.length - phraseIndex; udtCallbacks[op.index](sysData, chars, phraseIndex, userData); validateUdtCallbackResult(udt, sysData, charsLeft); if (!lookAhead) { /* end AST */ if (astDefined) { if (sysData.state === id.NOMATCH) { p.ast.setLength(astLength); } else { p.ast.up(astIndex, udt.name, phraseIndex, sysData.phraseLength); } } } }; // The `AND` operator.<br> // This is the positive `look ahead` operator. // Executes its single child node, returning the EMPTY state // if it succeedsand NOMATCH if it fails. // *Always* backtracks on any matched phrase and returns EMPTY on success. const opAND = (opIndex, phraseIndex) => { lookAhead += 1; opExecute(opIndex + 1, phraseIndex); lookAhead -= 1; sysData.phraseLength = 0; switch (sysData.state) { case id.EMPTY: sysData.state = id.EMPTY; break; case id.MATCH: sysData.state = id.EMPTY; break; case id.NOMATCH: sysData.state = id.NOMATCH; break; default: throw new Error(`opAND: invalid state ${sysData.state}`); } }; // The `NOT` operator.<br> // This is the negative `look ahead` operator. // Executes its single child node, returning the EMPTY state // if it *fails* and NOMATCH if it succeeds. // *Always* backtracks on any matched phrase and returns EMPTY // on success (failure of its child node). const opNOT = (opIndex, phraseIndex) => { lookAhead += 1; opExecute(opIndex + 1, phraseIndex); lookAhead -= 1; sysData.phraseLength = 0; switch (sysData.state) { case id.EMPTY: case id.MATCH: sysData.state = id.NOMATCH; break; case id.NOMATCH: sysData.state = id.EMPTY; break; default: throw new Error(`opNOT: invalid state ${sysData.state}`); } }; const opExecute = (opIndex, phraseIndex) => { const functionName = `${thisFileName}opExecute(): `; const op = opcodes[opIndex]; nodeHits += 1; if (treeDepth > maxTreeDepth) { maxTreeDepth = treeDepth; } treeDepth += 1; sysData.refresh(); if (p.trace) { p.trace.down(op, phraseIndex); } switch (op.type) { case id.ALT: opALT(opIndex, phraseIndex); break; case id.CAT: opCAT(opIndex, phraseIndex); break; case id.REP: opREP(opIndex, phraseIndex); break; case id.RNM: opRNM(opIndex, phraseIndex); break; case id.TRG: opTRG(opIndex, phraseIndex); break; case id.TBS: opTBS(opIndex, phraseIndex); break; case id.TLS: opTLS(opIndex, phraseIndex); break; case id.UDT: opUDT(opIndex, phraseIndex); break; case id.AND: opAND(opIndex, phraseIndex); break; case id.NOT: opNOT(opIndex, phraseIndex); break; default: throw new Error(`${functionName}unrecognized operator`); } if (!lookAhead) { if (phraseIndex + sysData.phraseLength > maxMatched) { maxMatched = phraseIndex + sysData.phraseLength; } } if (p.stats) { p.stats.collect(op, sysData); } if (p.trace) { p.trace.up(op, sysData.state, phraseIndex, sysData.phraseLength); } treeDepth -= 1; }; }; const Ast = function fnast() { const thisFileName = 'parser.js: Ast()): '; const id = identifiers; const utils = utilities; const a = this; let rules = undefined; let udts = undefined; let chars = undefined; let nodeCount = 0; const nodeCallbacks = []; const stack = []; const records = []; a.callbacks = []; /* called by the parser to initialize the AST with the rules, UDTs and the input characters */ a.init = (rulesIn, udtsIn, charsIn) => { stack.length = 0; records.length = 0; nodeCount = 0; rules = rulesIn; udts = udtsIn; chars = charsIn; let i; const list = []; for (i = 0; i < rules.length; i += 1) { list.push(rules[i].lower); } for (i = 0; i < udts.length; i += 1) { list.push(udts[i].lower); } nodeCount = rules.length + udts.length; for (i = 0; i < nodeCount; i += 1) { nodeCallbacks[i] = undefined; } for (const index in a.callbacks) { if (a.callbacks.hasOwnProperty(index)) { const lower = index.toLowerCase(); i = list.indexOf(lower); if (i < 0) { throw new Error(`${thisFileName}init: node '${index}' not a rule or udt name`); } nodeCallbacks[i] = a.callbacks[index]; } } }; /* AST node rule callbacks - called by the parser's `RNM` operator */ a.ruleDefined = (index) => !!nodeCallbacks[index]; /* AST node UDT callbacks - called by the parser's `UDT` operator */ a.udtDefined = (index) => !!nodeCallbacks[rules.length + index]; /* called by the parser's `RNM` & `UDT` operators builds a record for the downward traversal of the node */ a.down = (callbackIndex, name) => { const thisIndex = records.length; stack.push(thisIndex); records.push({ name, thisIndex, thatIndex: undefined, state: id.SEM_PRE, callbackIndex, phraseIndex: undefined, phraseLength: undefined, stack: stack.length, }); return thisIndex; }; /* called by the parser's `RNM` & `UDT` operators */ /* builds a record for the upward traversal of the node */ a.up = (callbackIndex, name, phraseIndex, phraseLength) => { const thisIndex = records.length; const thatIndex = stack.pop(); records.push({ name, thisIndex, thatIndex, state: id.SEM_POST, callbackIndex, phraseIndex, phraseLength, stack: stack.length, }); records[thatIndex].thatIndex = thisIndex; records[thatIndex].phraseIndex = phraseIndex; records[thatIndex].phraseLength = phraseLength; return thisIndex; }; // Called by the user to translate the AST. // Translate means to associate or apply some semantic action to the // phrases that were syntactically matched to the AST nodes according // to the defining grammar. // ``` // data - optional user-defined data // passed to the callback functions by the translator // ``` a.translate = (data) => { let ret; let callback; let record; for (let i = 0; i < records.length; i += 1) { record = records[i]; callback = nodeCallbacks[record.callbackIndex]; if (callback) { if (record.state === id.SEM_PRE) { callback(id.SEM_PRE, chars, record.phraseIndex, record.phraseLength, data); } else if (callback) { callback(id.SEM_POST, chars, record.phraseIndex, record.phraseLength, data); } } } }; /* called by the parser to reset the length of the records array */ /* necessary on backtracking */ a.setLength = (length) => { records.length = length; if (length > 0) { stack.length = records[length - 1].stack; } else { stack.length = 0; } }; /* called by the parser to get the length of the records array */ a.getLength = () => records.length; /* helper for XML display */ function indent(n) { let ret = ''; while (n-- > 0) { ret += ' '; } return ret; } // Generate an `XML` version of the AST. // Useful if you want to use a special or favorite XML parser to translate the // AST. Node data are JavaScript strings. a.toXml = () => { let xml = ''; let depth = 0; xml += '<?xml version="1.0" encoding="utf-8"?>\n'; xml += `<root nodes="${records.length / 2}" characters="${chars.length}">\n`; xml += `<!-- input string -->\n`; xml += indent(depth + 2); xml += utils.charsToString(chars); xml += '\n'; records.forEach((rec) => { if (rec.state === id.SEM_PRE) { depth += 1; xml += indent(depth); xml += `<node name="${rec.name}" index="${rec.phraseIndex}" length="${rec.phraseLength}">\n`; xml += indent(depth + 2); xml += utils.charsToString(chars, rec.phraseIndex, rec.phraseLength); xml += '\n'; } else { xml += indent(depth); xml += `</node><!-- name="${rec.name}" -->\n`; depth -= 1; } }); xml += '</root>\n'; return xml; }; }; const Trace = function fntrace() { const id = identifiers; const utils = utilities; const thisFile = 'parser.js: Trace(): '; let chars = undefined; let rules = undefined; let udts = undefined; let out = ''; let treeDepth = 0; const MAX_PHRASE = 100; const t = this; const indent = (n) => { let ret = ''; let count = 0; if (n >= 0) { while (n--) { count += 1; if (count === 5) { ret += '|'; count = 0; } else { ret += '.'; } } } return ret; }; t.init = (r, u, c) => { rules = r; udts = u; chars = c; }; const opName = (op) => { let name; switch (op.type) { case id.ALT: name = 'ALT'; break; case id.CAT: name = 'CAT'; break; case id.REP: if (op.