UNPKG

jsoi-lib

Version:

A minimalistic, zero dependency javascript library that can perform string interpolation recursively over javascript objects preserving types. Great for dynamic configuration loading, supports asynchronous loading of objects with user-defined callbacks in

1 lines 121 kB
{"version":3,"file":"interpolation_objects.min.mjs","sources":["../src/expression_parser.js","../src/interpolation_objects.js"],"sourcesContent":["\n//---------------------------------------------------------------------------------------------------------------------\n// Exceptions\n//---------------------------------------------------------------------------------------------------------------------\nclass ExpressionParseError extends Error { constructor(reason){ super(reason); } }\n\nconst REGX = Object.freeze({\n OPERAND_NUMBERS: '-*\\\\d+(\\\\.\\\\d*)*',\n OPERAND_STRING: \"(?:'|\\\")[^'\\\"]*(?:'|\\\")\",\n OPERAND_LOGICAL_TRUE: 'true',\n OPERAND_LOGICAL_FALSE: 'false',\n OPERAND_POS_INFINITY: 'Infinity',\n OPERAND_NEG_INFINITY: '-Infinity',\n MATH_OPERATORS: '\\\\+|\\\\-|\\\\*|\\\\/|\\\\^|\\\\(|\\\\)',\n TERNARY_OPERATORS: '\\\\?|\\\\:',\n LOGICAL_OPERATORS: '\\\\|\\\\||&&|==|!=|!',\n EQUALITY_OPERATORS: '>=|<=|>|<'\n});\n\n\n// --------------------------------------------------------------------------------------------------------------------\n//\n// Stack implementation\n//\n// --------------------------------------------------------------------------------------------------------------------\nclass Stack {\n // ----------------------------------------------------------------------------------------------------------------\n //\n // ----------------------------------------------------------------------------------------------------------------\n constructor() { this._values = []; }\n\n // ----------------------------------------------------------------------------------------------------------------\n //\n // ----------------------------------------------------------------------------------------------------------------\n get size() { return this._values.length; }\n empty() { return this.size === 0; }\n peek() { return this.empty() ? undefined : this._values[this._values.length - 1]; }\n\n // ----------------------------------------------------------------------------------------------------------------\n //\n // ----------------------------------------------------------------------------------------------------------------\n push(value) { this._values.push(value); }\n pop() { return this.empty() ? undefined : this._values.pop(); }\n popN(n) {\n const result = [];\n if (this.size >= n) {\n for (let i = 0; i < n; i++)\n result.unshift(this.pop());\n }\n return result;\n }\n}\n//---------------------------------------------------------------------------------------------------------------------\n//\n// Working with operators\n//\n//---------------------------------------------------------------------------------------------------------------------\n\n//---------------------------------------------------------------------------------------------------------------------\n// To number\n//---------------------------------------------------------------------------------------------------------------------\nfunction toNum(s) {\n const result = parseFloat(s);\n if(!isNaN(result))\n return result;\n else\n throw new ExpressionParseError(`Failed to convert ${s} to number`);\n}\n//---------------------------------------------------------------------------------------------------------------------\n// To boolean\n//---------------------------------------------------------------------------------------------------------------------\nfunction toBool(s) {\n if((s === 'true') || (s === true))\n return true;\n else if((s === 'false') || (s === false))\n return false;\n else\n throw new ExpressionParseError(`Failed to convert ${s} to boolean`);\n}\n//---------------------------------------------------------------------------------------------------------------------\n// To number\n//---------------------------------------------------------------------------------------------------------------------\nfunction extractStrOperand(s) {\n return s.slice(1, s.length - 1)\n\n}\nfunction toStr(s) {\n function isQuote(char) { return (char === \"'\") || (char === '\"'); }\n let result = s.trim();\n if(result.length > 1) {\n const firstChar = result[0];\n const lastChar = result[result.length - 1];\n if( !isQuote(firstChar) && !isQuote(lastChar))\n throw new ExpressionParseError(`String expression ${s} must be enclosed in single quotes`);\n }\n else\n throw new ExpressionParseError(`Failed to convert ${s} to number`);\n\n return result;\n}\n//---------------------------------------------------------------------------------------------------------------------\n// To supported operand\n//---------------------------------------------------------------------------------------------------------------------\nfunction toOperand(s) {\n try {\n return toNum(s);\n }\n catch(er) {\n try {\n return toBool(s);\n }\n catch(er) {\n return toStr(s);\n }\n }\n}\n\n//---------------------------------------------------------------------------------------------------------------------\n// Converts both a and b to expected types\n//---------------------------------------------------------------------------------------------------------------------\nfunction toTypePair(a, b) {\n const va = toOperand(a);\n const vb = toOperand(b)\n if(typeof va === typeof vb)\n return [va, vb];\n else\n throw new ExpressionParseError(`Cannot operate on parameters with different types ${a} <??> ${b}`);\n}\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n// Information on all supported operators\n//\n//---------------------------------------------------------------------------------------------------------------------\nconst operatorData = {\n '?': { precedence: 0, associativity: 'R', NArgs: 0, f:function() { throw new ExpressionParseError('Ternary if mismatched'); } },\n ':': { precedence: 0, associativity: 'R', NArgs: 2, f:function() { throw new ExpressionParseError('Ternary else if mismatched'); } },\n '?:': { precedence: 1, associativity: 'R', NArgs: 3, f:function(a, b, c) { return toBool(a) ? toOperand(b) : toOperand(c); } },\n '^': { precedence: 2, associativity: 'R', NArgs: 2, f:function(a, b) { return toNum(a) ** toNum(b); } },\n '+': { precedence: 4, associativity: 'L', NArgs: 2, f:function(a, b) { return toNum(a) + toNum(b); } },\n '-': { precedence: 4, associativity: 'L', NArgs: 2, f:function(a, b) { return toNum(a) - toNum(b); } },\n '*': { precedence: 3, associativity: 'L', NArgs: 2, f:function(a, b) { return toNum(a) * toNum(b); } },\n '/': { precedence: 3, associativity: 'L', NArgs: 2, f:function(a, b) { return toNum(a) / toNum(b); } },\n\n '==': { precedence: 7, associativity: 'L', NArgs: 2, f:function(a, b) {\n const [va, vb] = toTypePair(a, b);\n return va === vb;\n } },\n '!=': { precedence: 7, associativity: 'L', NArgs: 2, f:function(a, b) {\n const [va, vb] = toTypePair(a, b);\n return va !== vb;\n } },\n '!': { precedence: 2, associativity: 'R', NArgs: 1, f:function(a) {\n return !toBool(a);\n } },\n '&&': { precedence: 11, associativity: 'L', NArgs: 2, f:function(a, b) { return toBool(a) && toBool(b); } },\n '||': { precedence: 12, associativity: 'L', NArgs: 2, f:function(a, b) { return toBool(a) || toBool(b); } },\n '>=': { precedence: 6, associativity: 'L', NArgs: 2, f:function(a, b) { return toNum(a) >= toNum(b); } },\n '<=': { precedence: 6, associativity: 'L', NArgs: 2, f:function(a, b) { return toNum(a) <= toNum(b); } },\n '>': { precedence: 6, associativity: 'L', NArgs: 2, f:function(a, b) { return toNum(a) > toNum(b); } },\n '<': { precedence: 6, associativity: 'L', NArgs: 2, f:function(a, b) { return toNum(a) < toNum(b); } },\n};\nconst operatorKeys = Object.keys(operatorData);\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n// Implementation of Shunting Yard\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass InfixNotationParser {\n // ----------------------------------------------------------------------------------------------------------------\n //\n // ----------------------------------------------------------------------------------------------------------------\n constructor(expression) {\n this._expression = expression;\n this._isOperand =\n new RegExp(`^${REGX.OPERAND_STRING}$|^${REGX.OPERAND_NUMBERS}$|^${REGX.OPERAND_LOGICAL_TRUE}$|^${REGX.OPERAND_LOGICAL_FALSE}$|^${REGX.OPERAND_POS_INFINITY}$|^${REGX.OPERAND_NEG_INFINITY}$`);\n\n this._regXParseExpression =\n new RegExp(`${REGX.OPERAND_STRING}|${REGX.OPERAND_LOGICAL_TRUE}|${REGX.OPERAND_LOGICAL_FALSE}|${REGX.OPERAND_POS_INFINITY}|${REGX.OPERAND_NEG_INFINITY}|${REGX.OPERAND_NUMBERS}|${REGX.MATH_OPERATORS}|${REGX.LOGICAL_OPERATORS}|${REGX.EQUALITY_OPERATORS}|${REGX.TERNARY_OPERATORS}`,'g');\n }\n // ----------------------------------------------------------------------------------------------------------------\n //\n // ----------------------------------------------------------------------------------------------------------------\n isOperator(token) { return operatorKeys.includes(token); }\n getOperator(token) { return operatorData[token]; }\n isAssociative(token, type) { return this.getOperator(token).associativity === type; }\n comparePrecedence(op1, op2) { return this.getOperator(op1).precedence - this.getOperator(op2).precedence; }\n hasHigherPrecedence(op1, op2) { return this.comparePrecedence(op1, op2) < 0; }\n hasLowerPrecedence(op1, op2) { return this.comparePrecedence(op1, op2) > 0; }\n hasSamePrecedence(op1, op2) { return this.comparePrecedence(op1, op2) === 0; }\n tokenize() { return this._expression.match(this._regXParseExpression); }\n isOperand(token) { return this._isOperand.test(token); }\n\n // ----------------------------------------------------------------------------------------------------------------\n //\n // ----------------------------------------------------------------------------------------------------------------\n toPostfix() {\n const outputQueue = [];\n const tokens = this.tokenize();\n\n if(Array.isArray(tokens)) {\n const operatorStack = new Stack();\n tokens.forEach(token => {\n // 1. If the incoming symbols is an operand - push to outputQueue\n if (this.isOperand(token)) {\n outputQueue.push(token);\n }\n // 2. If the incoming symbol is a left parenthesis, push it on the stack.\n else if (token === '(') {\n operatorStack.push(token);\n }\n // 3. If the incoming symbol is a right parenthesis: discard the right parenthesis, pop and print the stack\n // symbols until you see a left parenthesis.\n else if (token === ')') {\n while (operatorStack.size > 0 && operatorStack.peek() !== '(') {\n outputQueue.push(operatorStack.pop());\n }\n // If there is a left parenthesis - discard it.\n if ((operatorStack.size > 0) && (operatorStack.peek() === '('))\n operatorStack.pop();\n else\n throw new ExpressionParseError(`Missing open parenthesis in expression`);\n }\n // If the incoming symbol is an operator...\n else if (this.isOperator(token)) {\n let topOperatorStack = operatorStack.peek();\n\n // 4. If the stack is empty or contains a left parenthesis on top or the incoming symbol is a ternary\n // begin if\n if (operatorStack.empty() || (topOperatorStack === \"(\") || token === \"?\") {\n operatorStack.push(token);\n }\n // 5. Handle possible ternary operator\n else if(token === ':') {\n // Both ternary if (\"?\") and else (\":\") will never appear in the output. The goal is to reverse\n // these symbols to RPN format: 'true a b ?:' or 'false a b ?:'. The rule therefore is, when\n // finding the else branch (\":\") of the ternary operator, we pop inclusively to the start if\n // symbol (\"?\"), this goes to the output, then we follow this with the ending RPN ternary\n // operator \"?:\"\n // see also https://stackoverflow.com/questions/35609168/extending-the-shunting-yard-algorithm-to-support-the-conditional-ternary-operato\n // which goes over this in an example:\n // * \"?\" ternary-open-if\n // * \":\" ternary-else\n // * \"?:\" ternary-closed-if (note this is a dummy symbol, could be anything). If this symbol is needed in your expression you should\n // update this parser and set it to a different unused symbol.\n let poppedSymbol = null;\n while(!operatorStack.empty()) {\n poppedSymbol = operatorStack.pop();\n if(poppedSymbol === '?') {\n operatorStack.push('?:');\n break;\n }\n else\n outputQueue.push(poppedSymbol);\n }\n if(poppedSymbol !== \"?\")\n throw new ExpressionParseError('Missing ternary ? symbol');\n }\n // 6. If the incoming operator has either higher precedence than the operator on the top of the stack,\n // or has the same precedence as the operator on the top of the stack and is right associative\n else if ((this.hasHigherPrecedence(token, topOperatorStack)) ||\n (this.hasSamePrecedence(token, topOperatorStack) && this.isAssociative(token, 'R'))) {\n operatorStack.push(token);\n } else {\n // 7. If the incoming operator has either lower precedence than the operator on the top of the stack,\n // or has the same precedence as the operator on the top of the stack and is left associative\n // -- continue to pop the stack until this is not true. Then, push the incoming operator.\n while (topOperatorStack && (topOperatorStack !== '(') && (this.hasLowerPrecedence(token, topOperatorStack) ||\n (this.hasSamePrecedence(token, topOperatorStack) && this.isAssociative(token, 'L')))) {\n outputQueue.push(operatorStack.pop());\n topOperatorStack = operatorStack.peek();\n }\n operatorStack.push(token);\n }\n }\n });\n\n // Last push any remaining symbols to the outputQueue\n while (operatorStack.size > 0) {\n const operator = operatorStack.pop()\n if (operator !== '(')\n outputQueue.push(operator);\n else\n throw new ExpressionParseError(`Missing closing parenthesis in expression`);\n }\n }\n else\n throw new ExpressionParseError('Parsing expression resulted in an empty parse');\n\n return outputQueue;\n }\n}\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n// Parse and evaluate passed in expression\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass ExpressionParser extends InfixNotationParser {\n // ----------------------------------------------------------------------------------------------------------------\n //\n // ----------------------------------------------------------------------------------------------------------------\n constructor(expression) {\n super(expression);\n }\n // ----------------------------------------------------------------------------------------------------------------\n //\n // ----------------------------------------------------------------------------------------------------------------\n evaluate() {\n const postFix = this.toPostfix();\n const stack = new Stack();\n for(let i = 0; i < postFix.length; i++) {\n const token = postFix[i];\n if(this.isOperator(token)) {\n const operator = this.getOperator(token);\n if(stack.size >= operator.NArgs) {\n const args = stack.popN(operator.NArgs);\n const result = operator.f(...args);\n stack.push(result);\n }\n else\n throw new ExpressionParseError(`Not enough args for operator ${token}`)\n }\n else\n stack.push(token);\n }\n if(stack.size === 1) {\n let result = toOperand(stack.pop());\n\n // If the type is a string remove single quotes\n if(typeof result === 'string')\n result = extractStrOperand(result);\n return result;\n\n }\n else\n throw new ExpressionParseError(`Resulting stack appears incorrect with size ${stack.size}`);\n }\n}\nexport { ExpressionParser, ExpressionParseError};\n","import {ExpressionParser} from \"./expression_parser.js\"\n//---------------------------------------------------------------------------------------------------------------------\n//---------------------------------------------------------------------------------------------------------------------\nconst REGX = Object.freeze({\n FUNCTION_TAG: '^\\\\s*->\\\\s*(.*)',\n CMD_KEY_QUEUE_DEL_CHILD_OBJ_IF_EMPTY: '^(<-\\\\s*false\\\\s*)|(<--\\\\s*false\\\\s*)$',\n CMD_KEY_COPY_INTO_OBJ: '^(<-\\\\s*true\\\\s*)|(<-\\\\s*)$',\n CMD_KEY_COPY_INTO_PARENT_OBJ: '^(<--\\\\s*true\\\\s*)|(<--\\\\s*)$',\n CMD_KEY_IF_BLOCK_START: '^(<--|<-)\\\\s*(IF\\\\().*',\n CMD_KEY_ELSE_BLOCK: '^(<--|<-)\\\\s*(ELSE)$'\n});\n\n//---------------------------------------------------------------------------------------------------------------------\n// Exceptions\n//---------------------------------------------------------------------------------------------------------------------\nclass InterpolationValueNotFoundError extends Error { constructor(){ super(\"Interpolation value not Found\"); } }\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n// Base class for a generic parser like an Abstract Syntax Tree or any simple parser requiring simple character scanning\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass BaseAST {\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n static isWhitespace(char) { return /\\s/.test(char); }\n static isOpen(char) { return char === '('; }\n static isClose(char) { return char === ')'; }\n static isCurlyOpen(char) { return char === '{'; }\n static isCurlyClose(char) { return char === '}'; }\n static isArgSeparate(char) { return char === ','; }\n static isSquareOpen(char) { return char === '['; }\n static isSquareClose(char) { return char === ']'; }\n static isGroupTokenBegin(char) { return (char === \"'\") || (char === '\"') || (char === '[') || (char === '{'); }\n static isMatchingGroupTokenEnd(beginChar, char) {\n if(beginChar === \"'\")\n return char === \"'\";\n else if(beginChar === '\"')\n return char === '\"';\n else if(beginChar === '[')\n return char === ']';\n else if(beginChar === '{')\n return char === '}';\n }\n\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n constructor(expression) {\n this._expression = expression;\n this._index = 0;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n notifyParseStart() { this._index = 0; }\n notifyParseComplete(parseResult) {}\n\n //-----------------------------------------------------------------------------------------------------------------\n // Basic character functions - non consuming\n //-----------------------------------------------------------------------------------------------------------------\n getI() { return this._index; }\n getExpr() { return this._expression; }\n cAtI(n = 0) { return this.getExpr()[this._index + n]; }\n cAtIIsWhite() { return BaseAST.isWhitespace(this.cAtI()); }\n skip(n=1) { return this._index += n; }\n hasChars(nChars = 1) { return (this.getExpr().length - this._index) >= nChars; }\n\n //-----------------------------------------------------------------------------------------------------------------\n // Useful character functions for specific cases\n //-----------------------------------------------------------------------------------------------------------------\n cAtIisCurlyOpen() { return BaseAST.isCurlyOpen(this.cAtI()); }\n cAtIisCurlyClose() { return BaseAST.isCurlyClose(this.cAtI()); }\n cAtIOIsOpen() { return BaseAST.isOpen(this.cAtI()); }\n cAtIOIsClose() { return BaseAST.isClose(this.cAtI()); }\n cAtIIsArgSeporator() { return BaseAST.isArgSeparate(this.cAtI())}\n cAtIIsGroupTokenEnd(beginChar) { return BaseAST.isMatchingGroupTokenEnd(beginChar, this.cAtI()); }\n cAtIIsTag(tag) {\n let hasTag = false;\n const tagLen = tag.length;\n if(this.hasChars(tagLen)) {\n let nFoundSymbols = 0;\n for(let i = 0; i < tag.length; i++) {\n if(this.cAtI(i) === tag[i])\n nFoundSymbols++;\n }\n hasTag = (nFoundSymbols === tagLen)\n }\n return hasTag;\n\n }\n //-----------------------------------------------------------------------------------------------------------------\n // Consuming\n //-----------------------------------------------------------------------------------------------------------------\n skipWhitespace() {\n while (this.hasChars() && this.cAtIIsWhite())\n this.skip();\n }\n}\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n//\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass TagParser extends BaseAST {\n\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n constructor(expression, options = {}) {\n super(expression);\n\n this._options = options;\n this._startTag = \"{{\";\n this._endTag = \"}}\"\n this._ignoreEnclosed = { Opens: [\"{\"], Closes: [\"}\"] }\n this._curlyBracketStack = [];\n this._replacementEdits = []\n this._options.TrackCurlyBrackets = this._options.TrackCurlyBrackets !== undefined ?\n this._options.TrackCurlyBrackets : false;\n }\n getTrackCurlyBrackets() { return this._options.TrackCurlyBrackets; }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n createTemplateKey(key) { return `${this._startTag}${key}${this._endTag}`; }\n cAtIIsBeginTag() { return this.cAtIIsTag(this._startTag); }\n cAtIIsEndTag() {\n let isTag = false;\n if(this._curlyBracketStack.length === 0)\n isTag = this.cAtIIsTag(this._endTag);\n return isTag;\n\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n cAtIisOpen() {\n let closing = undefined;\n const indexOfOpen = this._ignoreEnclosed.Opens.indexOf(this.cAtI());\n if(indexOfOpen !== -1)\n closing = this._ignoreEnclosed.Closes[indexOfOpen];\n\n\n return closing;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n cAtIisClosing() {\n let matchedClosing = false;\n if(this._curlyBracketStack.length > 0) {\n const lastClosing = this._curlyBracketStack[this._curlyBracketStack.length - 1];\n if(this.cAtI() === lastClosing)\n matchedClosing = true;\n }\n return matchedClosing;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n trackEnclosedChars() {\n // In the cases where we have not identified a start or end token, track curly brackets.\n if(this.getTrackCurlyBrackets()) {\n const closingChar = this.cAtIisOpen();\n if (closingChar !== undefined)\n this._curlyBracketStack.push(closingChar);\n else if (this.cAtIisClosing()) {\n this._curlyBracketStack.pop();\n }\n }\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n parseToken() {\n // {{A {{B}} }}\n let token = undefined;\n let start = -1; let end = -1;\n let iStartToken = -1; let iEndToken = -1;\n while (this.hasChars()) {\n if (this.cAtIIsBeginTag()) {\n iStartToken = this.getI();\n this.skip(this._startTag.length);\n start = this.getI();\n }\n else if ((start !== -1) && this.cAtIIsEndTag()) {\n end = this.getI();\n iEndToken = end + this._endTag.length;\n\n // slice out our token\n token = this.getExpr().slice(start, end);\n\n this.skip(this._endTag.length);\n break;\n }\n else {\n this.trackEnclosedChars();\n this.skip();\n }\n }\n return token !== undefined ? {\n Match: this.createTemplateKey(token),\n Key: token,\n IStartToken: iStartToken,\n IEndToken: iEndToken\n } : undefined;\n }\n\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n applyReplacementEdits() {\n return SinglePassTagReplacer.customStringReplacer(this.getExpr(), this._replacementEdits);\n }\n}\n\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n//\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass SimpleTagParser extends TagParser {\n\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n constructor(expression, options = {}) {\n super(expression, options);\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n has() {\n let found = false;\n this.notifyParseStart();\n let matchResultObj = this.parseToken();\n this.notifyParseComplete(matchResultObj);\n return matchResultObj !== undefined;\n }\n}\n\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n//\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass SinglePassTagReplacer extends TagParser {\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n static customStringReplacer(expression, replacementEdits) {\n // Note: edits are already sorted from left to right\n\n // Iterate over the replacementEdits\n let result = '';\n let lastIndex = 0;\n\n for (let i = 0; i < replacementEdits.length; i++) {\n const { ReplaceWith, IStartToken, IEndToken } = replacementEdits[i];\n\n // Add the existing part of the string before the start token\n result += expression.slice(lastIndex, IStartToken);\n\n // Add the new string (key)\n result += ReplaceWith;\n\n // Update the lastIndex to be after the end token\n lastIndex = IEndToken;\n }\n\n // Add any remaining part of the original string after the last change\n result += expression.slice(lastIndex);\n\n return result;\n }\n\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n constructor(expression, cb, options = {}) {\n super(expression, options);\n this._cb = cb;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n notifyParseResult(matchResultObj) {\n const match = matchResultObj.Match;\n const key = matchResultObj.Key;\n const offset = matchResultObj.IStartToken;\n\n matchResultObj.ReplaceWith = this._cb(this, match, key, offset, this.getExpr());\n this._replacementEdits.push(matchResultObj);\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n applyReplacementEdits() {\n return SinglePassTagReplacer.customStringReplacer(this.getExpr(), this._replacementEdits);\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n replace() {\n this.notifyParseStart();\n let matchResultObj = this.parseToken();\n while(matchResultObj !== undefined) {\n this.notifyParseResult(matchResultObj);\n matchResultObj = this.parseToken();\n }\n if(this._curlyBracketStack.length > 0)\n throw new Error(`Match Error - unbalanced symbols missing: ${this._curlyBracketStack.join(',')}`);\n this.notifyParseComplete(matchResultObj);\n return this.applyReplacementEdits();\n }\n}\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass PromisesHandler {\n static isPromise(p) {\n let result = false;\n if ((p !== null) && (typeof p === 'object') && typeof p.then === 'function')\n result = true;\n return result;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n constructor() {\n this._promises = [];\n this._promiseKeys = [];\n this._matchedOn = []\n\n this._nextKeyId = 0;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n hasPromises() { return this._promises.length > 0; }\n allSettled() { return Promise.allSettled(this._promises); }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n async processPromises() {\n let replaceKeys = undefined;\n if (this.hasPromises()) {\n replaceKeys = {NResolved: 0, NRejected: 0};\n const results = await this.allSettled();\n for (let i = 0; i < results.length; i++) {\n const promiseKey = this._promiseKeys[i];\n const pResult = results[i];\n if(pResult.status === 'fulfilled') {\n replaceKeys[promiseKey] = pResult.value;\n replaceKeys.NResolved++;\n }\n else if(pResult.status === 'rejected') {\n replaceKeys[promiseKey] = this._matchedOn[i];\n replaceKeys.NRejected++;\n }\n }\n }\n return replaceKeys;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n getNextPromiseKey() {\n const key = `${this._nextKeyId}__@uniquePKey@__${this._nextKeyId}`;\n this._nextKeyId++;\n return key;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n add(matchedOn, pCandidate) {\n let addedKey = undefined;\n if(PromisesHandler.isPromise(pCandidate)) {\n this._matchedOn.push(matchedOn);\n this._promises.push(pCandidate);\n addedKey = this.getNextPromiseKey();\n this._promiseKeys.push(addedKey);\n }\n return addedKey;\n }\n}\nconst defaultOptions = Object.freeze({\n CovertValueToType: true,\n});\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n// Context for key values allows lookup by simple key. This is the Base Interface for getting a value based on a key\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass KeyValueContextI {\n constructor(keyValues) {\n this._keyValues = keyValues;\n }\n getKeyValues() { return this._keyValues; }\n get(key) { return this._keyValues[key]; }\n}\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n// Context for key values allows lookup by a query string.\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass QueryObjKeyValueContextI extends KeyValueContextI {\n constructor(keyValues, useSeparator = \".\") {\n super(keyValues);\n this._useSeparator = useSeparator;\n }\n get(q) {\n function arr_deref(o, ref, i) {\n const key = ref.slice(0, i ? -1 : ref.length);\n return !ref ? o : (o[key]);\n }\n function dot_deref(o, ref) {\n return !ref ? o : ref.split('[').reduce(arr_deref, o);\n }\n try {\n return q.split(this._useSeparator).reduce(dot_deref, this.getKeyValues());\n }\n catch(err) { return undefined; }\n }\n}\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n//\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass StringInterpolator {\n\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n constructor(templateStr, keyValuesI, options = {}) {\n this._templateStr = templateStr;\n\n this._keyValuesI = (keyValuesI instanceof KeyValueContextI) ?\n keyValuesI : new KeyValueContextI(keyValuesI);\n\n this._options = options;\n\n this._options.CovertValueToType = this._options.CovertValueToType === undefined ?\n defaultOptions.CovertValueToType : this._options.CovertValueToType;\n\n this._options.ReplaceNotFoundHandler =\n this._options.ReplaceNotFoundHandler !== undefined ? this._options.ReplaceNotFoundHandler : (templateVar, key) => { return templateVar };\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n getOptionConvertType() { return this._options.CovertValueToType; }\n getOptionReplaceNotFoundHandler() { return this._options.ReplaceNotFoundHandler; }\n\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n getValueInMap(key) { return this._keyValuesI.get(key); }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n async doReplaces(templateStr, options) {\n let simpleReplace = undefined;\n const promisesHandler = new PromisesHandler();\n\n // Replace string with found key\n let nReplaces = 0;\n const replace = (new SinglePassTagReplacer(templateStr,\n (sender, match, key, offset, string) => {\n\n let replaceCandidate = options.getValueInMap(key.trim());\n if((replaceCandidate === undefined) && options.canInvokeNotFoundHandler())\n replaceCandidate = this.getOptionReplaceNotFoundHandler()(match, key);\n\n // If there are promises to resolve, replace the token with our promise key\n const promiseKey = promisesHandler.add(match, replaceCandidate);\n if(promiseKey !== undefined)\n replaceCandidate = sender.createTemplateKey(promiseKey);\n else if(this.getOptionConvertType() && (match === string))\n simpleReplace = replaceCandidate;\n else {\n if((replaceCandidate !== null) && (typeof replaceCandidate === 'object'))\n replaceCandidate = JSON.stringify(replaceCandidate)\n }\n\n return replaceCandidate;\n\n }, this._options)).replace();\n let resultingS = undefined;\n const promiseReplaceKeys = await promisesHandler.processPromises();\n if(promiseReplaceKeys !== undefined) {\n resultingS = await this.doReplaces(replace, {\n getValueInMap: (key) => promiseReplaceKeys[key],\n canInvokeNotFoundHandler: () => false\n });\n }\n\n resultingS = resultingS || (simpleReplace !== undefined ? simpleReplace : replace);\n return resultingS;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n async sInterpolate() {\n let resultingS = this._templateStr;\n\n if (typeof resultingS === 'string') {\n resultingS = await this.doReplaces(resultingS, {\n getValueInMap: (key) => this.getValueInMap(key),\n canInvokeNotFoundHandler: () => true\n });\n }\n\n return resultingS;\n }\n}\n\n//---------------------------------------------------------------------------------------------------------------------\n//---------------------------------------------------------------------------------------------------------------------\nconst ReplaceObjectAction = Object.freeze({\n ACTION_NONE: Symbol(\"ACTION_NONE\"),\n ACTION_DELETE: Symbol(\"ACTION_DELETE\"),\n ACTION_THROW: Symbol(\"ACTION_THROW\"),\n isValidAction: function (action) {\n let isValid = false;\n if(action)\n isValid = (action === ReplaceObjectAction.ACTION_NONE) ||\n (action === ReplaceObjectAction.ACTION_DELETE) ||\n (action === ReplaceObjectAction.ACTION_THROW);\n return isValid;\n }\n});\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n//\n//---------------------------------------------------------------------------------------------------------------------\nclass ActionI {\n constructor() {\n this._action = ReplaceObjectAction.ACTION_NONE;\n }\n\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n getAction() { return this._action; }\n setAction(newActionValue) {\n let actionSet = false;\n if(ReplaceObjectAction.isValidAction(newActionValue)) {\n this._action = newActionValue;\n\n actionSet = true;\n }\n\n return actionSet;\n }\n}\n\n//---------------------------------------------------------------------------------------------------------------------\n//\n//\n//---------------------------------------------------------------------------------------------------------------------\nconst KeyCommands = Object.freeze({\n KeyCmdNone: 0,\n KeyCmdCopyIntoObject: 1,\n KeyCmdCopyIntoParentObject: 2,\n KeyCmdDelKey: 3,\n KeyCmdQueueDelChildObjectIfEmpty: 4\n});\n\nclass ObjectInterpolatorBase {\n static isKeyCmd(key, cmd) { return key.search(new RegExp(cmd)) !== -1; }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n static containsTemplateVar(templateString) {\n return (templateString && typeof templateString === 'string') ?\n ((new SimpleTagParser(templateString)).has()) : false;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n static dupWithoutVars(childObj) {\n const obj = {...childObj};\n for (const [key, value] of Object.entries(childObj)) {\n // we should remove any variable which has a template variable\n if (ObjectInterpolatorBase.containsTemplateVar(value))\n delete obj[key];\n }\n return obj;\n }\n static mergeInto(target, source) {\n if(Array.isArray(target) && Array.isArray((source))) {\n target.splice(0, target.length, ...source);\n }\n else {\n Object.assign(target, source);\n }\n }\n //-----------------------------------------------------------------------------------------------------------------\n // {A:{a:1,b:2, c:{d:4}}\n //-----------------------------------------------------------------------------------------------------------------\n static *iterateObjStrings(obj, keys = [], objs= []) {\n let isRoot = false;\n if(obj) {\n\n // push the root object\n if(objs.length === 0) { objs.push(obj); isRoot = true; }\n\n\n const useKeys = Array.isArray(obj.__ProcessKeys__) ? obj.__ProcessKeys__ : Object.keys(obj);\n for(let i = 0; i < useKeys.length; i++) {\n const key = useKeys[i]; const value = obj[key];\n\n if ((typeof value === 'object') && (value !== null)) {\n if(!Array.isArray(value))\n yield [obj, key, value, keys, objs];\n keys.push(key); objs.push(value);\n yield* ObjectInterpolatorBase.iterateObjStrings(value, keys, objs );\n keys.pop(); objs.pop();\n }\n else if (typeof value === 'string') {\n yield [obj, key, value.trim(), keys, objs];\n const regex = /^__DEBUG__\\d*$/;\n const isDebugPrint = !!key.match(regex);\n if (isDebugPrint) {\n console.log(obj[key]);\n delete obj[key];\n }\n }\n }\n\n // pop the root object\n if(isRoot) { objs.pop(); }\n }\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n static createPathDotNotation(keys, key) { return keys.length > 0 ? keys.join(\"¤\") + \"¤\" + key : key; }\n static createObjectPath(pathDotNotation) { return pathDotNotation.split(\"¤\"); }\n static dup(obj) { return JSON.parse(JSON.stringify(obj)); }\n\n //-----------------------------------------------------------------------------------------------------------------\n //\n //-----------------------------------------------------------------------------------------------------------------\n constructor(obj, keyValues, options = {}) {\n this._options = options;\n this._options.CopyObj = this._options.CopyObj !== undefined ? this._options.CopyObj : false\n\n this._obj = this.getCopyObj() ? ObjectInterpolatorBase.dup(obj) : obj;\n this._keyValues = keyValues;\n\n this._options.ActionOnNotFound = ReplaceObjectAction.isValidAction(this._options.ActionOnNotFound) ?\n this._options.ActionOnNotFound : ReplaceObjectAction.ACTION_NONE;\n\n this._options.KeyValueContextI = this._options.KeyValueContextI !== undefined ?\n this._options.KeyValueContextI : KeyValueContextI;\n\n this._nPass = 3;\n }\n //-----------------------------------------------------------------------------------------------------------------\n //\n //------------------------------------------------