let trace_on = false; let location_with_token = false; /** * Keep location with return value of token parsers. * If set/true: the token functions return a list of [ = this.lastError.pos ? newError : this.lastError; } clearLastError() { this.lastError = null; } } class ParserError extends Error { constructor(message, pos, noRecover = false) { super(message); this.pos = pos; this.data = null; this.filePath = null; this.noRecover = noRecover } setLastError(lastError) { if (lastError == null) { return this; } return this.setLastError_(lastError); } setLastError_(lastError) { if (lastError.pos > this.pos) { lastError.lastError = this; return lastError; } if (lastError.pos == this.pos) { this.lastError = lastError.lastError; return this; } if (this.lastError == null) { return lastError; } this.lastError = this.lastError.setLastError_(lastError); return this; } } function getMostMatchingError(errors) { let mostMatching = null; let lastErrorPos = -1; for(let i=0; i< errors.length; ++i) { let errorPos = errors[i].pos; if (errorPos > lastErrorPos) { lastErrorPos = errorPos; mostMatching = errors[i]; } } return [ mostMatching, lastErrorPos ]; } function throwError(error, lastError) { let rt = error.setLastError(lastError); //console.log("throw: " + rt.pos + " : " + rt.message); throw rt; } function getLineNo(data, pos) { let count = 1; for(let i=0; i= data.length) { pos = data.length - 1; } // skip back whitespaces. for(;pos>0 && isSpace(data.charAt(pos));--pos); let start = pos; for(;start>=0 && data.charAt(start) != '\n'; --start); start += 1; let end = pos; for(;end < data.length && data.charAt(end) != '\n'; ++end); if (start > pos) { start = pos; } return [ data.substring(start, end), pos-start+1]; } function isSpace(ch) { return ch.trim() == ""; } let skipWhitespace = function(state) { while(state.pos < state.data.length) { let ch = state.data.charAt(state.pos); if (!isSpace(ch)) { break } state.pos+=1; } } /** * set function that is called in order to skip whitespaces * this can be used to skip comments as well * @param argFunction - function that skips whitespaces. The function gets a State object as parameter and advances the pos member) */ function setSkipWhitespaceFunction(argFunction) { skipWhitespace = argFunction; } /** * @returns function that skips whitespaces */ function getSkipWhitespaceFunction() { return skipWhitespace; } /** * format a parser error * @param er - instance of ParserError * @param data - the string that was being parsed. * @returns the formatted message */ function formatParserError(er, data) { if (er instanceof ParserError) { //console.log("er: " + JSON.stringify(er)); let msg = ""; let pos = er.pos; if (er.filePath != null) { msg = er.filePath + ": "; } msg += er.message; let entry = getLineAt(data, pos); let prefix = getLineNo(data,pos) + ": "; msg += "\n" + prefix + entry[0] + "\n" + Array(entry[1]+prefix.length).join(".") + "^"; if (er.nextException != null) { msg += "\n"; msg += formatParserError(er.nextException, data); } return msg; } return er.message + "\n" + er.stack; } const makeTracer = function(parser, title) { if (!trace_on) { parser.prototype['title'] = title; return parser; } let ret = function(state) { console.log("enter parser: " + title); //console.debug("enter parser: state.pos: " + state.pos + (state.lastError != null ? " lastError.pos: " + state.lastError.getLatestPos() : "")); let entry = getLineAt(state.data,state.pos); let msg = entry[0] + "\n" + " ".repeat(entry[1]) + "^"; console.log(msg); let ret = null; try { ret = parser(state); } catch(e) { console.log("error in parser: " + title); throw e; } entry = getLineAt(state.data,state.pos); msg = entry[0] + "\n" + " ".repeat(entry[1]) + "^"; console.log(msg); console.log("exit parser: " + title); //console.debug("exit parser: state.pos: " + state.pos + (state.lastError != null ? " lastError.pos: " + state.lastError.getLatestPos() : "")); return ret; } ret.prototype['title'] = title; return ret; } /** * Returns a parser that can matches/consumes a given regular expression * @param regex - the regex to match * @param name - optional name of the parser (for more readable error messages) * @returns is setKeepLocationWithToken - returns a list [ "", offset_of_token ] , else the token string is returned. */ const makeRegexParser = function (regex, name = null) { if (!(regex instanceof RegExp)) { let msg = "Illegal parser definition, expected RegRxp, got: " + typeof(regex); throw new Error(msg); } if (name == null) { name = regex.source; } return makeTracer(function (state) { skipWhitespace(state); if (state.pos >= state.data.length) { throw new ParserError(name + " expected", state.pos); } let remainder = state.data.substring(state.pos); let tres = regex.exec( remainder ); if (tres != null) { let data = null; if (location_with_token) { data = [ tres[0], state.pos ]; } else { data = tres[0]; } state.pos = state.pos + tres[0].length; return data; } throw new ParserError(name + " expected", state.pos); }, name=name); } /** * returns parser that consumes argument token string * @param token * @returns is setKeepLocationWithToken - returns a list [ "", offset_of_token ] , else the token string is returned. */ const makeTokenParser = function (token) { if (typeof(token) != 'string') { let msg = "Illegal parser definition, expected string, got " + typeof(token); console.log(msg); throw new Error(msg); } return makeTracer(function (state) { skipWhitespace(state); if (state.pos >= state.data.length) { throw new ParserError(token + " expected", state.pos); } if (state.data.substring(state.pos, state.pos + token.length) == token) { let data = null; if (location_with_token) { data = [ token, state.pos ]; } else { data = token; } state.pos = state.pos + token.length; return data; } throw new ParserError(token + " expected", state.pos); }, name=token); } function requireFunction(f) { if (!(f instanceof Function)) { let msg="argument is not a function"; console.trace(msg); throw new Error(msg); } } function requireArrayOfFunctions(a) { if (!(a instanceof Array)) { let msg = "argument is not a array"; console.trace(msg); throw new Error(msg); } for(let i=0; i < a.length; ++i) { if (!a[i] instanceof Function) { let msg = "Array element " + i + " is not a function. Requires array of functions"; console.trace(msg); throw new Error(msg); } } } /** * returns parser that applies all argument parsers sequentially. The parser succeeds if all argument parsers succeeded to parse their input. * @param arrayOfParsers - array of argument parsers, each one applied after the previous one. * @param title - optional name of the parser (for more readable error messages) * @param concat - if not true: result of each parser is pushed to result array. (default value). if set: result of each parser is concatenated to the result array * @param clearError - set to true if the last term is an optional sequence. * @returns an array with the results returned by each of the sequence parser. */ const makeSequenceParser = function(arrayOfParsers, title ="SequenceParser", concat = false, clearError = true) { requireArrayOfFunctions(arrayOfParsers); return makeTracer( function(state) { let result = []; skipWhitespace(state); // for error reporting. let startPos = state.pos; for(let i=0; i 0) { errorTitle += " after " + arrayOfParsers[ i - 1].title; } throwError(new ParserError(errorTitle, errorPos), er); } else { throw er; } } } if (clearError) { //console.log("clearLastError: " + title); state.clearLastError(); } // todo: i don't like that (get rid of that) // Achtung! if last element is an empty array then chop it off // that's for cases when we have an optional repetition of clauses if (result.length != 0) { let last = result[result.length-1]; if (last instanceof Array && last.length == 0) { result.pop(); } } return result; }, title); } /** * returns parser that applies argument parser repeatedly * @param parser - argument parsing function * @param minMatching - number of minimal matches (default 1) * @param maxMatching - number of maximum allowed matches, -1 - no limit * @param name - optional name of the parser (for more readable error messages) * @param concat - if not true: result of each parser is pushed to result array. (default value). if set: result of each parser is concatenated to the result array * @returns a list with the results returned by each nested parser invocation */ const makeRepetitionParser = function(parser, minMatching = 1, maxMatching = -1, name = "RepetitionParser", concat = false) { requireFunction(parser); return makeTracer( function (state) { let result = []; let matching = 0; let firstPos = state.pos; for (; state.pos < state.data.length && (maxMatching == -1 || matching < maxMatching); ++matching) { let lastPos = state.pos; try { let res = parser(state); if (concat) { result = result.concat(res); } else { result.push(res); } } catch(er) { state.pos = lastPos; state.setStateError(er); break; } } if (matching < minMatching && minMatching != 0) { state.pos = firstPos; if (matching == 0) { throw new ParserError("didn't match even one in " + name, state.pos); } else { throw new ParserError("didn't match enough in " + name, state.pos); } } return result; }, name); } /** * returns parser that applies argument parser repeatedly * @param parserMandatory - argument parsing function * @param parserRepetition - argument parsing function * @param title - optional name of the parser (for more readable error messages) * @param concat - if not true: result of each parser is pushed to result array. (default value). if set: result of each parser is concatenated to the result array * @returns a list with the results returned by each nested parser invocation */ const makeRepetitionRecClause = function(parserMandatory, parserRepetition, title = "RecursiveClause", concat = false) { requireFunction(parserMandatory); requireFunction(parserRepetition); return makeTracer( function (state) { let result = []; let res = parserMandatory(state); if (concat) { result = result.concat(res); } else { result.push(res); } while(true) { let lastPos = state.pos; try { let res = parserRepetition(state); if (concat) { result = result.concat(res); } else { result.push( res ); } } catch(er) { state.pos = lastPos; if (!(er instanceof ParserError)) { console.log(er.message + " " + er.stack) } else { state.setStateError(er); } break; } } return result; }, title); } /** * returns parser that applies the argument parser at least once * @param parser * @param name - optional name of the parser (for more readable error messages) * @returns parsing function that receives a State object for the current position within the input and returns the next state.* */ const makeOptParser = function(parser, name = "OptParser") { return makeRepetitionParser(parser, 0, 1, name); } function getParserTitle(parser) { if ("title" in parser.prototype) { return parser.prototype.title; } return null; } function makeDetailedAltErrorMessage(arrayOfParsers) { let errMsg = ""; for(let i =0; i