const fs = require('fs-extra'); const path = require('path'); const { randomUUID } = require('crypto'); const { v4: uuidv4 } = require('uuid'); const NOOP = function() {}; const OPERATORS = { '{' : 'L_CURLY', '}' : 'R_CURLY', '(' : 'L_PAREN', ')' : 'R_PAREN', '[' : 'L_BRACKET', ']' : 'R_BRACKET', '.' : 'PERIOD', ',' : 'COMMA', ':' : 'COLON', '?' : 'QUESTION', // Arithmetic operators '+' : 'ADDICTIVE', '-' : 'ADDICTIVE', '*' : 'MULTIPLICATIVE', '/' : 'MULTIPLICATIVE', '%' : 'MULTIPLICATIVE', // Comparison operators '===': 'EQUALITY', '!==': 'EQUALITY', '==' : 'EQUALITY', '!=' : 'EQUALITY', '<' : 'RELATIONAL', '>' : 'RELATIONAL', '<=' : 'RELATIONAL', '>=' : 'RELATIONAL', 'in' : 'RELATIONAL', // Logical operators '&&' : 'LOGICAL_AND', '||' : 'LOGICAL_OR', '!' : 'LOGICAL_NOT', // Bitwise operators '&' : 'BITWISE_AND', '|' : 'BITWISE_OR', '^' : 'BITWISE_XOR', '~' : 'BITWISE_NOT', '<<' : 'BITWISE_SHIFT', '>>' : 'BITWISE_SHIFT', '>>>': 'BITWISE_SHIFT' }; const EXPRESSIONS = { 'in' : function(a, b) { return a() in b(); }, '?' : function(a, b, c) { return a() ? b() : c(); }, '+' : function(a, b) { a = a(); b = b(); return a == null ? b : b == null ? a : a + b; }, '-' : function(a, b) { return a() - b(); }, '*' : function(a, b) { return a() * b(); }, '/' : function(a, b) { return a() / b(); }, '%' : function(a, b) { return a() % b(); }, '===': function(a, b) { return a() === b(); }, '!==': function(a, b) { return a() !== b(); }, '==' : function(a, b) { return a() == b(); }, '!=' : function(a, b) { return a() != b(); }, '<' : function(a, b) { return a() < b(); }, '>' : function(a, b) { return a() > b(); }, '<=' : function(a, b) { return a() <= b(); }, '>=' : function(a, b) { return a() >= b(); }, '&&' : function(a, b) { return a() && b(); }, '||' : function(a, b) { return a() || b(); }, '&' : function(a, b) { return a() & b(); }, '|' : function(a, b) { return a() | b(); }, '^' : function(a, b) { return a() ^ b(); }, '<<' : function(a, b) { return a() << b(); }, '>>' : function(a, b) { return a() >> b(); }, '>>>': function(a, b) { return a() >>> b(); }, '~' : function(a) { return ~a(); }, '!' : function(a) { return !a(); } }; const ESCAPE = { 'n': '\n', 'f': '\f', 'r': '\r', 't': '\t', 'v': '\v', "'": "'", '"': '"', '`': '`' }; const formatters = require('../formatters'); // User formatters if (fs.existsSync('extensions/server_connect/formatters')) { const files = fs.readdirSync('extensions/server_connect/formatters'); for (let file of files) { if (path.extname(file) == '.js') { Object.assign(formatters, require(`../../extensions/server_connect/formatters/${file}`)); } } } function lexer(expr) { let tokens = [], token, name, start, index = 0, op = true, ch, ch2, ch3; while (index < expr.length) { start = index; ch = read(); if (isQuote(ch) && op) { name = 'STRING'; token = readString(ch); op = false; } else if ((isDigid(ch) || (is('.') && peek() && isDigid(peek()))) && op) { name = 'NUMBER'; token = readNumber(); op = false; } else if (isAlpha(ch) && op) { name = 'IDENT'; token = readIdent(); if (is('(')) { name = 'METHOD'; } op = false; } else if (is('/') && op && (token == '(' || token == ',' || token == '?' || token == ':') && testRegexp()) { name = 'REGEXP'; token = readRegexp(); op = false; } else if (isWhitespace(ch)) { index++; continue; } else if ((ch3 = read(3)) && OPERATORS[ch3]) { name = OPERATORS[ch3]; token = ch3; op = true; index += 3; } else if ((ch2 = read(2)) && OPERATORS[ch2]) { name = OPERATORS[ch2]; token = ch2; op = true; index += 2; } else if (OPERATORS[ch]) { name = OPERATORS[ch]; token = ch; op = true; index++; } else { throw new Error('Lexer Error: Unexpected token "' + ch + '" at column ' + index + ' in expression {{' + expr + '}}'); } tokens.push({ name: name, index: start, value: token }); } return tokens; function read(n) { if (!n) n = 1; return expr.substr(index, n); } function peek(n) { n = n || 1; return index + n < expr.length ? expr[index + n] : false; } function is(chars) { return chars.indexOf(ch) != -1; } function isQuote(ch) { return ch == '"' || ch == "'"; } function isDigid(ch) { return ch >= '0' && ch <= '9'; } function isAlpha(ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '$'; } function isAlphaNum(ch) { return isAlpha(ch) || isDigid(ch); } function isWhitespace(ch) { return ch == ' ' || ch == '\r' || ch == '\t' || ch == '\n' || ch == '\v' || ch == '\u00A0'; } function isExpOperator(ch) { return ch == '-' || ch == '+' || isDigid(ch); } function readString(quote) { let str = '', esc = false; index++; while (index < expr.length) { ch = read(); if (esc) { if (ch == 'u') { // unicode escape index++; let hex = read(4); if (!hex.match(/[\da-f]{4}/i)) { throw new Error('Lexer Error: Invalid unicode escape at column ' + index + ' in expression {{' + expr + '}}'); } str += String.fromCharCode(parseInt(hex, 16)); index += 3; } else { str += ESCAPE[ch] ? ESCAPE[ch] : ch; } esc = false; } else if (ch == '\\') { // escape character esc = true; } else if (ch == quote) { // end of string index ++; return str; } else { str += ch; } index++; } throw new Error('Lexer Error: Unterminated string at column ' + index + ' in expression {{' + expr + '}}'); } function readNumber() { let num = '', exp = false; while (index < expr.length) { ch = read(); if (isDigid(ch) || (is('.') && peek() && isDigid(peek()))) { num += ch; } else { let next = peek(); if (is('eE') && isExpOperator(next)) { num += 'e'; exp = true; } else if (isExpOperator(ch) && next && isDigid(next) && exp) { num += ch; exp = false; } else if (isExpOperator(ch) && (!next || !isDigid(next)) && exp) { throw new Error('Lexer Error: Invalid exponent at column ' + index + ' in expression {{' + expr + '}}'); } else { break; } } index++; } return +num; } function readIdent() { let ident = ''; while (index < expr.length) { ch = read(); if (isAlphaNum(ch)) { ident += ch; } else { break; } index++; } return ident; } function readRegexp() { let re = '', mod = '', esc = false; index ++; while (index < expr.length) { ch = read(); if (esc) { esc = false; } else if (ch == '\\') { esc = true; } else if (ch == '/') { index++; while ('ign'.indexOf(ch = read()) != -1) { mod += ch; index++; } return re + '%%%' + mod; } re += ch; index++; } throw new Error('Lexer Error: Unterminated regexp at column ' + index + ' in expression {{' + expr + '}}'); } function testRegexp() { var idx = index, ok = true; try { readRegexp(); } catch (e) { ok = false; } // reset our index and ch index = idx; ch = '/'; return ok; } } function parser(expr, scope) { let tokens = lexer(expr), context = undefined, RESERVED = { 'PI' : function() { return Math.PI; }, 'UUID' : function() { return randomUUID ? randomUUID() : uuidv4(); }, 'NOW' : function() { return date(); }, 'NOW_UTC' : function() { return utc_date(); }, 'TIMESTAMP': function() { return timestamp(); }, '$this' : function() { return scope.data; }, '$global' : function() { return globalScope.data; }, '$parent' : function() { return scope.parent && scope.parent.data; }, 'null' : function() { return null; }, 'true' : function() { return true; }, 'false' : function() { return false; }, '_' : function() { return { __dmxScope__: true } } }; return start()(); function pad(s, n) { return ('000' + s).substr(-n); } function date(dt) { dt = dt || new Date(); return pad(dt.getFullYear(), 4) + '-' + pad(dt.getMonth() + 1, 2) + '-' + pad(dt.getDate(), 2) + ' ' + pad(dt.getHours(), 2) + ':' + pad(dt.getMinutes(), 2) + ':' + pad(dt.getSeconds(), 2); } function utc_date(dt) { dt = dt || new Date(); return pad(dt.getUTCFullYear(), 4) + '-' + pad(dt.getUTCMonth() + 1, 2) + '-' + pad(dt.getUTCDate(), 2) + 'T' + pad(dt.getUTCHours(), 2) + ':' + pad(dt.getUTCMinutes(), 2) + ':' + pad(dt.getUTCSeconds(), 2) + 'Z'; } function timestamp(dt) { dt = dt || new Date(); return ~~(dt / 1000); } function read() { if (tokens.length === 0) { throw new Error('Parser Error: Unexpected end of expression {{' + expr + '}}'); } return tokens[0]; } function peek(e) { if (tokens.length > 0) { let token = tokens[0]; if (!e || token.name == e) { return token; } } return false; } function expect(e) { let token = peek(e); if (token) { tokens.shift(); return token; } return false; } function consume(e) { if (!expect(e)) { throw new Error('Parser Error: Unexpected token, expecting ' + e + ' in expression {{' + expr + '}}'); } } function fn(expr) { let args = [].slice.call(arguments, 1); return function() { if (EXPRESSIONS[expr]) { return EXPRESSIONS[expr].apply(context, args); } else { return expr; } } } function start() { return conditional(); } function conditional() { let left = logicalOr(), middle, token; if ((token = expect('QUESTION'))) { middle = conditional(); if ((token = expect('COLON'))) { return fn('?', left, middle, conditional()); } else { throw new Error('Parse Error: Expecting : in expression {{' + expr + '}}'); } } else { return left; } } function logicalOr() { let left = logicalAnd(), token; while (true) { if ((token = expect('LOGICAL_OR'))) { left = fn(token.value, left, logicalAnd()); } else { return left; } } } function logicalAnd() { let left = bitwiseOr(), token; if ((token = expect('LOGICAL_AND'))) { left = fn(token.value, left, logicalAnd()); } return left; } function bitwiseOr() { let left = bitwiseXor(), token; if ((token = expect('BITWISE_OR'))) { left = fn(token.value, left, bitwiseXor()); } return left; } function bitwiseXor() { let left = bitwiseAnd(), token; if ((token = expect('BITWISE_XOR'))) { left = fn(token.value, left, bitwiseAnd()); } return left; } function bitwiseAnd() { let left = equality(), token; if ((token = expect('BITWISE_AND'))) { left = fn(token.value, left, bitwiseAnd()); } return left; } function equality() { let left = relational(), token; if ((token = expect('EQUALITY'))) { left = fn(token.value, left, equality()); } return left; } function relational() { let left = bitwiseShift(), token; if ((token = expect('RELATIONAL'))) { left = fn(token.value, left, relational()); } return left; } function bitwiseShift() { let left = addictive(), token; if ((token = expect('BITWISE_SHIFT'))) { left = fn(token.value, left, addictive()); } return left; } function addictive() { let left = multiplicative(), token; while ((token = expect('ADDICTIVE'))) { left = fn(token.value, left, multiplicative()); } return left; } function multiplicative() { let left = unary(), token; while ((token = expect('MULTIPLICATIVE'))) { left = fn(token.value, left, unary()); } return left; } function unary() { let token; if ((token = expect('ADDICTIVE'))) { if (token.value == '+') { return primary(); } else { return fn(token.value, function() { return 0; }, unary()); } } else if ((token = expect('LOGICAL_NOT'))) { return fn(token.value, unary()); } return primary(); } function primary() { let value, next; if (expect('L_PAREN')) { value = start(); consume('R_PAREN'); } else if (expect('L_CURLY')) { let obj = {}; if (read().name != 'R_CURLY') { do { let key = expect().value; consume('COLON'); obj[key] = start()(); } while (expect('COMMA')); } value = fn(obj); consume('R_CURLY'); } else if (expect('L_BRACKET')) { let arr = []; if (read().name != 'R_BRACKET') { do { arr.push(start()()); } while (expect('COMMA')); } value = fn(arr); consume('R_BRACKET'); } else if (expect('PERIOD')) { value = peek() ? objectMember(fn(scope.data)) : fn(scope.data); } else { let token = expect(); if (token === false) { throw new Error('Parser Error: Not a primary expression {{' + expr + '}}'); } if (token.name == 'IDENT') { value = RESERVED.hasOwnProperty(token.value) ? RESERVED[token.value] : function() { return scope.get(token.value) }; } else if (token.name == 'METHOD') { if (!formatters[token.value]) { throw new Error('Parser Error: Formatter "' + token.value + '" does not exist, expression {{' + expression + '}}'); } value = fn(formatters[token.value]); } else if (token.name == 'REGEXP') { value = function() { let re = token.value.split('%%%'); return new RegExp(re[0], re[1]); }; } else { value = function() { return token.value }; } } while ((next = expect('L_PAREN') || expect('L_BRACKET') || expect('PERIOD'))) { if (next.value == '(') { value = functionCall(value, context); } else if (next.value == '[') { value = objectIndex(value); } else if (next.value == '.') { context = value; value = objectMember(value); } else { throw new Error('Parser Error: Parse error in expression {{' + expr + '}}'); } } context = undefined; return value; } function functionCall(func, ctx) { let argsFn = []; if (read().name != 'R_PAREN') { do { argsFn.push(start()); } while (expect('COMMA')); } consume('R_PAREN'); return function() { let args = []; if (ctx) args.push(ctx()); for (let argFn of argsFn) { args.push(argFn()); } let fnPtr = func() || NOOP; return fnPtr.apply(null, args); } } function objectIndex(obj) { let indexFn = start(); consume('R_BRACKET'); return function() { let o = obj(), i = indexFn(); if (typeof o != 'object') return undefined; if (o.__dmxScope__) { return scope.get(i); } return o[i]; } } function objectMember(obj) { let token = expect(); return function() { let o = obj(); if (token.name == 'METHOD') { if (!formatters[token.value]) { throw new Error('Parser Error: Formatter "' + token.value + '" does not exist, expression {{' + expr + '}}'); } return formatters[token.value]; } if (o && o.__dmxScope) { return scope.get(token.value); } return o && o[token.value]; } } } function parseValue(value, scope) { if (value == null) return value; value = value.valueOf(); if (typeof value == 'object') { for (let key in value) { value[key] = parseValue(value[key], scope); } } if (typeof value == 'string') { if (value.substr(0, 2) == '{{' && value.substr(-2) == '}}') { let expr = value.replace(/^\{\{|\}\}$/g, ''); if (expr.indexOf('{{') == -1) { return parser(expr, scope); } } return parseTemplate(value, scope); } return value; } function parseTemplate(template, scope) { return template.replace(/\{\{(.*?)\}\}/g, function(a, m) { var value = parser(m, scope); return value != null ? String(value) : ''; }); } exports.lexer = lexer; exports.parse = parser; exports.parseValue = parseValue; exports.parseTemplate = parseTemplate;