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