From bf90509526a66b295e4c438dc5c88594563707c4 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 10 Nov 2016 23:58:34 +0000 Subject: [PATCH 01/44] Add jsonata support to Change/Switch nodes --- Gruntfile.js | 3 +- editor/images/typedInput/expr.png | Bin 0 -> 786 bytes editor/js/ui/common/typedInput.js | 3 +- editor/templates/index.mst | 1 + editor/vendor/jsonata/jsonata.js | 2805 ++++++++++++++++++++++++ nodes/core/locales/en-US/messages.json | 3 + nodes/core/logic/10-switch.html | 8 +- nodes/core/logic/10-switch.js | 53 +- nodes/core/logic/15-change.html | 2 +- nodes/core/logic/15-change.js | 10 + package.json | 1 + red/runtime/util.js | 3 + 12 files changed, 2884 insertions(+), 8 deletions(-) create mode 100644 editor/images/typedInput/expr.png create mode 100644 editor/vendor/jsonata/jsonata.js diff --git a/Gruntfile.js b/Gruntfile.js index ffb1df3c3..4fc9b6fed 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -160,7 +160,8 @@ module.exports = function(grunt) { build: { files: { 'public/red/red.min.js': 'public/red/red.js', - 'public/red/main.min.js': 'public/red/main.js' + 'public/red/main.min.js': 'public/red/main.js', + 'public/vendor/jsonata/jsonata.min.js': 'editor/vendor/jsonata/jsonata.js' } } }, diff --git a/editor/images/typedInput/expr.png b/editor/images/typedInput/expr.png new file mode 100644 index 0000000000000000000000000000000000000000..74d9516aee3c9d64b8f0455bdab7f82b94208433 GIT binary patch literal 786 zcmV+t1MU2YP)53WFU8GbZ8()Nlj2>E@cM*00MSNL_t(o!|j(#YZFlv z$N%RhnMRZ##rRruV?c=5g<+UUf)GVSC=?e8tybE83BQ9MLTQ2(H@fO$B@IYsh+#m{ zg<^1F7fOsSL@}8(bKFF@xhCl(ooq_Z@^J3`o%fvqCNhyBh5Q(0GMW1T9!RPemStTV z1t|O&HJi->Gw(^NE9rE4W^6#malQiBl+Q8q^4Nd?AcT0>RSTkJza<$77>Pu-nE8tY zJZGBb<*@;a#bTR?@=BDeV*>&}tJQj>sBUMo*<)h^Rw|W`%=}V{@F+9i8XK?!i7Tqr zzz1Ji044xiQ8R#qWm%sBkFd||5rCDzQ<}@=rT{Dqa6|+ObY1r*5gqSS*BQewt_BKJ z)5*+llufh}Skq*)*)yK!?GjPwU%<>fOGIx0L^`@Z&1Uno<2XML93e9=NiZ<;hHcv) zn0ZSwJECdYLLezs((s6=>`d+pG)+?{q6-qd)@U@g0iaf^Z2eyO+!5LL&j=@%TJ|X(^qr zEz9Z}hjO|63c!2Gp|h^*ULOhAdDZ3S$@ek!-8_APx~`vM<{R>JD1=z=>^{tm}sZ`npup>Dz6$*tGhXVFU=h~q3d!ASD^ZC&eXc)#N z0JDHet@=Z?=bUcDXk|pO`Dti!W}O83&4u%I{^j^ Qu>b%707*qoM6N<$f?82bZU6uP literal 0 HcmV?d00001 diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index a455efc66..b30af8961 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -94,7 +94,8 @@ bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.png",options:["true","false"]}, json: {value:"json",label:"JSON",icon:"red/images/typedInput/json.png", validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}}, re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.png"}, - date: {value:"date",label:"timestamp",hasValue:false} + date: {value:"date",label:"timestamp",hasValue:false}, + jsonata: {value:"jsonata",label:"expression",icon:"red/images/typedInput/expr.png", validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}}, }; var nlsd = false; diff --git a/editor/templates/index.mst b/editor/templates/index.mst index 146cc866b..0cd340863 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -160,6 +160,7 @@ + diff --git a/editor/vendor/jsonata/jsonata.js b/editor/vendor/jsonata/jsonata.js new file mode 100644 index 000000000..2883b5fc4 --- /dev/null +++ b/editor/vendor/jsonata/jsonata.js @@ -0,0 +1,2805 @@ +/** + * © Copyright IBM Corp. 2016 All Rights Reserved + * Project name: JSONata + * This project is licensed under the MIT License, see LICENSE + */ + +'use strict'; +/** + * @module JSONata + * @description JSON query and transformation language + */ + +/** + * jsonata + * @function + * @param {Object} expr - JSONata expression + * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression + */ +var jsonata = (function() { + var operators = { + '.': 75, + '[': 80, + ']': 0, + '{': 70, + '}': 0, + '(': 80, + ')': 0, + ',': 0, + '@': 75, + '#': 70, + ';': 80, + ':': 80, + '?': 20, + '+': 50, + '-': 50, + '*': 60, + '/': 60, + '%': 60, + '|': 20, + '=': 40, + '<': 40, + '>': 40, + '`': 80, + '**': 60, + '..': 20, + ':=': 10, + '!=': 40, + '<=': 40, + '>=': 40, + 'and': 30, + 'or': 25, + 'in': 40, + '&': 50, + '!': 0 // not an operator, but needed as a stop character for name tokens + }; + + var escapes = { // JSON string escape sequences - see json.org + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }; + + // Tokenizer (lexer) - invoked by the parser to return one token at a time + var tokenizer = function (path) { + var position = 0; + var length = path.length; + + var create = function (type, value) { + var obj = {type: type, value: value, position: position}; + return obj; + }; + + var next = function () { + if (position >= length) return null; + var currentChar = path.charAt(position); + // skip whitespace + while (position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { + position++; + currentChar = path.charAt(position); + } + // handle double-char operators + if (currentChar === '.' && path.charAt(position + 1) === '.') { + // double-dot .. range operator + position += 2; + return create('operator', '..'); + } + if (currentChar === ':' && path.charAt(position + 1) === '=') { + // := assignment + position += 2; + return create('operator', ':='); + } + if (currentChar === '!' && path.charAt(position + 1) === '=') { + // != + position += 2; + return create('operator', '!='); + } + if (currentChar === '>' && path.charAt(position + 1) === '=') { + // >= + position += 2; + return create('operator', '>='); + } + if (currentChar === '<' && path.charAt(position + 1) === '=') { + // <= + position += 2; + return create('operator', '<='); + } + if (currentChar === '*' && path.charAt(position + 1) === '*') { + // ** descendant wildcard + position += 2; + return create('operator', '**'); + } + // test for operators + if (operators.hasOwnProperty(currentChar)) { + position++; + return create('operator', currentChar); + } + // test for string literals + if (currentChar === '"' || currentChar === "'") { + var quoteType = currentChar; + // double quoted string literal - find end of string + position++; + var qstr = ""; + while (position < length) { + currentChar = path.charAt(position); + if (currentChar === '\\') { // escape sequence + position++; + currentChar = path.charAt(position); + if (escapes.hasOwnProperty(currentChar)) { + qstr += escapes[currentChar]; + } else if (currentChar === 'u') { + // \u should be followed by 4 hex digits + var octets = path.substr(position + 1, 4); + if (/^[0-9a-fA-F]+$/.test(octets)) { + var codepoint = parseInt(octets, 16); + qstr += String.fromCharCode(codepoint); + position += 4; + } else { + throw { + message: "The escape sequence \\u must be followed by 4 hex digits at column " + position, + stack: (new Error()).stack, + position: position + }; + } + } else { + // illegal escape sequence + throw { + message: 'unsupported escape sequence: \\' + currentChar + ' at column ' + position, + stack: (new Error()).stack, + position: position, + token: currentChar + }; + + } + } else if (currentChar === quoteType) { + position++; + return create('string', qstr); + } else { + qstr += currentChar; + } + position++; + } + throw { + message: 'no terminating quote found in string literal at column ' + position, + stack: (new Error()).stack, + position: position + }; + } + // test for numbers + var numregex = /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?/; + var match = numregex.exec(path.substring(position)); + if (match !== null) { + var num = parseFloat(match[0]); + if (!isNaN(num) && isFinite(num)) { + position += match[0].length; + return create('number', num); + } else { + throw { + message: 'Number out of range: ' + match[0] + ' at column ' + position, + stack: (new Error()).stack, + position: position, + token: match[0] + }; + } + } + // test for names + var i = position; + var ch; + var name; + for (;;) { + ch = path.charAt(i); + if (i == length || ' \t\n\r\v'.indexOf(ch) > -1 || operators.hasOwnProperty(ch)) { + if (path.charAt(position) === '$') { + // variable reference + name = path.substring(position + 1, i); + position = i; + return create('variable', name); + } else { + name = path.substring(position, i); + position = i; + switch (name) { + case 'and': + case 'or': + case 'in': + return create('operator', name); + case 'true': + return create('value', true); + case 'false': + return create('value', false); + case 'null': + return create('value', null); + default: + if (position == length && name === '') { + // whitespace at end of input + return null; + } + return create('name', name); + } + } + } else { + i++; + } + } + }; + + return next; + }; + + + // This parser implements the 'Top down operator precedence' algorithm developed by Vaughan R Pratt; http://dl.acm.org/citation.cfm?id=512931. + // and builds on the Javascript framework described by Douglas Crockford at http://javascript.crockford.com/tdop/tdop.html + // and in 'Beautiful Code', edited by Andy Oram and Greg Wilson, Copyright 2007 O'Reilly Media, Inc. 798-0-596-51004-6 + + var parser = function (source) { + var node; + var lexer; + + var symbol_table = {}; + + var base_symbol = { + nud: function () { + return this; + } + }; + + var symbol = function (id, bp) { + var s = symbol_table[id]; + bp = bp || 0; + if (s) { + if (bp >= s.lbp) { + s.lbp = bp; + } + } else { + s = Object.create(base_symbol); + s.id = s.value = id; + s.lbp = bp; + symbol_table[id] = s; + } + return s; + }; + + var advance = function (id) { + if (id && node.id !== id) { + var msg; + if(node.id === '(end)') { + // unexpected end of buffer + msg = "Syntax error: expected '" + id + "' before end of expression"; + } else { + msg = "Syntax error: expected '" + id + "', got '" + node.id + "' at column " + node.position; + } + throw { + message: msg , + stack: (new Error()).stack, + position: node.position, + token: node.id, + value: id + }; + } + var next_token = lexer(); + if (next_token === null) { + node = symbol_table["(end)"]; + return node; + } + var value = next_token.value; + var type = next_token.type; + var symbol; + switch (type) { + case 'name': + case 'variable': + symbol = symbol_table["(name)"]; + break; + case 'operator': + symbol = symbol_table[value]; + if (!symbol) { + throw { + message: "Unknown operator: " + value + " at column " + next_token.position, + stack: (new Error()).stack, + position: next_token.position, + token: value + }; + } + break; + case 'string': + case 'number': + case 'value': + type = "literal"; + symbol = symbol_table["(literal)"]; + break; + /* istanbul ignore next */ + default: + throw { + message: "Unexpected token:" + value + " at column " + next_token.position, + stack: (new Error()).stack, + position: next_token.position, + token: value + }; + } + + node = Object.create(symbol); + node.value = value; + node.type = type; + node.position = next_token.position; + return node; + }; + + // Pratt's algorithm + var expression = function (rbp) { + var left; + var t = node; + advance(); + left = t.nud(); + while (rbp < node.lbp) { + t = node; + advance(); + left = t.led(left); + } + return left; + }; + + // match infix operators + // + // left associative + var infix = function (id, bp, led) { + var bindingPower = bp || operators[id]; + var s = symbol(id, bindingPower); + s.led = led || function (left) { + this.lhs = left; + this.rhs = expression(bindingPower); + this.type = "binary"; + return this; + }; + return s; + }; + + // match infix operators + // + // right associative + var infixr = function (id, bp, led) { + var bindingPower = bp || operators[id]; + var s = symbol(id, bindingPower); + s.led = led || function (left) { + this.lhs = left; + this.rhs = expression(bindingPower - 1); // subtract 1 from bindingPower for right associative operators + this.type = "binary"; + return this; + }; + return s; + }; + + // match prefix operators + // + var prefix = function (id, nud) { + var s = symbol(id); + s.nud = nud || function () { + this.expression = expression(70); + this.type = "unary"; + return this; + }; + return s; + }; + + symbol("(end)"); + symbol("(name)"); + symbol("(literal)"); + symbol(":"); + symbol(";"); + symbol(","); + symbol(")"); + symbol("]"); + symbol("}"); + symbol(".."); // range operator + infix("."); // field reference + infix("+"); // numeric addition + infix("-"); // numeric subtraction + infix("*"); // numeric multiplication + infix("/"); // numeric division + infix("%"); // numeric modulus + infix("="); // equality + infix("<"); // less than + infix(">"); // greater than + infix("!="); // not equal to + infix("<="); // less than or equal + infix(">="); // greater than or equal + infix("&"); // string concatenation + infix("and"); // Boolean AND + infix("or"); // Boolean OR + infix("in"); // is member of array + infixr(":="); // bind variable + prefix("-"); // unary numeric negation + + // field wildcard (single level) + prefix('*', function () { + this.type = "wildcard"; + return this; + }); + + // descendant wildcard (multi-level) + prefix('**', function () { + this.type = "descendant"; + return this; + }); + + // function invocation + infix("(", operators['('], function (left) { + // left is is what we are trying to invoke + this.procedure = left; + this.type = 'function'; + this.arguments = []; + if (node.id !== ')') { + for (;;) { + if (node.type === 'operator' && node.id === '?') { + // partial function application + this.type = 'partial'; + this.arguments.push(node); + advance('?'); + } else { + this.arguments.push(expression(0)); + } + if (node.id !== ',') break; + advance(','); + } + } + advance(")"); + // if the name of the function is 'function' or λ, then this is function definition (lambda function) + if (left.type === 'name' && (left.value === 'function' || left.value === '\u03BB')) { + // all of the args must be VARIABLE tokens + this.arguments.forEach(function (arg, index) { + if (arg.type !== 'variable') { + throw { + message: 'Parameter ' + (index + 1) + ' of function definition must be a variable name (start with $)', + stack: (new Error()).stack, + position: arg.position, + token: arg.value + }; + } + }); + this.type = 'lambda'; + // parse the function body + advance('{'); + this.body = expression(0); + advance('}'); + } + return this; + }); + + // parenthesis - block expression + prefix("(", function () { + var expressions = []; + while (node.id !== ")") { + expressions.push(expression(0)); + if (node.id !== ";") { + break; + } + advance(";"); + } + advance(")"); + this.type = 'block'; + this.expressions = expressions; + return this; + }); + + // object constructor + prefix("{", function () { + var a = []; + if (node.id !== "}") { + for (;;) { + var n = expression(0); + advance(":"); + var v = expression(0); + a.push([n, v]); // holds an array of name/value expression pairs + if (node.id !== ",") { + break; + } + advance(","); + } + } + advance("}"); + this.lhs = a; + this.type = "unary"; + return this; + }); + + // array constructor + prefix("[", function () { + var a = []; + if (node.id !== "]") { + for (;;) { + var item = expression(0); + if (node.id === "..") { + // range operator + var range = {type: "binary", value: "..", position: node.position, lhs: item}; + advance(".."); + range.rhs = expression(0); + item = range; + } + a.push(item); + if (node.id !== ",") { + break; + } + advance(","); + } + } + advance("]"); + this.lhs = a; + this.type = "unary"; + return this; + }); + + // filter - predicate or array index + infix("[", operators['['], function (left) { + this.lhs = left; + this.rhs = expression(operators[']']); + this.type = 'binary'; + advance("]"); + return this; + }); + + // aggregator + infix("{", operators['{'], function (left) { + this.lhs = left; + this.rhs = expression(operators['}']); + this.type = 'binary'; + advance("}"); + return this; + }); + + // if/then/else ternary operator ?: + infix("?", operators['?'], function (left) { + this.type = 'condition'; + this.condition = left; + this.then = expression(0); + if (node.id === ':') { + // else condition + advance(":"); + this.else = expression(0); + } + return this; + }); + + // tail call optimization + // this is invoked by the post parser to analyse lambda functions to see + // if they make a tail call. If so, it is replaced by a thunk which will + // be invoked by the trampoline loop during function application. + // This enables tail-recursive functions to be written without growing the stack + var tail_call_optimize = function(expr) { + var result; + if(expr.type === 'function') { + var thunk = {type: 'lambda', thunk: true, arguments: [], position: expr.position}; + thunk.body = expr; + result = thunk; + } else if(expr.type === 'condition') { + // analyse both branches + expr.then = tail_call_optimize(expr.then); + expr.else = tail_call_optimize(expr.else); + result = expr; + } else if(expr.type === 'block') { + // only the last expression in the block + var length = expr.expressions.length; + if(length > 0) { + expr.expressions[length - 1] = tail_call_optimize(expr.expressions[length - 1]); + } + result = expr; + } else { + result = expr; + } + return result; + }; + + // post-parse stage + // the purpose of this is flatten the parts of the AST representing location paths, + // converting them to arrays of steps which in turn may contain arrays of predicates. + // following this, nodes containing '.' and '[' should be eliminated from the AST. + var post_parse = function (expr) { + var result = []; + switch (expr.type) { + case 'binary': + switch (expr.value) { + case '.': + var step = post_parse(expr.lhs); + if (Array.isArray(step)) { + Array.prototype.push.apply(result, step); + } else { + result.push(step); + } + var rest = [post_parse(expr.rhs)]; + Array.prototype.push.apply(result, rest); + result.type = 'path'; + break; + case '[': + // predicated step + // LHS is a step or a predicated step + // RHS is the predicate expr + result = post_parse(expr.lhs); + if (typeof result.aggregate !== 'undefined') { + throw { + message: 'A predicate cannot follow an aggregate in a step. Error at column: ' + expr.position, + stack: (new Error()).stack, + position: expr.position + }; + } + if (typeof result.predicate === 'undefined') { + result.predicate = []; + } + result.predicate.push(post_parse(expr.rhs)); + break; + case '{': + // aggregate + // LHS is a step or a predicated step + // RHS is the predicate expr + result = post_parse(expr.lhs); + if (typeof result.aggregate !== 'undefined') { + throw { + message: 'Each step can only have one aggregator. Error at column: ' + expr.position, + stack: (new Error()).stack, + position: expr.position + }; + } + result.aggregate = post_parse(expr.rhs); + break; + default: + result = {type: expr.type, value: expr.value, position: expr.position}; + result.lhs = post_parse(expr.lhs); + result.rhs = post_parse(expr.rhs); + } + break; + case 'unary': + result = {type: expr.type, value: expr.value, position: expr.position}; + if (expr.value === '[') { + // array constructor - process each item + result.lhs = expr.lhs.map(function (item) { + return post_parse(item); + }); + } else if (expr.value === '{') { + // object constructor - process each pair + result.lhs = expr.lhs.map(function (pair) { + return [post_parse(pair[0]), post_parse(pair[1])]; + }); + } else { + // all other unary expressions - just process the expression + result.expression = post_parse(expr.expression); + // if unary minus on a number, then pre-process + if (expr.value === '-' && result.expression.type === 'literal' && isNumeric(result.expression.value)) { + result = result.expression; + result.value = -result.value; + } + } + break; + case 'function': + case 'partial': + result = {type: expr.type, name: expr.name, value: expr.value, position: expr.position}; + result.arguments = expr.arguments.map(function (arg) { + return post_parse(arg); + }); + result.procedure = post_parse(expr.procedure); + break; + case 'lambda': + result = {type: expr.type, arguments: expr.arguments, position: expr.position}; + var body = post_parse(expr.body); + result.body = tail_call_optimize(body); + break; + case 'condition': + result = {type: expr.type, position: expr.position}; + result.condition = post_parse(expr.condition); + result.then = post_parse(expr.then); + if (typeof expr.else !== 'undefined') { + result.else = post_parse(expr.else); + } + break; + case 'block': + result = {type: expr.type, position: expr.position}; + // array of expressions - process each one + result.expressions = expr.expressions.map(function (item) { + return post_parse(item); + }); + // TODO scan the array of expressions to see if any of them assign variables + // if so, need to mark the block as one that needs to create a new frame + break; + case 'name': + case 'literal': + case 'wildcard': + case 'descendant': + case 'variable': + result = expr; + break; + case 'operator': + // the tokens 'and' and 'or' might have been used as a name rather than an operator + if (expr.value === 'and' || expr.value === 'or' || expr.value === 'in') { + expr.type = 'name'; + result = post_parse(expr); + } else if (expr.value === '?') { + // partial application + result = expr; + } else { + throw { + message: "Syntax error: " + expr.value + " at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value + }; + } + break; + default: + var reason = "Unknown expression type: " + expr.value + " at column " + expr.position; + /* istanbul ignore else */ + if (expr.id === '(end)') { + reason = "Syntax error: unexpected end of expression"; + } + throw { + message: reason, + stack: (new Error()).stack, + position: expr.position, + token: expr.value + }; + } + return result; + }; + + // now invoke the tokenizer and the parser and return the syntax tree + + lexer = tokenizer(source); + advance(); + // parse the tokens + var expr = expression(0); + if (node.id !== '(end)') { + throw { + message: "Syntax error: " + node.value + " at column " + node.position, + stack: (new Error()).stack, + position: node.position, + token: node.value + }; + } + expr = post_parse(expr); + + // a single name token is a single step location path + if (expr.type === 'name') { + expr = [expr]; + expr.type = 'path'; + } + + return expr; + }; + + /** + * Check if value is a finite number + * @param {float} n - number to evaluate + * @returns {boolean} True if n is a finite number + */ + function isNumeric(n) { + var isNum = false; + if(typeof n === 'number') { + var num = parseFloat(n); + isNum = !isNaN(num); + if (isNum && !isFinite(num)) { + throw { + message: "Number out of range", + value: n, + stack: (new Error()).stack + }; + } + } + return isNum; + } + + /** + * Returns true if the arg is an array of numbers + * @param {*} arg - the item to test + * @returns {boolean} True if arg is an array of numbers + */ + function isArrayOfNumbers(arg) { + var result = false; + if(Array.isArray(arg)) { + result = (arg.filter(function(item){return !isNumeric(item);}).length == 0); + } + return result; + } + + // Polyfill + /* istanbul ignore next */ + Number.isInteger = Number.isInteger || function(value) { + return typeof value === "number" && + isFinite(value) && + Math.floor(value) === value; + }; + + /** + * Evaluate expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluate(expr, input, environment) { + var result; + + var entryCallback = environment.lookup('__evaluate_entry'); + if(entryCallback) { + entryCallback(expr, input, environment); + } + + switch (expr.type) { + case 'path': + result = evaluatePath(expr, input, environment); + break; + case 'binary': + result = evaluateBinary(expr, input, environment); + break; + case 'unary': + result = evaluateUnary(expr, input, environment); + break; + case 'name': + result = evaluateName(expr, input, environment); + break; + case 'literal': + result = evaluateLiteral(expr, input, environment); + break; + case 'wildcard': + result = evaluateWildcard(expr, input, environment); + break; + case 'descendant': + result = evaluateDescendants(expr, input, environment); + break; + case 'condition': + result = evaluateCondition(expr, input, environment); + break; + case 'block': + result = evaluateBlock(expr, input, environment); + break; + case 'function': + result = evaluateFunction(expr, input, environment); + break; + case 'variable': + result = evaluateVariable(expr, input, environment); + break; + case 'lambda': + result = evaluateLambda(expr, input, environment); + break; + case 'partial': + result = evaluatePartialApplication(expr, input, environment); + break; + } + if (expr.hasOwnProperty('predicate')) { + result = applyPredicates(expr.predicate, result, environment); + } + if (expr.hasOwnProperty('aggregate')) { + result = applyAggregate(expr.aggregate, result, environment); + } + + var exitCallback = environment.lookup('__evaluate_exit'); + if(exitCallback) { + exitCallback(expr, input, environment, result); + } + + return result; + } + + /** + * Evaluate path expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluatePath(expr, input, environment) { + var result; + var inputSequence; + // expr is an array of steps + // if the first step is a variable reference ($...), including root reference ($$), + // then the path is absolute rather than relative + if (expr[0].type === 'variable') { + expr[0].absolute = true; + } else if(expr[0].type === 'unary' && expr[0].value === '[') { + // array constructor - not relative to the input + input = [null];// dummy singleton sequence for first step + } + + // evaluate each step in turn + expr.forEach(function (step) { + var resultSequence = []; + result = undefined; + // if input is not an array, make it so + if (step.absolute === true) { + inputSequence = [input]; // dummy singleton sequence for first (absolute) step + } else if (Array.isArray(input)) { + inputSequence = input; + } else { + inputSequence = [input]; + } + // if there is more than one step in the path, handle quoted field names as names not literals + if (expr.length > 1 && step.type === 'literal') { + step.type = 'name'; + } + if (step.value === '{') { + if(typeof input !== 'undefined') { + result = evaluateGroupExpression(step, inputSequence, environment); + } + } else { + inputSequence.forEach(function (item) { + var res = evaluate(step, item, environment); + if (typeof res !== 'undefined') { + if (Array.isArray(res)) { + // is res an array - if so, flatten it into the parent array + res.forEach(function (innerRes) { + if (typeof innerRes !== 'undefined') { + resultSequence.push(innerRes); + } + }); + } else { + resultSequence.push(res); + } + } + }); + if (resultSequence.length == 1) { + result = resultSequence[0]; + } else if (resultSequence.length > 1) { + result = resultSequence; + } + } + + input = result; + }); + return result; + } + + /** + * Apply predicates to input data + * @param {Object} predicates - Predicates + * @param {Object} input - Input data to apply predicates against + * @param {Object} environment - Environment + * @returns {*} Result after applying predicates + */ + function applyPredicates(predicates, input, environment) { + var result; + var inputSequence = input; + // lhs potentially holds an array + // we want to iterate over the array, and only keep the items that are + // truthy when applied to the predicate. + // if the predicate evaluates to an integer, then select that index + + var results = []; + predicates.forEach(function (predicate) { + // if it's not an array, turn it into one + // since in XPath >= 2.0 an item is equivalent to a singleton sequence of that item + // if input is not an array, make it so + if (!Array.isArray(inputSequence)) { + inputSequence = [inputSequence]; + } + results = []; + result = undefined; + if (predicate.type === 'literal' && isNumeric(predicate.value)) { + var index = predicate.value; + if (!Number.isInteger(index)) { + // round it down + index = Math.floor(index); + } + if (index < 0) { + // count in from end of array + index = inputSequence.length + index; + } + result = inputSequence[index]; + } else { + inputSequence.forEach(function (item, index) { + var res = evaluate(predicate, item, environment); + if (isNumeric(res)) { + res = [res]; + } + if(isArrayOfNumbers(res)) { + res.forEach(function(ires) { + if (!Number.isInteger(ires)) { + // round it down + ires = Math.floor(ires); + } + if (ires < 0) { + // count in from end of array + ires = inputSequence.length + ires; + } + if (ires == index) { + results.push(item); + } + }); + } else if (functionBoolean(res)) { // truthy + results.push(item); + } + }); + } + if (results.length == 1) { + result = results[0]; + } else if (results.length > 1) { + result = results; + } + inputSequence = result; + }); + return result; + } + + /** + * Apply aggregate to input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {{}} Result after applying aggregate + */ + function applyAggregate(expr, input, environment) { + var result = {}; + // this is effectively a 'reduce' HOF (fold left) + // if the input is a singleton, then just return this as the result + // otherwise iterate over the input array and aggregate the result + if (Array.isArray(input)) { + // create a new frame to limit the scope + var aggEnv = createFrame(environment); + // the variable $@ will hold the aggregated value, initialize this to the first array item + aggEnv.bind('_', input[0]); + // loop over the remainder of the array + for (var index = 1; index < input.length; index++) { + var reduce = evaluate(expr, input[index], aggEnv); + aggEnv.bind('_', reduce); + } + result = aggEnv.lookup('_'); + } else { + result = input; + } + return result; + } + + /** + * Evaluate binary expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateBinary(expr, input, environment) { + var result; + + switch (expr.value) { + case '+': + case '-': + case '*': + case '/': + case '%': + result = evaluateNumericExpression(expr, input, environment); + break; + case '=': + case '!=': + case '<': + case '<=': + case '>': + case '>=': + result = evaluateComparisonExpression(expr, input, environment); + break; + case '&': + result = evaluateStringConcat(expr, input, environment); + break; + case 'and': + case 'or': + result = evaluateBooleanExpression(expr, input, environment); + break; + case '..': + result = evaluateRangeExpression(expr, input, environment); + break; + case ':=': + result = evaluateBindExpression(expr, input, environment); + break; + case 'in': + result = evaluateIncludesExpression(expr, input, environment); + break; + } + return result; + } + + /** + * Evaluate unary expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateUnary(expr, input, environment) { + var result; + + switch (expr.value) { + case '-': + result = evaluate(expr.expression, input, environment); + if (isNumeric(result)) { + result = -result; + } else { + throw { + message: "Cannot negate a non-numeric value: " + result + " at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: result + }; + } + break; + case '[': + // array constructor - evaluate each item + result = []; + expr.lhs.forEach(function (item) { + var value = evaluate(item, input, environment); + if (typeof value !== 'undefined') { + if (item.value === '..') { + // array generated by the range operator - merge into results + result = functionAppend(result, value); + } else { + result.push(value); + } + } + }); + break; + case '{': + // object constructor - apply grouping + result = evaluateGroupExpression(expr, input, environment); + break; + + } + return result; + } + + /** + * Evaluate name object against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateName(expr, input, environment) { + // lookup the 'name' item in the input + var result; + if (Array.isArray(input)) { + var results = []; + input.forEach(function (item) { + var res = evaluateName(expr, item, environment); + if (typeof res !== 'undefined') { + results.push(res); + } + }); + if (results.length == 1) { + result = results[0]; + } else if (results.length > 1) { + result = results; + } + } else if (input !== null && typeof input === 'object') { + result = input[expr.value]; + } + return result; + } + + /** + * Evaluate literal against input data + * @param {Object} expr - JSONata expression + * @returns {*} Evaluated input data + */ + function evaluateLiteral(expr) { + return expr.value; + } + + /** + * Evaluate wildcard against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @returns {*} Evaluated input data + */ + function evaluateWildcard(expr, input) { + var result; + var results = []; + if (input !== null && typeof input === 'object') { + Object.keys(input).forEach(function (key) { + var value = input[key]; + if(Array.isArray(value)) { + value = flatten(value); + results = functionAppend(results, value); + } else { + results.push(value); + } + }); + } + + if (results.length == 1) { + result = results[0]; + } else if (results.length > 1) { + result = results; + } + return result; + } + + /** + * Returns a flattened array + * @param {Array} arg - the array to be flatten + * @param {Array} flattened - carries the flattened array - if not defined, will initialize to [] + * @returns {Array} - the flattened array + */ + function flatten(arg, flattened) { + if(typeof flattened === 'undefined') { + flattened = []; + } + if(Array.isArray(arg)) { + arg.forEach(function (item) { + flatten(item, flattened); + }); + } else { + flattened.push(arg); + } + return flattened; + } + + /** + * Evaluate descendants against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @returns {*} Evaluated input data + */ + function evaluateDescendants(expr, input) { + var result; + var resultSequence = []; + if (typeof input !== 'undefined') { + // traverse all descendants of this object/array + recurseDescendants(input, resultSequence); + if (resultSequence.length == 1) { + result = resultSequence[0]; + } else { + result = resultSequence; + } + } + return result; + } + + /** + * Recurse through descendants + * @param {Object} input - Input data + * @param {Object} results - Results + */ + function recurseDescendants(input, results) { + // this is the equivalent of //* in XPath + if (!Array.isArray(input)) { + results.push(input); + } + if (Array.isArray(input)) { + input.forEach(function (member) { + recurseDescendants(member, results); + }); + } else if (input !== null && typeof input === 'object') { + Object.keys(input).forEach(function (key) { + recurseDescendants(input[key], results); + }); + } + } + + /** + * Evaluate numeric expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateNumericExpression(expr, input, environment) { + var result; + + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + // if either side is undefined, the result is undefined + return result; + } + + if (!isNumeric(lhs)) { + throw { + message: 'LHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: lhs + }; + } + if (!isNumeric(rhs)) { + throw { + message: 'RHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: rhs + }; + } + + switch (expr.value) { + case '+': + result = lhs + rhs; + break; + case '-': + result = lhs - rhs; + break; + case '*': + result = lhs * rhs; + break; + case '/': + result = lhs / rhs; + break; + case '%': + result = lhs % rhs; + break; + } + return result; + } + + /** + * Evaluate comparison expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateComparisonExpression(expr, input, environment) { + var result; + + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + // if either side is undefined, the result is false + return false; + } + + switch (expr.value) { + case '=': + result = lhs == rhs; + break; + case '!=': + result = (lhs != rhs); + break; + case '<': + result = lhs < rhs; + break; + case '<=': + result = lhs <= rhs; + break; + case '>': + result = lhs > rhs; + break; + case '>=': + result = lhs >= rhs; + break; + } + return result; + } + + /** + * Inclusion operator - in + * + * @param {Object} expr - AST + * @param {*} input - input context + * @param {Object} environment - frame + * @returns {boolean} - true if lhs is a member of rhs + */ + function evaluateIncludesExpression(expr, input, environment) { + var result = false; + + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + // if either side is undefined, the result is false + return false; + } + + if(!Array.isArray(rhs)) { + rhs = [rhs]; + } + + for(var i = 0; i < rhs.length; i++) { + if(rhs[i] === lhs) { + result = true; + break; + } + } + + return result; + } + + /** + * Evaluate boolean expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateBooleanExpression(expr, input, environment) { + var result; + + switch (expr.value) { + case 'and': + result = functionBoolean(evaluate(expr.lhs, input, environment)) && + functionBoolean(evaluate(expr.rhs, input, environment)); + break; + case 'or': + result = functionBoolean(evaluate(expr.lhs, input, environment)) || + functionBoolean(evaluate(expr.rhs, input, environment)); + break; + } + return result; + } + + /** + * Evaluate string concatenation against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {string|*} Evaluated input data + */ + function evaluateStringConcat(expr, input, environment) { + var result; + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + var lstr = ''; + var rstr = ''; + if (typeof lhs !== 'undefined') { + lstr = functionString(lhs); + } + if (typeof rhs !== 'undefined') { + rstr = functionString(rhs); + } + + result = lstr.concat(rstr); + return result; + } + + /** + * Evaluate group expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {{}} Evaluated input data + */ + function evaluateGroupExpression(expr, input, environment) { + var result = {}; + var groups = {}; + // group the input sequence by 'key' expression + if (!Array.isArray(input)) { + input = [input]; + } + input.forEach(function (item) { + expr.lhs.forEach(function (pair) { + var key = evaluate(pair[0], item, environment); + // key has to be a string + if (typeof key !== 'string') { + throw { + message: 'Key in object structure must evaluate to a string. Got: ' + key + ' at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + value: key + }; + } + var entry = {data: item, expr: pair[1]}; + if (groups.hasOwnProperty(key)) { + // a value already exists in this slot + // append it as an array + groups[key].data = functionAppend(groups[key].data, item); + } else { + groups[key] = entry; + } + }); + }); + // iterate over the groups to evaluate the 'value' expression + for (var key in groups) { + var entry = groups[key]; + var value = evaluate(entry.expr, entry.data, environment); + result[key] = value; + } + return result; + } + + /** + * Evaluate range expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateRangeExpression(expr, input, environment) { + var result; + + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + // if either side is undefined, the result is undefined + return result; + } + + if (lhs > rhs) { + // if the lhs is greater than the rhs, return undefined + return result; + } + + if (!Number.isInteger(lhs)) { + throw { + message: 'LHS of range operator (..) must evaluate to an integer at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: lhs + }; + } + if (!Number.isInteger(rhs)) { + throw { + message: 'RHS of range operator (..) must evaluate to an integer at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: rhs + }; + } + + result = new Array(rhs - lhs + 1); + for (var item = lhs, index = 0; item <= rhs; item++, index++) { + result[index] = item; + } + return result; + } + + /** + * Evaluate bind expression against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateBindExpression(expr, input, environment) { + // The RHS is the expression to evaluate + // The LHS is the name of the variable to bind to - should be a VARIABLE token + var value = evaluate(expr.rhs, input, environment); + if (expr.lhs.type !== 'variable') { + throw { + message: "Left hand side of := must be a variable name (start with $) at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: expr.lhs.value + }; + } + environment.bind(expr.lhs.value, value); + return value; + } + + /** + * Evaluate condition against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateCondition(expr, input, environment) { + var result; + var condition = evaluate(expr.condition, input, environment); + if (functionBoolean(condition)) { + result = evaluate(expr.then, input, environment); + } else if (typeof expr.else !== 'undefined') { + result = evaluate(expr.else, input, environment); + } + return result; + } + + /** + * Evaluate block against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateBlock(expr, input, environment) { + var result; + // create a new frame to limit the scope of variable assignments + // TODO, only do this if the post-parse stage has flagged this as required + var frame = createFrame(environment); + // invoke each expression in turn + // only return the result of the last one + expr.expressions.forEach(function (expression) { + result = evaluate(expression, input, frame); + }); + + return result; + } + + /** + * Evaluate variable against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateVariable(expr, input, environment) { + // lookup the variable value in the environment + var result; + // if the variable name is empty string, then it refers to context value + if (expr.value === '') { + result = input; + } else { + result = environment.lookup(expr.value); + } + return result; + } + + /** + * Evaluate function against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluateFunction(expr, input, environment) { + var result; + // evaluate the arguments + var evaluatedArgs = []; + expr.arguments.forEach(function (arg) { + evaluatedArgs.push(evaluate(arg, input, environment)); + }); + // lambda function on lhs + // create the procedure + // can't assume that expr.procedure is a lambda type directly + // could be an expression that evaluates to a function (e.g. variable reference, parens expr etc. + // evaluate it generically first, then check that it is a function. Throw error if not. + var proc = evaluate(expr.procedure, input, environment); + + if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) { + // help the user out here if they simply forgot the leading $ + throw { + message: 'Attempted to invoke a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.value + }; + } + // apply the procedure + try { + result = apply(proc, evaluatedArgs, environment, input); + while(typeof result === 'object' && result.lambda == true && result.thunk == true) { + // trampoline loop - this gets invoked as a result of tail-call optimization + // the function returned a tail-call thunk + // unpack it, evaluate its arguments, and apply the tail call + var next = evaluate(result.body.procedure, result.input, result.environment); + evaluatedArgs = []; + result.body.arguments.forEach(function (arg) { + evaluatedArgs.push(evaluate(arg, result.input, result.environment)); + }); + + result = apply(next, evaluatedArgs); + } + } catch (err) { + // add the position field to the error + err.position = expr.position; + // and the function identifier + err.token = expr.procedure.value; + throw err; + } + return result; + } + + /** + * Apply procedure or function + * @param {Object} proc - Procedure + * @param {Array} args - Arguments + * @param {Object} environment - Environment + * @param {Object} self - Self + * @returns {*} Result of procedure + */ + function apply(proc, args, environment, self) { + var result; + if (proc && proc.lambda) { + result = applyProcedure(proc, args); + } else if (typeof proc === 'function') { + result = proc.apply(self, args); + } else { + throw { + message: 'Attempted to invoke a non-function', + stack: (new Error()).stack + }; + } + return result; + } + + /** + * Evaluate lambda against input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {{lambda: boolean, input: *, environment: *, arguments: *, body: *}} Evaluated input data + */ + function evaluateLambda(expr, input, environment) { + // make a function (closure) + var procedure = { + lambda: true, + input: input, + environment: environment, + arguments: expr.arguments, + body: expr.body + }; + if(expr.thunk == true) { + procedure.thunk = true; + } + return procedure; + } + + /** + * Evaluate partial application + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {*} Evaluated input data + */ + function evaluatePartialApplication(expr, input, environment) { + // partially apply a function + var result; + // evaluate the arguments + var evaluatedArgs = []; + expr.arguments.forEach(function (arg) { + if (arg.type === 'operator' && arg.value === '?') { + evaluatedArgs.push(arg); + } else { + evaluatedArgs.push(evaluate(arg, input, environment)); + } + }); + // lookup the procedure + var proc = evaluate(expr.procedure, input, environment); + if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) { + // help the user out here if they simply forgot the leading $ + throw { + message: 'Attempted to partially apply a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.value + }; + } + if (proc && proc.lambda) { + result = partialApplyProcedure(proc, evaluatedArgs); + } else if (typeof proc === 'function') { + result = partialApplyNativeFunction(proc, evaluatedArgs); + } else { + throw { + message: 'Attempted to partially apply a non-function at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.value + }; + } + return result; + } + + /** + * Apply procedure + * @param {Object} proc - Procedure + * @param {Array} args - Arguments + * @returns {*} Result of procedure + */ + function applyProcedure(proc, args) { + var result; + var env = createFrame(proc.environment); + proc.arguments.forEach(function (param, index) { + env.bind(param.value, args[index]); + }); + if (typeof proc.body === 'function') { + // this is a lambda that wraps a native function - generated by partially evaluating a native + result = applyNativeFunction(proc.body, env); + } else { + result = evaluate(proc.body, proc.input, env); + } + return result; + } + + /** + * Partially apply procedure + * @param {Object} proc - Procedure + * @param {Array} args - Arguments + * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applied procedure + */ + function partialApplyProcedure(proc, args) { + // create a closure, bind the supplied parameters and return a function that takes the remaining (?) parameters + var env = createFrame(proc.environment); + var unboundArgs = []; + proc.arguments.forEach(function (param, index) { + var arg = args[index]; + if (arg && arg.type === 'operator' && arg.value === '?') { + unboundArgs.push(param); + } else { + env.bind(param.value, arg); + } + }); + var procedure = { + lambda: true, + input: proc.input, + environment: env, + arguments: unboundArgs, + body: proc.body + }; + return procedure; + } + + /** + * Partially apply native function + * @param {Function} native - Native function + * @param {Array} args - Arguments + * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applying native function + */ + function partialApplyNativeFunction(native, args) { + // create a lambda function that wraps and invokes the native function + // get the list of declared arguments from the native function + // this has to be picked out from the toString() value + var sigArgs = getNativeFunctionArguments(native); + sigArgs = sigArgs.map(function (sigArg) { + return '$' + sigArg.trim(); + }); + var body = 'function(' + sigArgs.join(', ') + '){ _ }'; + + var bodyAST = parser(body); + bodyAST.body = native; + + var partial = partialApplyProcedure(bodyAST, args); + return partial; + } + + /** + * Apply native function + * @param {Object} proc - Procedure + * @param {Object} env - Environment + * @returns {*} Result of applying native function + */ + function applyNativeFunction(proc, env) { + var sigArgs = getNativeFunctionArguments(proc); + // generate the array of arguments for invoking the function - look them up in the environment + var args = sigArgs.map(function (sigArg) { + return env.lookup(sigArg.trim()); + }); + + var result = proc.apply(null, args); + return result; + } + + /** + * Get native function arguments + * @param {Function} func - Function + * @returns {*|Array} Native function arguments + */ + function getNativeFunctionArguments(func) { + var signature = func.toString(); + var sigParens = /\(([^\)]*)\)/.exec(signature)[1]; // the contents of the parens + var sigArgs = sigParens.split(','); + return sigArgs; + } + + /** + * Tests whether arg is a lambda function + * @param {*} arg - the value to test + * @returns {boolean} - true if it is a lambda function + */ + function isLambda(arg) { + var result = false; + if(arg && typeof arg === 'object' && + arg.lambda === true && + arg.hasOwnProperty('input') && + arg.hasOwnProperty('arguments') && + arg.hasOwnProperty('environment') && + arg.hasOwnProperty('body')) { + result = true; + } + + return result; + } + + /** + * Sum function + * @param {Object} args - Arguments + * @returns {number} Total value of arguments + */ + function functionSum(args) { + var total = 0; + + if (arguments.length != 1) { + throw { + message: 'The sum function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + + // it must be an array of numbers + var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); + if(nonNumerics.length > 0) { + throw { + message: 'Type error: argument of sum function must be an array of numbers', + stack: (new Error()).stack, + value: nonNumerics + }; + } + args.forEach(function(num){total += num;}); + return total; + } + + /** + * Count function + * @param {Object} args - Arguments + * @returns {number} Number of elements in the array + */ + function functionCount(args) { + if (arguments.length != 1) { + throw { + message: 'The count function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof args === 'undefined') { + return 0; + } + + if(!Array.isArray(args)) { + args = [args]; + } + + return args.length; + } + + /** + * Max function + * @param {Object} args - Arguments + * @returns {number} Max element in the array + */ + function functionMax(args) { + var max; + + if (arguments.length != 1) { + throw { + message: 'The max function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + + // it must be an array of numbers + var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); + if(nonNumerics.length > 0) { + throw { + message: 'Type error: argument of max function must be an array of numbers', + stack: (new Error()).stack, + value: nonNumerics + }; + } + max = Math.max.apply(Math, args); + return max; + } + + /** + * Min function + * @param {Object} args - Arguments + * @returns {number} Min element in the array + */ + function functionMin(args) { + var min; + + if (arguments.length != 1) { + throw { + message: 'The min function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + + // it must be an array of numbers + var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); + if(nonNumerics.length > 0) { + throw { + message: 'Type error: argument of min function must be an array of numbers', + stack: (new Error()).stack, + value: nonNumerics + }; + } + min = Math.min.apply(Math, args); + return min; + } + + /** + * Average function + * @param {Object} args - Arguments + * @returns {number} Average element in the array + */ + function functionAverage(args) { + var total = 0; + + if (arguments.length != 1) { + throw { + message: 'The average function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + + // it must be an array of numbers + var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); + if(nonNumerics.length > 0) { + throw { + message: 'Type error: argument of average function must be an array of numbers', + stack: (new Error()).stack, + value: nonNumerics + }; + } + args.forEach(function(num){total += num;}); + return total/args.length; + } + + /** + * Stingify arguments + * @param {Object} arg - Arguments + * @returns {String} String from arguments + */ + function functionString(arg) { + var str; + + if(arguments.length != 1) { + throw { + message: 'The string function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof arg === 'undefined') { + return undefined; + } + + if (typeof arg === 'string') { + // already a string + str = arg; + } else if(typeof arg === 'function' || isLambda(arg)) { + // functions (built-in and lambda convert to empty string + str = ''; + } else if (typeof arg === 'number' && !isFinite(arg)) { + throw { + message: "Attempting to invoke string function on Infinity or NaN", + value: arg, + stack: (new Error()).stack + }; + } else + str = JSON.stringify(arg, function (key, val) { + return (typeof val !== 'undefined' && val !== null && val.toPrecision && isNumeric(val)) ? Number(val.toPrecision(13)) : + (val && isLambda(val)) ? '' : + (typeof val === 'function') ? '' : val; + }); + return str; + } + + /** + * Create substring based on character number and length + * @param {String} str - String to evaluate + * @param {Integer} start - Character number to start substring + * @param {Integer} [length] - Number of characters in substring + * @returns {string|*} Substring + */ + function functionSubstring(str, start, length) { + if(arguments.length != 2 && arguments.length != 3) { + throw { + message: 'The substring function expects two or three arguments', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof str === 'undefined') { + return undefined; + } + + // otherwise it must be a string + if(typeof str !== 'string') { + throw { + message: 'Type error: first argument of substring function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + + if(typeof start !== 'number') { + throw { + message: 'Type error: second argument of substring function must evaluate to a number', + stack: (new Error()).stack, + value: start + }; + } + + if(typeof length !== 'undefined' && typeof length !== 'number') { + throw { + message: 'Type error: third argument of substring function must evaluate to a number', + stack: (new Error()).stack, + value: length + }; + } + + return str.substr(start, length); + } + + /** + * Create substring up until a character + * @param {String} str - String to evaluate + * @param {String} chars - Character to define substring boundary + * @returns {*} Substring + */ + function functionSubstringBefore(str, chars) { + if(arguments.length != 2) { + throw { + message: 'The substringBefore function expects two arguments', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof str === 'undefined') { + return undefined; + } + + // otherwise it must be a string + if(typeof str !== 'string') { + throw { + message: 'Type error: first argument of substringBefore function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + if(typeof chars !== 'string') { + throw { + message: 'Type error: second argument of substringBefore function must evaluate to a string', + stack: (new Error()).stack, + value: chars + }; + } + + var pos = str.indexOf(chars); + if (pos > -1) { + return str.substr(0, pos); + } else { + return str; + } + } + + /** + * Create substring after a character + * @param {String} str - String to evaluate + * @param {String} chars - Character to define substring boundary + * @returns {*} Substring + */ + function functionSubstringAfter(str, chars) { + if(arguments.length != 2) { + throw { + message: 'The substringAfter function expects two arguments', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof str === 'undefined') { + return undefined; + } + + // otherwise it must be a string + if(typeof str !== 'string') { + throw { + message: 'Type error: first argument of substringAfter function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + if(typeof chars !== 'string') { + throw { + message: 'Type error: second argument of substringAfter function must evaluate to a string', + stack: (new Error()).stack, + value: chars + }; + } + + var pos = str.indexOf(chars); + if (pos > -1) { + return str.substr(pos + chars.length); + } else { + return str; + } + } + + /** + * Lowercase a string + * @param {String} str - String to evaluate + * @returns {string} Lowercase string + */ + function functionLowercase(str) { + if(arguments.length != 1) { + throw { + message: 'The lowercase function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof str === 'undefined') { + return undefined; + } + + // otherwise it must be a string + if(typeof str !== 'string') { + throw { + message: 'Type error: argument of lowercase function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + + return str.toLowerCase(); + } + + /** + * Uppercase a string + * @param {String} str - String to evaluate + * @returns {string} Uppercase string + */ + function functionUppercase(str) { + if(arguments.length != 1) { + throw { + message: 'The uppercase function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof str === 'undefined') { + return undefined; + } + + // otherwise it must be a string + if(typeof str !== 'string') { + throw { + message: 'Type error: argument of uppercase function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + + return str.toUpperCase(); + } + + /** + * length of a string + * @param {String} str - string + * @returns {Number} The number of characters in the string + */ + function functionLength(str) { + if(arguments.length != 1) { + throw { + message: 'The length function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof str === 'undefined') { + return undefined; + } + + // otherwise it must be a string + if(typeof str !== 'string') { + throw { + message: 'Type error: argument of length function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + + return str.length; + } + + /** + * Split a string into an array of substrings + * @param {String} str - string + * @param {String} separator - the token that splits the string + * @param {Integer} [limit] - max number of substrings + * @returns {Array} The array of string + */ + function functionSplit(str, separator, limit) { + if(arguments.length != 2 && arguments.length != 3) { + throw { + message: 'The split function expects two or three arguments', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof str === 'undefined') { + return undefined; + } + + // otherwise it must be a string + if(typeof str !== 'string') { + throw { + message: 'Type error: first argument of split function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + + // separator must be a string + if(typeof separator !== 'string') { + throw { + message: 'Type error: second argument of split function must evaluate to a string', + stack: (new Error()).stack, + value: separator + }; + } + + // limit, if specified, must be a number + if(typeof limit !== 'undefined' && (typeof limit !== 'number' || limit < 0)) { + throw { + message: 'Type error: third argument of split function must evaluate to a positive number', + stack: (new Error()).stack, + value: limit + }; + } + + return str.split(separator, limit); + } + + /** + * Join an array of strings + * @param {Array} strs - array of string + * @param {String} [separator] - the token that splits the string + * @returns {String} The concatenated string + */ + function functionJoin(strs, separator) { + if(arguments.length != 1 && arguments.length != 2) { + throw { + message: 'The join function expects one or two arguments', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof strs === 'undefined') { + return undefined; + } + + if(!Array.isArray(strs)) { + strs = [strs]; + } + + // it must be an array of strings + var nonStrings = strs.filter(function(val) {return (typeof val !== 'string');}); + if(nonStrings.length > 0) { + throw { + message: 'Type error: first argument of join function must be an array of strings', + stack: (new Error()).stack, + value: nonStrings + }; + } + + + // if separator is not specified, default to empty string + if(typeof separator === 'undefined') { + separator = ""; + } + + // separator, if specified, must be a string + if(typeof separator !== 'string') { + throw { + message: 'Type error: second argument of split function must evaluate to a string', + stack: (new Error()).stack, + value: separator + }; + } + + return strs.join(separator); + } + + /** + * Cast argument to number + * @param {Object} arg - Argument + * @returns {Number} numeric value of argument + */ + function functionNumber(arg) { + var result; + + if(arguments.length != 1) { + throw { + message: 'The number function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof arg === 'undefined') { + return undefined; + } + + if (typeof arg === 'number') { + // already a number + result = arg; + } else if(typeof arg === 'string' && /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?$/.test(arg) && !isNaN(parseFloat(arg)) && isFinite(arg)) { + result = parseFloat(arg); + } else { + throw { + message: "Unable to cast value to a number", + value: arg, + stack: (new Error()).stack + }; + } + return result; + } + + + /** + * Evaluate an input and return a boolean + * @param {*} arg - Arguments + * @returns {boolean} Boolean + */ + function functionBoolean(arg) { + // cast arg to its effective boolean value + // boolean: unchanged + // string: zero-length -> false; otherwise -> true + // number: 0 -> false; otherwise -> true + // null -> false + // array: empty -> false; length > 1 -> true + // object: empty -> false; non-empty -> true + // function -> false + + if(arguments.length != 1) { + throw { + message: 'The boolean function expects one argument', + stack: (new Error()).stack + }; + } + + // undefined inputs always return undefined + if(typeof arg === 'undefined') { + return undefined; + } + + var result = false; + if (Array.isArray(arg)) { + if (arg.length == 1) { + result = functionBoolean(arg[0]); + } else if (arg.length > 1) { + var trues = arg.filter(function(val) {return functionBoolean(val);}); + result = trues.length > 0; + } + } else if (typeof arg === 'string') { + if (arg.length > 0) { + result = true; + } + } else if (isNumeric(arg)) { + if (arg != 0) { + result = true; + } + } else if (arg != null && typeof arg === 'object') { + if (Object.keys(arg).length > 0) { + // make sure it's not a lambda function + if (!(isLambda(arg))) { + result = true; + } + } + } else if (typeof arg === 'boolean' && arg == true) { + result = true; + } + return result; + } + + /** + * returns the Boolean NOT of the arg + * @param {*} arg - argument + * @returns {boolean} - NOT arg + */ + function functionNot(arg) { + return !functionBoolean(arg); + } + + /** + * Create a map from an array of arguments + * @param {Function} func - function to apply + * @returns {Array} Map array + */ + function functionMap(func) { + // this can take a variable number of arguments - each one should be mapped to the equivalent arg of func + // assert that func is a function + var varargs = arguments; + var result = []; + + // each subsequent arg must be an array - coerce if not + var args = []; + for (var ii = 1; ii < varargs.length; ii++) { + if (Array.isArray(varargs[ii])) { + args.push(varargs[ii]); + } else { + args.push([varargs[ii]]); + } + + } + // do the map - iterate over the arrays, and invoke func + if (args.length > 0) { + for (var i = 0; i < args[0].length; i++) { + var func_args = []; + for (var j = 0; j < func.arguments.length; j++) { + func_args.push(args[j][i]); + } + // invoke func + result.push(apply(func, func_args, null, null)); + } + } + return result; + } + + /** + * Fold left function + * @param {Function} func - Function + * @param {Array} sequence - Sequence + * @param {Object} init - Initial value + * @returns {*} Result + */ + function functionFoldLeft(func, sequence, init) { + var result; + + if (!(func.length == 2 || func.arguments.length == 2)) { + throw { + message: 'The first argument of the reduce function must be a function of arity 2', + stack: (new Error()).stack + }; + } + + if (!Array.isArray(sequence)) { + sequence = [sequence]; + } + + var index; + if (typeof init === 'undefined' && sequence.length > 0) { + result = sequence[0]; + index = 1; + } else { + result = init; + index = 0; + } + + while (index < sequence.length) { + result = apply(func, [result, sequence[index]], null, null); + index++; + } + + return result; + } + + /** + * Return keys for an object + * @param {Object} arg - Object + * @returns {Array} Array of keys + */ + function functionKeys(arg) { + var result = []; + + if(Array.isArray(arg)) { + // merge the keys of all of the items in the array + var merge = {}; + arg.forEach(function(item) { + var keys = functionKeys(item); + if(Array.isArray(keys)) { + keys.forEach(function(key) { + merge[key] = true; + }); + } + }); + result = functionKeys(merge); + } else if(arg != null && typeof arg === 'object' && !(isLambda(arg))) { + result = Object.keys(arg); + if(result.length == 0) { + result = undefined; + } + } else { + result = undefined; + } + return result; + } + + /** + * Return value from an object for a given key + * @param {Object} object - Object + * @param {String} key - Key in object + * @returns {*} Value of key in object + */ + function functionLookup(object, key) { + var result = evaluateName({value: key}, object); + return result; + } + + /** + * Append second argument to first + * @param {Array|Object} arg1 - First argument + * @param {Array|Object} arg2 - Second argument + * @returns {*} Appended arguments + */ + function functionAppend(arg1, arg2) { + // disregard undefined args + if (typeof arg1 === 'undefined') { + return arg2; + } + if (typeof arg2 === 'undefined') { + return arg1; + } + // if either argument is not an array, make it so + if (!Array.isArray(arg1)) { + arg1 = [arg1]; + } + if (!Array.isArray(arg2)) { + arg2 = [arg2]; + } + Array.prototype.push.apply(arg1, arg2); + return arg1; + } + + /** + * Determines if the argument is undefined + * @param {*} arg - argument + * @returns {boolean} False if argument undefined, otherwise true + */ + function functionExists(arg){ + if (arguments.length != 1) { + throw { + message: 'The exists function expects one argument', + stack: (new Error()).stack + }; + } + + if (typeof arg === 'undefined') { + return false; + } else { + return true; + } + } + + /** + * Splits an object into an array of object with one property each + * @param {*} arg - the object to split + * @returns {*} - the array + */ + function functionSpread(arg) { + var result = []; + + if(Array.isArray(arg)) { + // spread all of the items in the array + arg.forEach(function(item) { + result = functionAppend(result, functionSpread(item)); + }); + } else if(arg != null && typeof arg === 'object' && !isLambda(arg)) { + for(var key in arg) { + var obj = {}; + obj[key] = arg[key]; + result.push(obj); + } + } else { + result = arg; + } + return result; + } + + /** + * Create frame + * @param {Object} enclosingEnvironment - Enclosing environment + * @returns {{bind: bind, lookup: lookup}} Created frame + */ + function createFrame(enclosingEnvironment) { + var bindings = {}; + return { + bind: function (name, value) { + bindings[name] = value; + }, + lookup: function (name) { + var value = bindings[name]; + if (typeof value === 'undefined' && enclosingEnvironment) { + value = enclosingEnvironment.lookup(name); + } + return value; + } + }; + } + + var staticFrame = createFrame(null); + + staticFrame.bind('sum', functionSum); + staticFrame.bind('count', functionCount); + staticFrame.bind('max', functionMax); + staticFrame.bind('min', functionMin); + staticFrame.bind('average', functionAverage); + staticFrame.bind('string', functionString); + staticFrame.bind('substring', functionSubstring); + staticFrame.bind('substringBefore', functionSubstringBefore); + staticFrame.bind('substringAfter', functionSubstringAfter); + staticFrame.bind('lowercase', functionLowercase); + staticFrame.bind('uppercase', functionUppercase); + staticFrame.bind('length', functionLength); + staticFrame.bind('split', functionSplit); + staticFrame.bind('join', functionJoin); + staticFrame.bind('number', functionNumber); + staticFrame.bind('boolean', functionBoolean); + staticFrame.bind('not', functionNot); + staticFrame.bind('map', functionMap); + staticFrame.bind('reduce', functionFoldLeft); + staticFrame.bind('keys', functionKeys); + staticFrame.bind('lookup', functionLookup); + staticFrame.bind('append', functionAppend); + staticFrame.bind('exists', functionExists); + staticFrame.bind('spread', functionSpread); + + /** + * JSONata + * @param {Object} expr - JSONata expression + * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression + */ + function jsonata(expr) { + var ast = parser(expr); + var environment = createFrame(staticFrame); + return { + evaluate: function (input, bindings) { + if (typeof bindings !== 'undefined') { + var exec_env; + // the variable bindings have been passed in - create a frame to hold these + exec_env = createFrame(environment); + for (var v in bindings) { + exec_env.bind(v, bindings[v]); + } + } else { + exec_env = environment; + } + // put the input document into the environment as the root object + exec_env.bind('$', input); + return evaluate(ast, input, exec_env); + }, + assign: function (name, value) { + environment.bind(name, value); + } + }; + } + + jsonata.parser = parser; + + return jsonata; + +})(); + +// node.js only - export the jsonata and parser functions +// istanbul ignore else +if(typeof module !== 'undefined') { + module.exports = jsonata; +} diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json index ab733c45d..ee8a20264 100644 --- a/nodes/core/locales/en-US/messages.json +++ b/nodes/core/locales/en-US/messages.json @@ -502,6 +502,9 @@ "null":"is null", "nnull":"is not null", "else":"otherwise" + }, + "errors": { + "invalid-expr": "Invalid expression: __error__" } }, "change": { diff --git a/nodes/core/logic/10-switch.html b/nodes/core/logic/10-switch.html index 1330df14d..3ce53c89f 100644 --- a/nodes/core/logic/10-switch.html +++ b/nodes/core/logic/10-switch.html @@ -64,7 +64,7 @@ var node = this; var previousValueType = {value:"prev",label:this._("inject.previous"),hasValue:false}; - $("#node-input-property").typedInput({default:this.propertyType||'msg',types:['msg','flow','global']}); + $("#node-input-property").typedInput({default:this.propertyType||'msg',types:['msg','flow','global','jsonata']}); var operators = [ {v:"eq",t:"=="}, {v:"neq",t:"!="}, @@ -129,10 +129,10 @@ for (var d in operators) { selectField.append($("").val(operators[d].v).text(operators[d].t)); } - var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num',previousValueType]}); - var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num',previousValueType]}); + var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num','jsonata',previousValueType]}); + var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]}); var btwnAndLabel = $('',{class:"node-input-rule-btwn-label"}).text(" "+andLabel+" ").appendTo(row3); - var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num',previousValueType]}); + var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]}); var finalspan = $('',{style:"float: right;margin-top: 6px;"}).appendTo(row); finalspan.append(' → '+(i+1)+' '); var caseSensitive = $('',{id:"node-input-rule-case-"+i,class:"node-input-rule-case",type:"checkbox",style:"width:auto;vertical-align:top"}).appendTo(row2); diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index 6585eace1..3a335a675 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -16,6 +16,9 @@ module.exports = function(RED) { "use strict"; + + var jsonata = require('jsonata'); + var operators = { 'eq': function(a, b) { return a == b; }, 'neq': function(a, b) { return a != b; }, @@ -38,9 +41,20 @@ module.exports = function(RED) { this.rules = n.rules || []; this.property = n.property; this.propertyType = n.propertyType || "msg"; + + if (this.propertyType === 'jsonata') { + try { + this.property = jsonata(this.property); + } catch(err) { + this.error(RED._("switch.errors.invalid-expr",{error:err.message})); + return; + } + } + this.checkall = n.checkall || "true"; this.previousValue = null; var node = this; + var valid = true; for (var i=0; i',{class:"node-input-rule-property-value",type:"text"}) .appendTo(row2) - .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','date']}); + .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','date','jsonata']}); var row3_1 = $('
').appendTo(row3); $('
',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"}) diff --git a/nodes/core/logic/15-change.js b/nodes/core/logic/15-change.js index 67f30f12f..805b708ed 100644 --- a/nodes/core/logic/15-change.js +++ b/nodes/core/logic/15-change.js @@ -16,6 +16,7 @@ module.exports = function(RED) { "use strict"; + var jsonata = require("jsonata"); function ChangeNode(n) { RED.nodes.createNode(this, n); @@ -85,6 +86,13 @@ module.exports = function(RED) { } } else if (rule.tot === 'bool') { rule.to = /^true$/i.test(rule.to); + } else if (rule.tot === 'jsonata') { + try { + rule.to = jsonata(rule.to); + } catch(e) { + valid = false; + this.error(RED._("change.errors.invalid-from",{error:e.message})); + } } } @@ -107,6 +115,8 @@ module.exports = function(RED) { value = node.context().global.get(rule.to); } else if (rule.tot === 'date') { value = Date.now(); + } else if (rule.tot === 'jsonata') { + value = rule.to.evaluate({msg:msg}); } if (rule.t === 'change') { if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { diff --git a/package.json b/package.json index ac20ba7e0..33de70e92 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "fs.notify":"0.0.4", "i18next":"1.10.6", "is-utf8":"0.2.1", + "jsonata":"1.0.7", "media-typer": "0.3.0", "mqtt": "1.14.1", "mustache": "2.2.1", diff --git a/red/runtime/util.js b/red/runtime/util.js index f132564c8..97213a389 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -15,6 +15,7 @@ **/ var clone = require("clone"); +var jsonata = require("jsonata"); function generateId() { return (1+Math.random()*4294967295).toString(16); @@ -310,6 +311,8 @@ function evaluateNodeProperty(value, type, node, msg) { return node.context().global.get(value); } else if (type === 'bool') { return /^true$/i.test(value); + } else if (type === 'jsonata') { + return jsonata(value).evaluate({msg:msg}); } return value; } From d33029027f3dcddc3271d91ec0bb574dd12b520d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 15 Nov 2016 00:19:04 +0000 Subject: [PATCH 02/44] Add expression editor for jsonata --- Gruntfile.js | 4 +- editor/js/ui/common/typedInput.js | 40 +- editor/js/ui/editor.js | 170 +- editor/sass/ace.scss | 8 + editor/sass/style.scss | 1 + editor/sass/ui/common/typedInput.scss | 16 + editor/templates/index.mst | 7 + editor/vendor/jsonata/mode-jsonata.js | 129 + editor/vendor/jsonata/worker-jsonata.js | 4233 +++++++++++++++++++++++ 9 files changed, 4540 insertions(+), 68 deletions(-) create mode 100644 editor/sass/ace.scss create mode 100644 editor/vendor/jsonata/mode-jsonata.js create mode 100644 editor/vendor/jsonata/worker-jsonata.js diff --git a/Gruntfile.js b/Gruntfile.js index 4fc9b6fed..0e15c8dcd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -161,7 +161,9 @@ module.exports = function(grunt) { files: { 'public/red/red.min.js': 'public/red/red.js', 'public/red/main.min.js': 'public/red/main.js', - 'public/vendor/jsonata/jsonata.min.js': 'editor/vendor/jsonata/jsonata.js' + 'public/vendor/jsonata/jsonata.min.js': 'editor/vendor/jsonata/jsonata.js', + 'public/vendor/ace/mode-jsonata.js': 'editor/vendor/jsonata/mode-jsonata.js', + 'public/vendor/ace/worker-jsonata.js': 'editor/vendor/jsonata/worker-jsonata.js' } } }, diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index b30af8961..c433c0366 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -95,7 +95,21 @@ json: {value:"json",label:"JSON",icon:"red/images/typedInput/json.png", validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}}, re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.png"}, date: {value:"date",label:"timestamp",hasValue:false}, - jsonata: {value:"jsonata",label:"expression",icon:"red/images/typedInput/expr.png", validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}}, + jsonata: { + value: "jsonata", + label: "expression", + icon: "red/images/typedInput/expr.png", + validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}, + expand:function() { + var that = this; + RED.editor.editExpression({ + value: this.value(), + complete: function(v) { + that.value(v); + } + }) + } + } }; var nlsd = false; @@ -189,7 +203,7 @@ that.uiSelect.addClass('red-ui-typedInput-focus'); }); - + this.optionExpandButton = $('').appendTo(this.uiSelect); this.type(this.options.default||this.typeList[0].value); @@ -323,11 +337,16 @@ this.uiSelect.width(this.uiWidth); } if (this.typeMap[this.propertyType] && this.typeMap[this.propertyType].hasValue === false) { - this.selectTrigger.css('width',"100%"); + this.selectTrigger.addClass("red-ui-typedInput-full-width"); } else { - this.selectTrigger.width('auto'); + this.selectTrigger.removeClass("red-ui-typedInput-full-width"); var labelWidth = this._getLabelWidth(this.selectTrigger); this.elementDiv.css('left',labelWidth+"px"); + if (this.optionExpandButton.is(":visible")) { + this.elementDiv.css('right',"22px"); + } else { + this.elementDiv.css('right','0'); + } if (this.optionSelectTrigger) { this.optionSelectTrigger.css({'left':(labelWidth)+"px",'width':'calc( 100% - '+labelWidth+'px )'}); } @@ -397,6 +416,9 @@ this.selectLabel.text(opt.label); } if (opt.options) { + if (this.optionExpandButton) { + this.optionExpandButton.hide(); + } if (this.optionSelectTrigger) { this.optionSelectTrigger.show(); this.elementDiv.hide(); @@ -430,6 +452,16 @@ } this.elementDiv.show(); } + if (opt.expand && typeof opt.expand === 'function') { + this.optionExpandButton.show(); + this.optionExpandButton.off('click'); + this.optionExpandButton.on('click',function(evt) { + evt.preventDefault(); + opt.expand.call(that); + }) + } else { + this.optionExpandButton.hide(); + } this.element.trigger('change',this.propertyType,this.value()); } if (image) { diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index c689352b0..cb821d731 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -494,12 +494,13 @@ RED.editor = (function() { } function getEditStackTitle() { - var title = '
    '; for (var i=0;i').appendTo(trayBody); + dialogForm.html($("script[data-template-name='"+type+"']").html()); + ns = ns||"node-red"; + dialogForm.find('[data-i18n]').each(function() { + var current = $(this).attr("data-i18n"); + var keys = current.split(";"); + for (var i=0;i').prependTo(dialogForm); + dialogForm.submit(function(e) { e.preventDefault();}); + return dialogForm; + } + function showEditDialog(node) { var editing_node = node; editStack.push(node); @@ -763,33 +791,13 @@ RED.editor = (function() { if (editing_node) { RED.sidebar.info.refresh(editing_node); } - var trayBody = tray.find('.editor-tray-body'); - var dialogForm = $('
    ').appendTo(trayBody); - dialogForm.html($("script[data-template-name='"+type+"']").html()); var ns; if (node._def.set.module === "node-red") { ns = "node-red"; } else { ns = node._def.set.id; } - dialogForm.find('[data-i18n]').each(function() { - var current = $(this).attr("data-i18n"); - var keys = current.split(";"); - for (var i=0;i').prependTo(dialogForm); + var dialogForm = buildEditForm(tray,"dialog-form",type,ns); prepareEditDialog(node,node._def,"node-input"); dialogForm.i18n(); }, @@ -882,7 +890,6 @@ RED.editor = (function() { }, open: function(tray) { var trayHeader = tray.find(".editor-tray-header"); - var trayBody = tray.find(".editor-tray-body"); var trayFooter = tray.find(".editor-tray-footer"); if (node_def.hasUsers !== false) { @@ -890,21 +897,8 @@ RED.editor = (function() { } trayFooter.append(''); - var dialogForm = $('
    ').appendTo(trayBody); - dialogForm.html($("script[data-template-name='"+type+"']").html()); - dialogForm.find('[data-i18n]').each(function() { - var current = $(this).attr("data-i18n"); - if (current.indexOf(":") === -1) { - var prefix = ""; - if (current.indexOf("[")===0) { - var parts = current.split("]"); - prefix = parts[0]+"]"; - current = parts[1]; - } - $(this).attr("data-i18n",prefix+ns+":"+current); - } - }); - $('').prependTo(dialogForm); + var dialogForm = buildEditForm(tray,"node-config-dialog-edit-form",type,ns); + prepareEditDialog(editing_config_node,node_def,"node-config-input"); if (editing_config_node._def.exclusive) { $("#node-config-dialog-scope").hide(); @@ -1338,30 +1332,7 @@ RED.editor = (function() { if (editing_node) { RED.sidebar.info.refresh(editing_node); } - var trayBody = tray.find('.editor-tray-body'); - var dialogForm = $('
    ').appendTo(trayBody); - dialogForm.html($("script[data-template-name='subflow-template']").html()); - var ns = "node-red"; - dialogForm.find('[data-i18n]').each(function() { - var current = $(this).attr("data-i18n"); - var keys = current.split(";"); - for (var i=0;i').prependTo(dialogForm); - - dialogForm.submit(function(e) { e.preventDefault();}); + var dialogForm = buildEditForm(tray,"dialog-form","subflow-template"); subflowEditor = RED.editor.createEditor({ id: 'subflow-input-info-editor', mode: 'ace/mode/markdown', @@ -1397,6 +1368,78 @@ RED.editor = (function() { RED.tray.show(trayOptions); } + + function editExpression(options) { + var value = options.value; + var onComplete = options.complete; + var type = "_expression" + editStack.push({type:type}); + RED.view.state(RED.state.EDITING); + var expressionEditor; + + var trayOptions = { + title: getEditStackTitle(), + buttons: [ + { + id: "node-dialog-cancel", + text: RED._("common.label.cancel"), + click: function() { + RED.tray.close(); + } + }, + { + id: "node-dialog-ok", + text: RED._("common.label.done"), + class: "primary", + click: function() { + onComplete(expressionEditor.getValue()); + RED.tray.close(); + } + } + ], + resize: function(dimensions) { + editTrayWidthCache[type] = dimensions.width; + + var rows = $("#dialog-form>div:not(.node-text-editor-row)"); + var editorRow = $("#dialog-form>div.node-text-editor-row"); + var height = $("#dialog-form").height(); + for (var i=0;i
+ + + diff --git a/editor/vendor/jsonata/mode-jsonata.js b/editor/vendor/jsonata/mode-jsonata.js new file mode 100644 index 000000000..24562c536 --- /dev/null +++ b/editor/vendor/jsonata/mode-jsonata.js @@ -0,0 +1,129 @@ +define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules","ace/worker/worker_client","ace/mode/text"], function(require, exports, module) { + "use strict"; + + var oop = require("../lib/oop"); + var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + var WorkerClient = require("../worker/worker_client").WorkerClient; + + var JSONataHighlightRules = function() { + + var keywordMapper = this.createKeywordMapper({ + "keyword.operator": + "and|or|in", + "constant.language": + "null|Infinity|NaN|undefined", + "storage.type": + "function", + "keyword": + "$sum|$count|$max|$min|$average|$string|$substring|$substringBefore|"+ + "$substringAfter|$lowercase|$uppercase|$length|$split|$join|$number|"+ + "$boolean|$not|$map|$reduce|$keys|$lookup|$append|$exists|$spread" + }, "identifier"); + this.$rules = { + "start" : [ + { + token : "string", + regex : "'(?=.)", + next : "qstring" + }, + { + token : "string", + regex : '"(?=.)', + next : "qqstring" + }, + { + token : "constant.numeric", // hex + regex : /0(?:[xX][0-9a-fA-F]+|[bB][01]+)\b/ + }, + { + token : "constant.numeric", // float + regex : /[+-]?\d[\d_]*(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/ + }, + { + token : keywordMapper, + regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*" + }, + { token: "keyword", + regex: /λ/ + }, + { + token: "constant.language.boolean", + regex: "true|false" + }, + { + token : "punctuation.operator", + regex : /[.](?![.])/ + }, + { + token : "keyword.operator", + regex : /\|\||<=|>=|\.\.|\*\*|!=|:=|[=<>`!$%&*+\-~\/^]/, + next : "start" + }, + { + token : "punctuation.operator", + regex : /[?:,;.]/, + next : "start" + }, + { + token : "paren.lparen", + regex : /[\[({]/, + next : "start" + }, + { + token : "paren.rparen", + regex : /[\])}]/ + } + ], + "qqstring" : [ + { + token : "string", + regex : '"|$', + next : "start" + }, { + defaultToken: "string" + } + ], + "qstring" : [ + { + token : "string", + regex : "'|$", + next : "start" + }, { + defaultToken: "string" + } + ] + }; + }; + + oop.inherits(JSONataHighlightRules, TextHighlightRules); + + var TextMode = require("./text").Mode; + var Mode = function() { + this.HighlightRules = JSONataHighlightRules; + }; + oop.inherits(Mode, TextMode); + + + (function() { + this.createWorker = function(session) { + var worker = new WorkerClient(["ace"], "ace/mode/jsonata_worker", "JSONataWorker"); + worker.attachToDocument(session.getDocument()); + + worker.on("annotate", function(e) { + session.setAnnotations(e.data); + }); + + worker.on("terminate", function() { + session.clearAnnotations(); + }); + + return worker; + }; + + + this.$id = "ace/mode/jsonata"; + }).call(Mode.prototype); + + exports.Mode = Mode; + +}); diff --git a/editor/vendor/jsonata/worker-jsonata.js b/editor/vendor/jsonata/worker-jsonata.js new file mode 100644 index 000000000..b6803d3af --- /dev/null +++ b/editor/vendor/jsonata/worker-jsonata.js @@ -0,0 +1,4233 @@ +"no use strict"; +;(function(window) { +if (typeof window.window != "undefined" && window.document) + return; +if (window.require && window.define) + return; + +if (!window.console) { + window.console = function() { + var msgs = Array.prototype.slice.call(arguments, 0); + postMessage({type: "log", data: msgs}); + }; + window.console.error = + window.console.warn = + window.console.log = + window.console.trace = window.console; +} +window.window = window; +window.ace = window; + +window.onerror = function(message, file, line, col, err) { + postMessage({type: "error", data: { + message: message, + data: err.data, + file: file, + line: line, + col: col, + stack: err.stack + }}); +}; + +window.normalizeModule = function(parentId, moduleName) { + // normalize plugin requires + if (moduleName.indexOf("!") !== -1) { + var chunks = moduleName.split("!"); + return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1]); + } + // normalize relative requires + if (moduleName.charAt(0) == ".") { + var base = parentId.split("/").slice(0, -1).join("/"); + moduleName = (base ? base + "/" : "") + moduleName; + + while (moduleName.indexOf(".") !== -1 && previous != moduleName) { + var previous = moduleName; + moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, ""); + } + } + + return moduleName; +}; + +window.require = function require(parentId, id) { + if (!id) { + id = parentId; + parentId = null; + } + if (!id.charAt) + throw new Error("worker.js require() accepts only (parentId, id) as arguments"); + + id = window.normalizeModule(parentId, id); + + var module = window.require.modules[id]; + if (module) { + if (!module.initialized) { + module.initialized = true; + module.exports = module.factory().exports; + } + return module.exports; + } + + if (!window.require.tlns) + return console.log("unable to load " + id); + + var path = resolveModuleId(id, window.require.tlns); + if (path.slice(-3) != ".js") path += ".js"; + + window.require.id = id; + window.require.modules[id] = {}; // prevent infinite loop on broken modules + importScripts(path); + return window.require(parentId, id); +}; +function resolveModuleId(id, paths) { + var testPath = id, tail = ""; + while (testPath) { + var alias = paths[testPath]; + if (typeof alias == "string") { + return alias + tail; + } else if (alias) { + return alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name); + } else if (alias === false) { + return ""; + } + var i = testPath.lastIndexOf("/"); + if (i === -1) break; + tail = testPath.substr(i) + tail; + testPath = testPath.slice(0, i); + } + return id; +} +window.require.modules = {}; +window.require.tlns = {}; + +window.define = function(id, deps, factory) { + if (arguments.length == 2) { + factory = deps; + if (typeof id != "string") { + deps = id; + id = window.require.id; + } + } else if (arguments.length == 1) { + factory = id; + deps = []; + id = window.require.id; + } + + if (typeof factory != "function") { + window.require.modules[id] = { + exports: factory, + initialized: true + }; + return; + } + + if (!deps.length) + // If there is no dependencies, we inject "require", "exports" and + // "module" as dependencies, to provide CommonJS compatibility. + deps = ["require", "exports", "module"]; + + var req = function(childId) { + return window.require(id, childId); + }; + + window.require.modules[id] = { + exports: {}, + factory: function() { + var module = this; + var returnExports = factory.apply(this, deps.map(function(dep) { + switch (dep) { + // Because "require", "exports" and "module" aren't actual + // dependencies, we must handle them seperately. + case "require": return req; + case "exports": return module.exports; + case "module": return module; + // But for all other dependencies, we can just go ahead and + // require them. + default: return req(dep); + } + })); + if (returnExports) + module.exports = returnExports; + return module; + } + }; +}; +window.define.amd = {}; +require.tlns = {}; +window.initBaseUrls = function initBaseUrls(topLevelNamespaces) { + for (var i in topLevelNamespaces) + require.tlns[i] = topLevelNamespaces[i]; +}; + +window.initSender = function initSender() { + + var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter; + var oop = window.require("ace/lib/oop"); + + var Sender = function() {}; + + (function() { + + oop.implement(this, EventEmitter); + + this.callback = function(data, callbackId) { + postMessage({ + type: "call", + id: callbackId, + data: data + }); + }; + + this.emit = function(name, data) { + postMessage({ + type: "event", + name: name, + data: data + }); + }; + + }).call(Sender.prototype); + + return new Sender(); +}; + +var main = window.main = null; +var sender = window.sender = null; + +window.onmessage = function(e) { + var msg = e.data; + if (msg.event && sender) { + sender._signal(msg.event, msg.data); + } + else if (msg.command) { + if (main[msg.command]) + main[msg.command].apply(main, msg.args); + else if (window[msg.command]) + window[msg.command].apply(window, msg.args); + else + throw new Error("Unknown command:" + msg.command); + } + else if (msg.init) { + window.initBaseUrls(msg.tlns); + require("ace/lib/es5-shim"); + sender = window.sender = window.initSender(); + var clazz = require(msg.module)[msg.classname]; + main = window.main = new clazz(sender); + } +}; +})(this); + +define("ace/lib/oop",["require","exports","module"], function(require, exports, module) { +"use strict"; + +exports.inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +exports.mixin = function(obj, mixin) { + for (var key in mixin) { + obj[key] = mixin[key]; + } + return obj; +}; + +exports.implement = function(proto, mixin) { + exports.mixin(proto, mixin); +}; + +}); + +define("ace/range",["require","exports","module"], function(require, exports, module) { +"use strict"; +var comparePoints = function(p1, p2) { + return p1.row - p2.row || p1.column - p2.column; +}; +var Range = function(startRow, startColumn, endRow, endColumn) { + this.start = { + row: startRow, + column: startColumn + }; + + this.end = { + row: endRow, + column: endColumn + }; +}; + +(function() { + this.isEqual = function(range) { + return this.start.row === range.start.row && + this.end.row === range.end.row && + this.start.column === range.start.column && + this.end.column === range.end.column; + }; + this.toString = function() { + return ("Range: [" + this.start.row + "/" + this.start.column + + "] -> [" + this.end.row + "/" + this.end.column + "]"); + }; + + this.contains = function(row, column) { + return this.compare(row, column) == 0; + }; + this.compareRange = function(range) { + var cmp, + end = range.end, + start = range.start; + + cmp = this.compare(end.row, end.column); + if (cmp == 1) { + cmp = this.compare(start.row, start.column); + if (cmp == 1) { + return 2; + } else if (cmp == 0) { + return 1; + } else { + return 0; + } + } else if (cmp == -1) { + return -2; + } else { + cmp = this.compare(start.row, start.column); + if (cmp == -1) { + return -1; + } else if (cmp == 1) { + return 42; + } else { + return 0; + } + } + }; + this.comparePoint = function(p) { + return this.compare(p.row, p.column); + }; + this.containsRange = function(range) { + return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0; + }; + this.intersects = function(range) { + var cmp = this.compareRange(range); + return (cmp == -1 || cmp == 0 || cmp == 1); + }; + this.isEnd = function(row, column) { + return this.end.row == row && this.end.column == column; + }; + this.isStart = function(row, column) { + return this.start.row == row && this.start.column == column; + }; + this.setStart = function(row, column) { + if (typeof row == "object") { + this.start.column = row.column; + this.start.row = row.row; + } else { + this.start.row = row; + this.start.column = column; + } + }; + this.setEnd = function(row, column) { + if (typeof row == "object") { + this.end.column = row.column; + this.end.row = row.row; + } else { + this.end.row = row; + this.end.column = column; + } + }; + this.inside = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column) || this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.insideStart = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.insideEnd = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.compare = function(row, column) { + if (!this.isMultiLine()) { + if (row === this.start.row) { + return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0); + } + } + + if (row < this.start.row) + return -1; + + if (row > this.end.row) + return 1; + + if (this.start.row === row) + return column >= this.start.column ? 0 : -1; + + if (this.end.row === row) + return column <= this.end.column ? 0 : 1; + + return 0; + }; + this.compareStart = function(row, column) { + if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + }; + this.compareEnd = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else { + return this.compare(row, column); + } + }; + this.compareInside = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + }; + this.clipRows = function(firstRow, lastRow) { + if (this.end.row > lastRow) + var end = {row: lastRow + 1, column: 0}; + else if (this.end.row < firstRow) + var end = {row: firstRow, column: 0}; + + if (this.start.row > lastRow) + var start = {row: lastRow + 1, column: 0}; + else if (this.start.row < firstRow) + var start = {row: firstRow, column: 0}; + + return Range.fromPoints(start || this.start, end || this.end); + }; + this.extend = function(row, column) { + var cmp = this.compare(row, column); + + if (cmp == 0) + return this; + else if (cmp == -1) + var start = {row: row, column: column}; + else + var end = {row: row, column: column}; + + return Range.fromPoints(start || this.start, end || this.end); + }; + + this.isEmpty = function() { + return (this.start.row === this.end.row && this.start.column === this.end.column); + }; + this.isMultiLine = function() { + return (this.start.row !== this.end.row); + }; + this.clone = function() { + return Range.fromPoints(this.start, this.end); + }; + this.collapseRows = function() { + if (this.end.column == 0) + return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0) + else + return new Range(this.start.row, 0, this.end.row, 0) + }; + this.toScreenRange = function(session) { + var screenPosStart = session.documentToScreenPosition(this.start); + var screenPosEnd = session.documentToScreenPosition(this.end); + + return new Range( + screenPosStart.row, screenPosStart.column, + screenPosEnd.row, screenPosEnd.column + ); + }; + this.moveBy = function(row, column) { + this.start.row += row; + this.start.column += column; + this.end.row += row; + this.end.column += column; + }; + +}).call(Range.prototype); +Range.fromPoints = function(start, end) { + return new Range(start.row, start.column, end.row, end.column); +}; +Range.comparePoints = comparePoints; + +Range.comparePoints = function(p1, p2) { + return p1.row - p2.row || p1.column - p2.column; +}; + + +exports.Range = Range; +}); + +define("ace/apply_delta",["require","exports","module"], function(require, exports, module) { +"use strict"; + +function throwDeltaError(delta, errorText){ + console.log("Invalid Delta:", delta); + throw "Invalid Delta: " + errorText; +} + +function positionInDocument(docLines, position) { + return position.row >= 0 && position.row < docLines.length && + position.column >= 0 && position.column <= docLines[position.row].length; +} + +function validateDelta(docLines, delta) { + if (delta.action != "insert" && delta.action != "remove") + throwDeltaError(delta, "delta.action must be 'insert' or 'remove'"); + if (!(delta.lines instanceof Array)) + throwDeltaError(delta, "delta.lines must be an Array"); + if (!delta.start || !delta.end) + throwDeltaError(delta, "delta.start/end must be an present"); + var start = delta.start; + if (!positionInDocument(docLines, delta.start)) + throwDeltaError(delta, "delta.start must be contained in document"); + var end = delta.end; + if (delta.action == "remove" && !positionInDocument(docLines, end)) + throwDeltaError(delta, "delta.end must contained in document for 'remove' actions"); + var numRangeRows = end.row - start.row; + var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0)); + if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars) + throwDeltaError(delta, "delta.range must match delta lines"); +} + +exports.applyDelta = function(docLines, delta, doNotValidate) { + + var row = delta.start.row; + var startColumn = delta.start.column; + var line = docLines[row] || ""; + switch (delta.action) { + case "insert": + var lines = delta.lines; + if (lines.length === 1) { + docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn); + } else { + var args = [row, 1].concat(delta.lines); + docLines.splice.apply(docLines, args); + docLines[row] = line.substring(0, startColumn) + docLines[row]; + docLines[row + delta.lines.length - 1] += line.substring(startColumn); + } + break; + case "remove": + var endColumn = delta.end.column; + var endRow = delta.end.row; + if (row === endRow) { + docLines[row] = line.substring(0, startColumn) + line.substring(endColumn); + } else { + docLines.splice( + row, endRow - row + 1, + line.substring(0, startColumn) + docLines[endRow].substring(endColumn) + ); + } + break; + } +} +}); + +define("ace/lib/event_emitter",["require","exports","module"], function(require, exports, module) { +"use strict"; + +var EventEmitter = {}; +var stopPropagation = function() { this.propagationStopped = true; }; +var preventDefault = function() { this.defaultPrevented = true; }; + +EventEmitter._emit = +EventEmitter._dispatchEvent = function(eventName, e) { + this._eventRegistry || (this._eventRegistry = {}); + this._defaultHandlers || (this._defaultHandlers = {}); + + var listeners = this._eventRegistry[eventName] || []; + var defaultHandler = this._defaultHandlers[eventName]; + if (!listeners.length && !defaultHandler) + return; + + if (typeof e != "object" || !e) + e = {}; + + if (!e.type) + e.type = eventName; + if (!e.stopPropagation) + e.stopPropagation = stopPropagation; + if (!e.preventDefault) + e.preventDefault = preventDefault; + + listeners = listeners.slice(); + for (var i=0; i this.row) + return; + + var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight); + this.setPosition(point.row, point.column, true); + }; + + function $pointsInOrder(point1, point2, equalPointsInOrder) { + var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; + return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter); + } + + function $getTransformedPoint(delta, point, moveIfEqual) { + var deltaIsInsert = delta.action == "insert"; + var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row); + var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column); + var deltaStart = delta.start; + var deltaEnd = deltaIsInsert ? deltaStart : delta.end; // Collapse insert range. + if ($pointsInOrder(point, deltaStart, moveIfEqual)) { + return { + row: point.row, + column: point.column + }; + } + if ($pointsInOrder(deltaEnd, point, !moveIfEqual)) { + return { + row: point.row + deltaRowShift, + column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) + }; + } + + return { + row: deltaStart.row, + column: deltaStart.column + }; + } + this.setPosition = function(row, column, noClip) { + var pos; + if (noClip) { + pos = { + row: row, + column: column + }; + } else { + pos = this.$clipPositionToDocument(row, column); + } + + if (this.row == pos.row && this.column == pos.column) + return; + + var old = { + row: this.row, + column: this.column + }; + + this.row = pos.row; + this.column = pos.column; + this._signal("change", { + old: old, + value: pos + }); + }; + this.detach = function() { + this.document.removeEventListener("change", this.$onChange); + }; + this.attach = function(doc) { + this.document = doc || this.document; + this.document.on("change", this.$onChange); + }; + this.$clipPositionToDocument = function(row, column) { + var pos = {}; + + if (row >= this.document.getLength()) { + pos.row = Math.max(0, this.document.getLength() - 1); + pos.column = this.document.getLine(pos.row).length; + } + else if (row < 0) { + pos.row = 0; + pos.column = 0; + } + else { + pos.row = row; + pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column)); + } + + if (column < 0) + pos.column = 0; + + return pos; + }; + +}).call(Anchor.prototype); + +}); + +define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var applyDelta = require("./apply_delta").applyDelta; +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var Range = require("./range").Range; +var Anchor = require("./anchor").Anchor; + +var Document = function(textOrLines) { + this.$lines = [""]; + if (textOrLines.length === 0) { + this.$lines = [""]; + } else if (Array.isArray(textOrLines)) { + this.insertMergedLines({row: 0, column: 0}, textOrLines); + } else { + this.insert({row: 0, column:0}, textOrLines); + } +}; + +(function() { + + oop.implement(this, EventEmitter); + this.setValue = function(text) { + var len = this.getLength() - 1; + this.remove(new Range(0, 0, len, this.getLine(len).length)); + this.insert({row: 0, column: 0}, text); + }; + this.getValue = function() { + return this.getAllLines().join(this.getNewLineCharacter()); + }; + this.createAnchor = function(row, column) { + return new Anchor(this, row, column); + }; + if ("aaa".split(/a/).length === 0) { + this.$split = function(text) { + return text.replace(/\r\n|\r/g, "\n").split("\n"); + }; + } else { + this.$split = function(text) { + return text.split(/\r\n|\r|\n/); + }; + } + + + this.$detectNewLine = function(text) { + var match = text.match(/^.*?(\r\n|\r|\n)/m); + this.$autoNewLine = match ? match[1] : "\n"; + this._signal("changeNewLineMode"); + }; + this.getNewLineCharacter = function() { + switch (this.$newLineMode) { + case "windows": + return "\r\n"; + case "unix": + return "\n"; + default: + return this.$autoNewLine || "\n"; + } + }; + + this.$autoNewLine = ""; + this.$newLineMode = "auto"; + this.setNewLineMode = function(newLineMode) { + if (this.$newLineMode === newLineMode) + return; + + this.$newLineMode = newLineMode; + this._signal("changeNewLineMode"); + }; + this.getNewLineMode = function() { + return this.$newLineMode; + }; + this.isNewLine = function(text) { + return (text == "\r\n" || text == "\r" || text == "\n"); + }; + this.getLine = function(row) { + return this.$lines[row] || ""; + }; + this.getLines = function(firstRow, lastRow) { + return this.$lines.slice(firstRow, lastRow + 1); + }; + this.getAllLines = function() { + return this.getLines(0, this.getLength()); + }; + this.getLength = function() { + return this.$lines.length; + }; + this.getTextRange = function(range) { + return this.getLinesForRange(range).join(this.getNewLineCharacter()); + }; + this.getLinesForRange = function(range) { + var lines; + if (range.start.row === range.end.row) { + lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)]; + } else { + lines = this.getLines(range.start.row, range.end.row); + lines[0] = (lines[0] || "").substring(range.start.column); + var l = lines.length - 1; + if (range.end.row - range.start.row == l) + lines[l] = lines[l].substring(0, range.end.column); + } + return lines; + }; + this.insertLines = function(row, lines) { + console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."); + return this.insertFullLines(row, lines); + }; + this.removeLines = function(firstRow, lastRow) { + console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."); + return this.removeFullLines(firstRow, lastRow); + }; + this.insertNewLine = function(position) { + console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."); + return this.insertMergedLines(position, ["", ""]); + }; + this.insert = function(position, text) { + if (this.getLength() <= 1) + this.$detectNewLine(text); + + return this.insertMergedLines(position, this.$split(text)); + }; + this.insertInLine = function(position, text) { + var start = this.clippedPos(position.row, position.column); + var end = this.pos(position.row, position.column + text.length); + + this.applyDelta({ + start: start, + end: end, + action: "insert", + lines: [text] + }, true); + + return this.clonePos(end); + }; + + this.clippedPos = function(row, column) { + var length = this.getLength(); + if (row === undefined) { + row = length; + } else if (row < 0) { + row = 0; + } else if (row >= length) { + row = length - 1; + column = undefined; + } + var line = this.getLine(row); + if (column == undefined) + column = line.length; + column = Math.min(Math.max(column, 0), line.length); + return {row: row, column: column}; + }; + + this.clonePos = function(pos) { + return {row: pos.row, column: pos.column}; + }; + + this.pos = function(row, column) { + return {row: row, column: column}; + }; + + this.$clipPosition = function(position) { + var length = this.getLength(); + if (position.row >= length) { + position.row = Math.max(0, length - 1); + position.column = this.getLine(length - 1).length; + } else { + position.row = Math.max(0, position.row); + position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length); + } + return position; + }; + this.insertFullLines = function(row, lines) { + row = Math.min(Math.max(row, 0), this.getLength()); + var column = 0; + if (row < this.getLength()) { + lines = lines.concat([""]); + column = 0; + } else { + lines = [""].concat(lines); + row--; + column = this.$lines[row].length; + } + this.insertMergedLines({row: row, column: column}, lines); + }; + this.insertMergedLines = function(position, lines) { + var start = this.clippedPos(position.row, position.column); + var end = { + row: start.row + lines.length - 1, + column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length + }; + + this.applyDelta({ + start: start, + end: end, + action: "insert", + lines: lines + }); + + return this.clonePos(end); + }; + this.remove = function(range) { + var start = this.clippedPos(range.start.row, range.start.column); + var end = this.clippedPos(range.end.row, range.end.column); + this.applyDelta({ + start: start, + end: end, + action: "remove", + lines: this.getLinesForRange({start: start, end: end}) + }); + return this.clonePos(start); + }; + this.removeInLine = function(row, startColumn, endColumn) { + var start = this.clippedPos(row, startColumn); + var end = this.clippedPos(row, endColumn); + + this.applyDelta({ + start: start, + end: end, + action: "remove", + lines: this.getLinesForRange({start: start, end: end}) + }, true); + + return this.clonePos(start); + }; + this.removeFullLines = function(firstRow, lastRow) { + firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1); + lastRow = Math.min(Math.max(0, lastRow ), this.getLength() - 1); + var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0; + var deleteLastNewLine = lastRow < this.getLength() - 1; + var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow ); + var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 ); + var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow ); + var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); + var range = new Range(startRow, startCol, endRow, endCol); + var deletedLines = this.$lines.slice(firstRow, lastRow + 1); + + this.applyDelta({ + start: range.start, + end: range.end, + action: "remove", + lines: this.getLinesForRange(range) + }); + return deletedLines; + }; + this.removeNewLine = function(row) { + if (row < this.getLength() - 1 && row >= 0) { + this.applyDelta({ + start: this.pos(row, this.getLine(row).length), + end: this.pos(row + 1, 0), + action: "remove", + lines: ["", ""] + }); + } + }; + this.replace = function(range, text) { + if (!(range instanceof Range)) + range = Range.fromPoints(range.start, range.end); + if (text.length === 0 && range.isEmpty()) + return range.start; + if (text == this.getTextRange(range)) + return range.end; + + this.remove(range); + var end; + if (text) { + end = this.insert(range.start, text); + } + else { + end = range.start; + } + + return end; + }; + this.applyDeltas = function(deltas) { + for (var i=0; i=0; i--) { + this.revertDelta(deltas[i]); + } + }; + this.applyDelta = function(delta, doNotValidate) { + var isInsert = delta.action == "insert"; + if (isInsert ? delta.lines.length <= 1 && !delta.lines[0] + : !Range.comparePoints(delta.start, delta.end)) { + return; + } + + if (isInsert && delta.lines.length > 20000) + this.$splitAndapplyLargeDelta(delta, 20000); + applyDelta(this.$lines, delta, doNotValidate); + this._signal("change", delta); + }; + + this.$splitAndapplyLargeDelta = function(delta, MAX) { + var lines = delta.lines; + var l = lines.length; + var row = delta.start.row; + var column = delta.start.column; + var from = 0, to = 0; + do { + from = to; + to += MAX - 1; + var chunk = lines.slice(from, to); + if (to > l) { + delta.lines = chunk; + delta.start.row = row + from; + delta.start.column = column; + break; + } + chunk.push(""); + this.applyDelta({ + start: this.pos(row + from, column), + end: this.pos(row + to, column = 0), + action: delta.action, + lines: chunk + }, true); + } while(true); + }; + this.revertDelta = function(delta) { + this.applyDelta({ + start: this.clonePos(delta.start), + end: this.clonePos(delta.end), + action: (delta.action == "insert" ? "remove" : "insert"), + lines: delta.lines.slice() + }); + }; + this.indexToPosition = function(index, startRow) { + var lines = this.$lines || this.getAllLines(); + var newlineLength = this.getNewLineCharacter().length; + for (var i = startRow || 0, l = lines.length; i < l; i++) { + index -= lines[i].length + newlineLength; + if (index < 0) + return {row: i, column: index + lines[i].length + newlineLength}; + } + return {row: l-1, column: lines[l-1].length}; + }; + this.positionToIndex = function(pos, startRow) { + var lines = this.$lines || this.getAllLines(); + var newlineLength = this.getNewLineCharacter().length; + var index = 0; + var row = Math.min(pos.row, lines.length); + for (var i = startRow || 0; i < row; ++i) + index += lines[i].length + newlineLength; + + return index + pos.column; + }; + +}).call(Document.prototype); + +exports.Document = Document; +}); + +define("ace/lib/lang",["require","exports","module"], function(require, exports, module) { +"use strict"; + +exports.last = function(a) { + return a[a.length - 1]; +}; + +exports.stringReverse = function(string) { + return string.split("").reverse().join(""); +}; + +exports.stringRepeat = function (string, count) { + var result = ''; + while (count > 0) { + if (count & 1) + result += string; + + if (count >>= 1) + string += string; + } + return result; +}; + +var trimBeginRegexp = /^\s\s*/; +var trimEndRegexp = /\s\s*$/; + +exports.stringTrimLeft = function (string) { + return string.replace(trimBeginRegexp, ''); +}; + +exports.stringTrimRight = function (string) { + return string.replace(trimEndRegexp, ''); +}; + +exports.copyObject = function(obj) { + var copy = {}; + for (var key in obj) { + copy[key] = obj[key]; + } + return copy; +}; + +exports.copyArray = function(array){ + var copy = []; + for (var i=0, l=array.length; i': 40, + '`': 80, + '**': 60, + '..': 20, + ':=': 10, + '!=': 40, + '<=': 40, + '>=': 40, + 'and': 30, + 'or': 25, + 'in': 40, + '&': 50, + '!': 0 // not an operator, but needed as a stop character for name tokens + }; + + var escapes = { // JSON string escape sequences - see json.org + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }; + var tokenizer = function (path) { + var position = 0; + var length = path.length; + + var create = function (type, value) { + var obj = {type: type, value: value, position: position}; + return obj; + }; + + var next = function () { + if (position >= length) return null; + var currentChar = path.charAt(position); + while (position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { + position++; + currentChar = path.charAt(position); + } + if (currentChar === '.' && path.charAt(position + 1) === '.') { + position += 2; + return create('operator', '..'); + } + if (currentChar === ':' && path.charAt(position + 1) === '=') { + position += 2; + return create('operator', ':='); + } + if (currentChar === '!' && path.charAt(position + 1) === '=') { + position += 2; + return create('operator', '!='); + } + if (currentChar === '>' && path.charAt(position + 1) === '=') { + position += 2; + return create('operator', '>='); + } + if (currentChar === '<' && path.charAt(position + 1) === '=') { + position += 2; + return create('operator', '<='); + } + if (currentChar === '*' && path.charAt(position + 1) === '*') { + position += 2; + return create('operator', '**'); + } + if (operators.hasOwnProperty(currentChar)) { + position++; + return create('operator', currentChar); + } + if (currentChar === '"' || currentChar === "'") { + var quoteType = currentChar; + position++; + var qstr = ""; + while (position < length) { + currentChar = path.charAt(position); + if (currentChar === '\\') { // escape sequence + position++; + currentChar = path.charAt(position); + if (escapes.hasOwnProperty(currentChar)) { + qstr += escapes[currentChar]; + } else if (currentChar === 'u') { + var octets = path.substr(position + 1, 4); + if (/^[0-9a-fA-F]+$/.test(octets)) { + var codepoint = parseInt(octets, 16); + qstr += String.fromCharCode(codepoint); + position += 4; + } else { + throw { + message: "The escape sequence \\u must be followed by 4 hex digits at column " + position, + stack: (new Error()).stack, + position: position + }; + } + } else { + throw { + message: 'unsupported escape sequence: \\' + currentChar + ' at column ' + position, + stack: (new Error()).stack, + position: position, + token: currentChar + }; + + } + } else if (currentChar === quoteType) { + position++; + return create('string', qstr); + } else { + qstr += currentChar; + } + position++; + } + throw { + message: 'no terminating quote found in string literal at column ' + position, + stack: (new Error()).stack, + position: position + }; + } + var numregex = /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?/; + var match = numregex.exec(path.substring(position)); + if (match !== null) { + var num = parseFloat(match[0]); + if (!isNaN(num) && isFinite(num)) { + position += match[0].length; + return create('number', num); + } else { + throw { + message: 'Number out of range: ' + match[0] + ' at column ' + position, + stack: (new Error()).stack, + position: position, + token: match[0] + }; + } + } + var i = position; + var ch; + var name; + for (;;) { + ch = path.charAt(i); + if (i == length || ' \t\n\r\v'.indexOf(ch) > -1 || operators.hasOwnProperty(ch)) { + if (path.charAt(position) === '$') { + name = path.substring(position + 1, i); + position = i; + return create('variable', name); + } else { + name = path.substring(position, i); + position = i; + switch (name) { + case 'and': + case 'or': + case 'in': + return create('operator', name); + case 'true': + return create('value', true); + case 'false': + return create('value', false); + case 'null': + return create('value', null); + default: + if (position == length && name === '') { + return null; + } + return create('name', name); + } + } + } else { + i++; + } + } + }; + + return next; + }; + + var parser = function (source) { + var node; + var lexer; + + var symbol_table = {}; + + var base_symbol = { + nud: function () { + return this; + } + }; + + var symbol = function (id, bp) { + var s = symbol_table[id]; + bp = bp || 0; + if (s) { + if (bp >= s.lbp) { + s.lbp = bp; + } + } else { + s = Object.create(base_symbol); + s.id = s.value = id; + s.lbp = bp; + symbol_table[id] = s; + } + return s; + }; + + var advance = function (id) { + if (id && node.id !== id) { + var msg; + if(node.id === '(end)') { + msg = "Syntax error: expected '" + id + "' before end of expression"; + } else { + msg = "Syntax error: expected '" + id + "', got '" + node.id + "' at column " + node.position; + } + throw { + message: msg , + stack: (new Error()).stack, + position: node.position, + token: node.id, + value: id + }; + } + var next_token = lexer(); + if (next_token === null) { + node = symbol_table["(end)"]; + return node; + } + var value = next_token.value; + var type = next_token.type; + var symbol; + switch (type) { + case 'name': + case 'variable': + symbol = symbol_table["(name)"]; + break; + case 'operator': + symbol = symbol_table[value]; + if (!symbol) { + throw { + message: "Unknown operator: " + value + " at column " + next_token.position, + stack: (new Error()).stack, + position: next_token.position, + token: value + }; + } + break; + case 'string': + case 'number': + case 'value': + type = "literal"; + symbol = symbol_table["(literal)"]; + break; + default: + throw { + message: "Unexpected token:" + value + " at column " + next_token.position, + stack: (new Error()).stack, + position: next_token.position, + token: value + }; + } + + node = Object.create(symbol); + node.value = value; + node.type = type; + node.position = next_token.position; + return node; + }; + var expression = function (rbp) { + var left; + var t = node; + advance(); + left = t.nud(); + while (rbp < node.lbp) { + t = node; + advance(); + left = t.led(left); + } + return left; + }; + var infix = function (id, bp, led) { + var bindingPower = bp || operators[id]; + var s = symbol(id, bindingPower); + s.led = led || function (left) { + this.lhs = left; + this.rhs = expression(bindingPower); + this.type = "binary"; + return this; + }; + return s; + }; + var infixr = function (id, bp, led) { + var bindingPower = bp || operators[id]; + var s = symbol(id, bindingPower); + s.led = led || function (left) { + this.lhs = left; + this.rhs = expression(bindingPower - 1); // subtract 1 from bindingPower for right associative operators + this.type = "binary"; + return this; + }; + return s; + }; + var prefix = function (id, nud) { + var s = symbol(id); + s.nud = nud || function () { + this.expression = expression(70); + this.type = "unary"; + return this; + }; + return s; + }; + + symbol("(end)"); + symbol("(name)"); + symbol("(literal)"); + symbol(":"); + symbol(";"); + symbol(","); + symbol(")"); + symbol("]"); + symbol("}"); + symbol(".."); // range operator + infix("."); // field reference + infix("+"); // numeric addition + infix("-"); // numeric subtraction + infix("*"); // numeric multiplication + infix("/"); // numeric division + infix("%"); // numeric modulus + infix("="); // equality + infix("<"); // less than + infix(">"); // greater than + infix("!="); // not equal to + infix("<="); // less than or equal + infix(">="); // greater than or equal + infix("&"); // string concatenation + infix("and"); // Boolean AND + infix("or"); // Boolean OR + infix("in"); // is member of array + infixr(":="); // bind variable + prefix("-"); // unary numeric negation + prefix('*', function () { + this.type = "wildcard"; + return this; + }); + prefix('**', function () { + this.type = "descendant"; + return this; + }); + infix("(", operators['('], function (left) { + this.procedure = left; + this.type = 'function'; + this.arguments = []; + if (node.id !== ')') { + for (;;) { + if (node.type === 'operator' && node.id === '?') { + this.type = 'partial'; + this.arguments.push(node); + advance('?'); + } else { + this.arguments.push(expression(0)); + } + if (node.id !== ',') break; + advance(','); + } + } + advance(")"); + if (left.type === 'name' && (left.value === 'function' || left.value === '\u03BB')) { + this.arguments.forEach(function (arg, index) { + if (arg.type !== 'variable') { + throw { + message: 'Parameter ' + (index + 1) + ' of function definition must be a variable name (start with $)', + stack: (new Error()).stack, + position: arg.position, + token: arg.value + }; + } + }); + this.type = 'lambda'; + advance('{'); + this.body = expression(0); + advance('}'); + } + return this; + }); + prefix("(", function () { + var expressions = []; + while (node.id !== ")") { + expressions.push(expression(0)); + if (node.id !== ";") { + break; + } + advance(";"); + } + advance(")"); + this.type = 'block'; + this.expressions = expressions; + return this; + }); + prefix("{", function () { + var a = []; + if (node.id !== "}") { + for (;;) { + var n = expression(0); + advance(":"); + var v = expression(0); + a.push([n, v]); // holds an array of name/value expression pairs + if (node.id !== ",") { + break; + } + advance(","); + } + } + advance("}"); + this.lhs = a; + this.type = "unary"; + return this; + }); + prefix("[", function () { + var a = []; + if (node.id !== "]") { + for (;;) { + var item = expression(0); + if (node.id === "..") { + var range = {type: "binary", value: "..", position: node.position, lhs: item}; + advance(".."); + range.rhs = expression(0); + item = range; + } + a.push(item); + if (node.id !== ",") { + break; + } + advance(","); + } + } + advance("]"); + this.lhs = a; + this.type = "unary"; + return this; + }); + infix("[", operators['['], function (left) { + this.lhs = left; + this.rhs = expression(operators[']']); + this.type = 'binary'; + advance("]"); + return this; + }); + infix("{", operators['{'], function (left) { + this.lhs = left; + this.rhs = expression(operators['}']); + this.type = 'binary'; + advance("}"); + return this; + }); + infix("?", operators['?'], function (left) { + this.type = 'condition'; + this.condition = left; + this.then = expression(0); + if (node.id === ':') { + advance(":"); + this.else = expression(0); + } + return this; + }); + var tail_call_optimize = function(expr) { + var result; + if(expr.type === 'function') { + var thunk = {type: 'lambda', thunk: true, arguments: [], position: expr.position}; + thunk.body = expr; + result = thunk; + } else if(expr.type === 'condition') { + expr.then = tail_call_optimize(expr.then); + expr.else = tail_call_optimize(expr.else); + result = expr; + } else if(expr.type === 'block') { + var length = expr.expressions.length; + if(length > 0) { + expr.expressions[length - 1] = tail_call_optimize(expr.expressions[length - 1]); + } + result = expr; + } else { + result = expr; + } + return result; + }; + var post_parse = function (expr) { + var result = []; + switch (expr.type) { + case 'binary': + switch (expr.value) { + case '.': + var step = post_parse(expr.lhs); + if (Array.isArray(step)) { + Array.prototype.push.apply(result, step); + } else { + result.push(step); + } + var rest = [post_parse(expr.rhs)]; + Array.prototype.push.apply(result, rest); + result.type = 'path'; + break; + case '[': + result = post_parse(expr.lhs); + if (typeof result.aggregate !== 'undefined') { + throw { + message: 'A predicate cannot follow an aggregate in a step. Error at column: ' + expr.position, + stack: (new Error()).stack, + position: expr.position + }; + } + if (typeof result.predicate === 'undefined') { + result.predicate = []; + } + result.predicate.push(post_parse(expr.rhs)); + break; + case '{': + result = post_parse(expr.lhs); + if (typeof result.aggregate !== 'undefined') { + throw { + message: 'Each step can only have one aggregator. Error at column: ' + expr.position, + stack: (new Error()).stack, + position: expr.position + }; + } + result.aggregate = post_parse(expr.rhs); + break; + default: + result = {type: expr.type, value: expr.value, position: expr.position}; + result.lhs = post_parse(expr.lhs); + result.rhs = post_parse(expr.rhs); + } + break; + case 'unary': + result = {type: expr.type, value: expr.value, position: expr.position}; + if (expr.value === '[') { + result.lhs = expr.lhs.map(function (item) { + return post_parse(item); + }); + } else if (expr.value === '{') { + result.lhs = expr.lhs.map(function (pair) { + return [post_parse(pair[0]), post_parse(pair[1])]; + }); + } else { + result.expression = post_parse(expr.expression); + if (expr.value === '-' && result.expression.type === 'literal' && isNumeric(result.expression.value)) { + result = result.expression; + result.value = -result.value; + } + } + break; + case 'function': + case 'partial': + result = {type: expr.type, name: expr.name, value: expr.value, position: expr.position}; + result.arguments = expr.arguments.map(function (arg) { + return post_parse(arg); + }); + result.procedure = post_parse(expr.procedure); + break; + case 'lambda': + result = {type: expr.type, arguments: expr.arguments, position: expr.position}; + var body = post_parse(expr.body); + result.body = tail_call_optimize(body); + break; + case 'condition': + result = {type: expr.type, position: expr.position}; + result.condition = post_parse(expr.condition); + result.then = post_parse(expr.then); + if (typeof expr.else !== 'undefined') { + result.else = post_parse(expr.else); + } + break; + case 'block': + result = {type: expr.type, position: expr.position}; + result.expressions = expr.expressions.map(function (item) { + return post_parse(item); + }); + break; + case 'name': + case 'literal': + case 'wildcard': + case 'descendant': + case 'variable': + result = expr; + break; + case 'operator': + if (expr.value === 'and' || expr.value === 'or' || expr.value === 'in') { + expr.type = 'name'; + result = post_parse(expr); + } else if (expr.value === '?') { + result = expr; + } else { + throw { + message: "Syntax error: " + expr.value + " at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value + }; + } + break; + default: + var reason = "Unknown expression type: " + expr.value + " at column " + expr.position; + if (expr.id === '(end)') { + reason = "Syntax error: unexpected end of expression"; + } + throw { + message: reason, + stack: (new Error()).stack, + position: expr.position, + token: expr.value + }; + } + return result; + }; + + lexer = tokenizer(source); + advance(); + var expr = expression(0); + if (node.id !== '(end)') { + throw { + message: "Syntax error: " + node.value + " at column " + node.position, + stack: (new Error()).stack, + position: node.position, + token: node.value + }; + } + expr = post_parse(expr); + if (expr.type === 'name') { + expr = [expr]; + expr.type = 'path'; + } + + return expr; + }; + function isNumeric(n) { + var isNum = false; + if(typeof n === 'number') { + var num = parseFloat(n); + isNum = !isNaN(num); + if (isNum && !isFinite(num)) { + throw { + message: "Number out of range", + value: n, + stack: (new Error()).stack + }; + } + } + return isNum; + } + function isArrayOfNumbers(arg) { + var result = false; + if(Array.isArray(arg)) { + result = (arg.filter(function(item){return !isNumeric(item);}).length == 0); + } + return result; + } + Number.isInteger = Number.isInteger || function(value) { + return typeof value === "number" && + isFinite(value) && + Math.floor(value) === value; + }; + function evaluate(expr, input, environment) { + var result; + + var entryCallback = environment.lookup('__evaluate_entry'); + if(entryCallback) { + entryCallback(expr, input, environment); + } + + switch (expr.type) { + case 'path': + result = evaluatePath(expr, input, environment); + break; + case 'binary': + result = evaluateBinary(expr, input, environment); + break; + case 'unary': + result = evaluateUnary(expr, input, environment); + break; + case 'name': + result = evaluateName(expr, input, environment); + break; + case 'literal': + result = evaluateLiteral(expr, input, environment); + break; + case 'wildcard': + result = evaluateWildcard(expr, input, environment); + break; + case 'descendant': + result = evaluateDescendants(expr, input, environment); + break; + case 'condition': + result = evaluateCondition(expr, input, environment); + break; + case 'block': + result = evaluateBlock(expr, input, environment); + break; + case 'function': + result = evaluateFunction(expr, input, environment); + break; + case 'variable': + result = evaluateVariable(expr, input, environment); + break; + case 'lambda': + result = evaluateLambda(expr, input, environment); + break; + case 'partial': + result = evaluatePartialApplication(expr, input, environment); + break; + } + if (expr.hasOwnProperty('predicate')) { + result = applyPredicates(expr.predicate, result, environment); + } + if (expr.hasOwnProperty('aggregate')) { + result = applyAggregate(expr.aggregate, result, environment); + } + + var exitCallback = environment.lookup('__evaluate_exit'); + if(exitCallback) { + exitCallback(expr, input, environment, result); + } + + return result; + } + function evaluatePath(expr, input, environment) { + var result; + var inputSequence; + if (expr[0].type === 'variable') { + expr[0].absolute = true; + } else if(expr[0].type === 'unary' && expr[0].value === '[') { + input = [null];// dummy singleton sequence for first step + } + expr.forEach(function (step) { + var resultSequence = []; + result = undefined; + if (step.absolute === true) { + inputSequence = [input]; // dummy singleton sequence for first (absolute) step + } else if (Array.isArray(input)) { + inputSequence = input; + } else { + inputSequence = [input]; + } + if (expr.length > 1 && step.type === 'literal') { + step.type = 'name'; + } + if (step.value === '{') { + if(typeof input !== 'undefined') { + result = evaluateGroupExpression(step, inputSequence, environment); + } + } else { + inputSequence.forEach(function (item) { + var res = evaluate(step, item, environment); + if (typeof res !== 'undefined') { + if (Array.isArray(res)) { + res.forEach(function (innerRes) { + if (typeof innerRes !== 'undefined') { + resultSequence.push(innerRes); + } + }); + } else { + resultSequence.push(res); + } + } + }); + if (resultSequence.length == 1) { + result = resultSequence[0]; + } else if (resultSequence.length > 1) { + result = resultSequence; + } + } + + input = result; + }); + return result; + } + function applyPredicates(predicates, input, environment) { + var result; + var inputSequence = input; + + var results = []; + predicates.forEach(function (predicate) { + if (!Array.isArray(inputSequence)) { + inputSequence = [inputSequence]; + } + results = []; + result = undefined; + if (predicate.type === 'literal' && isNumeric(predicate.value)) { + var index = predicate.value; + if (!Number.isInteger(index)) { + index = Math.floor(index); + } + if (index < 0) { + index = inputSequence.length + index; + } + result = inputSequence[index]; + } else { + inputSequence.forEach(function (item, index) { + var res = evaluate(predicate, item, environment); + if (isNumeric(res)) { + res = [res]; + } + if(isArrayOfNumbers(res)) { + res.forEach(function(ires) { + if (!Number.isInteger(ires)) { + ires = Math.floor(ires); + } + if (ires < 0) { + ires = inputSequence.length + ires; + } + if (ires == index) { + results.push(item); + } + }); + } else if (functionBoolean(res)) { // truthy + results.push(item); + } + }); + } + if (results.length == 1) { + result = results[0]; + } else if (results.length > 1) { + result = results; + } + inputSequence = result; + }); + return result; + } + function applyAggregate(expr, input, environment) { + var result = {}; + if (Array.isArray(input)) { + var aggEnv = createFrame(environment); + aggEnv.bind('_', input[0]); + for (var index = 1; index < input.length; index++) { + var reduce = evaluate(expr, input[index], aggEnv); + aggEnv.bind('_', reduce); + } + result = aggEnv.lookup('_'); + } else { + result = input; + } + return result; + } + function evaluateBinary(expr, input, environment) { + var result; + + switch (expr.value) { + case '+': + case '-': + case '*': + case '/': + case '%': + result = evaluateNumericExpression(expr, input, environment); + break; + case '=': + case '!=': + case '<': + case '<=': + case '>': + case '>=': + result = evaluateComparisonExpression(expr, input, environment); + break; + case '&': + result = evaluateStringConcat(expr, input, environment); + break; + case 'and': + case 'or': + result = evaluateBooleanExpression(expr, input, environment); + break; + case '..': + result = evaluateRangeExpression(expr, input, environment); + break; + case ':=': + result = evaluateBindExpression(expr, input, environment); + break; + case 'in': + result = evaluateIncludesExpression(expr, input, environment); + break; + } + return result; + } + function evaluateUnary(expr, input, environment) { + var result; + + switch (expr.value) { + case '-': + result = evaluate(expr.expression, input, environment); + if (isNumeric(result)) { + result = -result; + } else { + throw { + message: "Cannot negate a non-numeric value: " + result + " at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: result + }; + } + break; + case '[': + result = []; + expr.lhs.forEach(function (item) { + var value = evaluate(item, input, environment); + if (typeof value !== 'undefined') { + if (item.value === '..') { + result = functionAppend(result, value); + } else { + result.push(value); + } + } + }); + break; + case '{': + result = evaluateGroupExpression(expr, input, environment); + break; + + } + return result; + } + function evaluateName(expr, input, environment) { + var result; + if (Array.isArray(input)) { + var results = []; + input.forEach(function (item) { + var res = evaluateName(expr, item, environment); + if (typeof res !== 'undefined') { + results.push(res); + } + }); + if (results.length == 1) { + result = results[0]; + } else if (results.length > 1) { + result = results; + } + } else if (input !== null && typeof input === 'object') { + result = input[expr.value]; + } + return result; + } + function evaluateLiteral(expr) { + return expr.value; + } + function evaluateWildcard(expr, input) { + var result; + var results = []; + if (input !== null && typeof input === 'object') { + Object.keys(input).forEach(function (key) { + var value = input[key]; + if(Array.isArray(value)) { + value = flatten(value); + results = functionAppend(results, value); + } else { + results.push(value); + } + }); + } + + if (results.length == 1) { + result = results[0]; + } else if (results.length > 1) { + result = results; + } + return result; + } + function flatten(arg, flattened) { + if(typeof flattened === 'undefined') { + flattened = []; + } + if(Array.isArray(arg)) { + arg.forEach(function (item) { + flatten(item, flattened); + }); + } else { + flattened.push(arg); + } + return flattened; + } + function evaluateDescendants(expr, input) { + var result; + var resultSequence = []; + if (typeof input !== 'undefined') { + recurseDescendants(input, resultSequence); + if (resultSequence.length == 1) { + result = resultSequence[0]; + } else { + result = resultSequence; + } + } + return result; + } + function recurseDescendants(input, results) { + if (!Array.isArray(input)) { + results.push(input); + } + if (Array.isArray(input)) { + input.forEach(function (member) { + recurseDescendants(member, results); + }); + } else if (input !== null && typeof input === 'object') { + Object.keys(input).forEach(function (key) { + recurseDescendants(input[key], results); + }); + } + } + function evaluateNumericExpression(expr, input, environment) { + var result; + + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + return result; + } + + if (!isNumeric(lhs)) { + throw { + message: 'LHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: lhs + }; + } + if (!isNumeric(rhs)) { + throw { + message: 'RHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: rhs + }; + } + + switch (expr.value) { + case '+': + result = lhs + rhs; + break; + case '-': + result = lhs - rhs; + break; + case '*': + result = lhs * rhs; + break; + case '/': + result = lhs / rhs; + break; + case '%': + result = lhs % rhs; + break; + } + return result; + } + function evaluateComparisonExpression(expr, input, environment) { + var result; + + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + return false; + } + + switch (expr.value) { + case '=': + result = lhs == rhs; + break; + case '!=': + result = (lhs != rhs); + break; + case '<': + result = lhs < rhs; + break; + case '<=': + result = lhs <= rhs; + break; + case '>': + result = lhs > rhs; + break; + case '>=': + result = lhs >= rhs; + break; + } + return result; + } + function evaluateIncludesExpression(expr, input, environment) { + var result = false; + + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + return false; + } + + if(!Array.isArray(rhs)) { + rhs = [rhs]; + } + + for(var i = 0; i < rhs.length; i++) { + if(rhs[i] === lhs) { + result = true; + break; + } + } + + return result; + } + function evaluateBooleanExpression(expr, input, environment) { + var result; + + switch (expr.value) { + case 'and': + result = functionBoolean(evaluate(expr.lhs, input, environment)) && + functionBoolean(evaluate(expr.rhs, input, environment)); + break; + case 'or': + result = functionBoolean(evaluate(expr.lhs, input, environment)) || + functionBoolean(evaluate(expr.rhs, input, environment)); + break; + } + return result; + } + function evaluateStringConcat(expr, input, environment) { + var result; + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + var lstr = ''; + var rstr = ''; + if (typeof lhs !== 'undefined') { + lstr = functionString(lhs); + } + if (typeof rhs !== 'undefined') { + rstr = functionString(rhs); + } + + result = lstr.concat(rstr); + return result; + } + function evaluateGroupExpression(expr, input, environment) { + var result = {}; + var groups = {}; + if (!Array.isArray(input)) { + input = [input]; + } + input.forEach(function (item) { + expr.lhs.forEach(function (pair) { + var key = evaluate(pair[0], item, environment); + if (typeof key !== 'string') { + throw { + message: 'Key in object structure must evaluate to a string. Got: ' + key + ' at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + value: key + }; + } + var entry = {data: item, expr: pair[1]}; + if (groups.hasOwnProperty(key)) { + groups[key].data = functionAppend(groups[key].data, item); + } else { + groups[key] = entry; + } + }); + }); + for (var key in groups) { + var entry = groups[key]; + var value = evaluate(entry.expr, entry.data, environment); + result[key] = value; + } + return result; + } + function evaluateRangeExpression(expr, input, environment) { + var result; + + var lhs = evaluate(expr.lhs, input, environment); + var rhs = evaluate(expr.rhs, input, environment); + + if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { + return result; + } + + if (lhs > rhs) { + return result; + } + + if (!Number.isInteger(lhs)) { + throw { + message: 'LHS of range operator (..) must evaluate to an integer at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: lhs + }; + } + if (!Number.isInteger(rhs)) { + throw { + message: 'RHS of range operator (..) must evaluate to an integer at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: rhs + }; + } + + result = new Array(rhs - lhs + 1); + for (var item = lhs, index = 0; item <= rhs; item++, index++) { + result[index] = item; + } + return result; + } + function evaluateBindExpression(expr, input, environment) { + var value = evaluate(expr.rhs, input, environment); + if (expr.lhs.type !== 'variable') { + throw { + message: "Left hand side of := must be a variable name (start with $) at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: expr.lhs.value + }; + } + environment.bind(expr.lhs.value, value); + return value; + } + function evaluateCondition(expr, input, environment) { + var result; + var condition = evaluate(expr.condition, input, environment); + if (functionBoolean(condition)) { + result = evaluate(expr.then, input, environment); + } else if (typeof expr.else !== 'undefined') { + result = evaluate(expr.else, input, environment); + } + return result; + } + function evaluateBlock(expr, input, environment) { + var result; + var frame = createFrame(environment); + expr.expressions.forEach(function (expression) { + result = evaluate(expression, input, frame); + }); + + return result; + } + function evaluateVariable(expr, input, environment) { + var result; + if (expr.value === '') { + result = input; + } else { + result = environment.lookup(expr.value); + } + return result; + } + function evaluateFunction(expr, input, environment) { + var result; + var evaluatedArgs = []; + expr.arguments.forEach(function (arg) { + evaluatedArgs.push(evaluate(arg, input, environment)); + }); + var proc = evaluate(expr.procedure, input, environment); + + if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) { + throw { + message: 'Attempted to invoke a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.value + }; + } + try { + result = apply(proc, evaluatedArgs, environment, input); + while(typeof result === 'object' && result.lambda == true && result.thunk == true) { + var next = evaluate(result.body.procedure, result.input, result.environment); + evaluatedArgs = []; + result.body.arguments.forEach(function (arg) { + evaluatedArgs.push(evaluate(arg, result.input, result.environment)); + }); + + result = apply(next, evaluatedArgs); + } + } catch (err) { + err.position = expr.position; + err.token = expr.procedure.value; + throw err; + } + return result; + } + function apply(proc, args, environment, self) { + var result; + if (proc && proc.lambda) { + result = applyProcedure(proc, args); + } else if (typeof proc === 'function') { + result = proc.apply(self, args); + } else { + throw { + message: 'Attempted to invoke a non-function', + stack: (new Error()).stack + }; + } + return result; + } + function evaluateLambda(expr, input, environment) { + var procedure = { + lambda: true, + input: input, + environment: environment, + arguments: expr.arguments, + body: expr.body + }; + if(expr.thunk == true) { + procedure.thunk = true; + } + return procedure; + } + function evaluatePartialApplication(expr, input, environment) { + var result; + var evaluatedArgs = []; + expr.arguments.forEach(function (arg) { + if (arg.type === 'operator' && arg.value === '?') { + evaluatedArgs.push(arg); + } else { + evaluatedArgs.push(evaluate(arg, input, environment)); + } + }); + var proc = evaluate(expr.procedure, input, environment); + if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) { + throw { + message: 'Attempted to partially apply a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.value + }; + } + if (proc && proc.lambda) { + result = partialApplyProcedure(proc, evaluatedArgs); + } else if (typeof proc === 'function') { + result = partialApplyNativeFunction(proc, evaluatedArgs); + } else { + throw { + message: 'Attempted to partially apply a non-function at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.value + }; + } + return result; + } + function applyProcedure(proc, args) { + var result; + var env = createFrame(proc.environment); + proc.arguments.forEach(function (param, index) { + env.bind(param.value, args[index]); + }); + if (typeof proc.body === 'function') { + result = applyNativeFunction(proc.body, env); + } else { + result = evaluate(proc.body, proc.input, env); + } + return result; + } + function partialApplyProcedure(proc, args) { + var env = createFrame(proc.environment); + var unboundArgs = []; + proc.arguments.forEach(function (param, index) { + var arg = args[index]; + if (arg && arg.type === 'operator' && arg.value === '?') { + unboundArgs.push(param); + } else { + env.bind(param.value, arg); + } + }); + var procedure = { + lambda: true, + input: proc.input, + environment: env, + arguments: unboundArgs, + body: proc.body + }; + return procedure; + } + function partialApplyNativeFunction(native, args) { + var sigArgs = getNativeFunctionArguments(native); + sigArgs = sigArgs.map(function (sigArg) { + return '$' + sigArg.trim(); + }); + var body = 'function(' + sigArgs.join(', ') + '){ _ }'; + + var bodyAST = parser(body); + bodyAST.body = native; + + var partial = partialApplyProcedure(bodyAST, args); + return partial; + } + function applyNativeFunction(proc, env) { + var sigArgs = getNativeFunctionArguments(proc); + var args = sigArgs.map(function (sigArg) { + return env.lookup(sigArg.trim()); + }); + + var result = proc.apply(null, args); + return result; + } + function getNativeFunctionArguments(func) { + var signature = func.toString(); + var sigParens = /\(([^\)]*)\)/.exec(signature)[1]; // the contents of the parens + var sigArgs = sigParens.split(','); + return sigArgs; + } + function isLambda(arg) { + var result = false; + if(arg && typeof arg === 'object' && + arg.lambda === true && + arg.hasOwnProperty('input') && + arg.hasOwnProperty('arguments') && + arg.hasOwnProperty('environment') && + arg.hasOwnProperty('body')) { + result = true; + } + + return result; + } + function functionSum(args) { + var total = 0; + + if (arguments.length != 1) { + throw { + message: 'The sum function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); + if(nonNumerics.length > 0) { + throw { + message: 'Type error: argument of sum function must be an array of numbers', + stack: (new Error()).stack, + value: nonNumerics + }; + } + args.forEach(function(num){total += num;}); + return total; + } + function functionCount(args) { + if (arguments.length != 1) { + throw { + message: 'The count function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return 0; + } + + if(!Array.isArray(args)) { + args = [args]; + } + + return args.length; + } + function functionMax(args) { + var max; + + if (arguments.length != 1) { + throw { + message: 'The max function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); + if(nonNumerics.length > 0) { + throw { + message: 'Type error: argument of max function must be an array of numbers', + stack: (new Error()).stack, + value: nonNumerics + }; + } + max = Math.max.apply(Math, args); + return max; + } + function functionMin(args) { + var min; + + if (arguments.length != 1) { + throw { + message: 'The min function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); + if(nonNumerics.length > 0) { + throw { + message: 'Type error: argument of min function must be an array of numbers', + stack: (new Error()).stack, + value: nonNumerics + }; + } + min = Math.min.apply(Math, args); + return min; + } + function functionAverage(args) { + var total = 0; + + if (arguments.length != 1) { + throw { + message: 'The average function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); + if(nonNumerics.length > 0) { + throw { + message: 'Type error: argument of average function must be an array of numbers', + stack: (new Error()).stack, + value: nonNumerics + }; + } + args.forEach(function(num){total += num;}); + return total/args.length; + } + function functionString(arg) { + var str; + + if(arguments.length != 1) { + throw { + message: 'The string function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof arg === 'undefined') { + return undefined; + } + + if (typeof arg === 'string') { + str = arg; + } else if(typeof arg === 'function' || isLambda(arg)) { + str = ''; + } else if (typeof arg === 'number' && !isFinite(arg)) { + throw { + message: "Attempting to invoke string function on Infinity or NaN", + value: arg, + stack: (new Error()).stack + }; + } else + str = JSON.stringify(arg, function (key, val) { + return (typeof val !== 'undefined' && val !== null && val.toPrecision && isNumeric(val)) ? Number(val.toPrecision(13)) : + (val && isLambda(val)) ? '' : + (typeof val === 'function') ? '' : val; + }); + return str; + } + function functionSubstring(str, start, length) { + if(arguments.length != 2 && arguments.length != 3) { + throw { + message: 'The substring function expects two or three arguments', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + if(typeof str !== 'string') { + throw { + message: 'Type error: first argument of substring function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + + if(typeof start !== 'number') { + throw { + message: 'Type error: second argument of substring function must evaluate to a number', + stack: (new Error()).stack, + value: start + }; + } + + if(typeof length !== 'undefined' && typeof length !== 'number') { + throw { + message: 'Type error: third argument of substring function must evaluate to a number', + stack: (new Error()).stack, + value: length + }; + } + + return str.substr(start, length); + } + function functionSubstringBefore(str, chars) { + if(arguments.length != 2) { + throw { + message: 'The substringBefore function expects two arguments', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + if(typeof str !== 'string') { + throw { + message: 'Type error: first argument of substringBefore function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + if(typeof chars !== 'string') { + throw { + message: 'Type error: second argument of substringBefore function must evaluate to a string', + stack: (new Error()).stack, + value: chars + }; + } + + var pos = str.indexOf(chars); + if (pos > -1) { + return str.substr(0, pos); + } else { + return str; + } + } + function functionSubstringAfter(str, chars) { + if(arguments.length != 2) { + throw { + message: 'The substringAfter function expects two arguments', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + if(typeof str !== 'string') { + throw { + message: 'Type error: first argument of substringAfter function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + if(typeof chars !== 'string') { + throw { + message: 'Type error: second argument of substringAfter function must evaluate to a string', + stack: (new Error()).stack, + value: chars + }; + } + + var pos = str.indexOf(chars); + if (pos > -1) { + return str.substr(pos + chars.length); + } else { + return str; + } + } + function functionLowercase(str) { + if(arguments.length != 1) { + throw { + message: 'The lowercase function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + if(typeof str !== 'string') { + throw { + message: 'Type error: argument of lowercase function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + + return str.toLowerCase(); + } + function functionUppercase(str) { + if(arguments.length != 1) { + throw { + message: 'The uppercase function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + if(typeof str !== 'string') { + throw { + message: 'Type error: argument of uppercase function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + + return str.toUpperCase(); + } + function functionLength(str) { + if(arguments.length != 1) { + throw { + message: 'The length function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + if(typeof str !== 'string') { + throw { + message: 'Type error: argument of length function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + + return str.length; + } + function functionSplit(str, separator, limit) { + if(arguments.length != 2 && arguments.length != 3) { + throw { + message: 'The split function expects two or three arguments', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + if(typeof str !== 'string') { + throw { + message: 'Type error: first argument of split function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + if(typeof separator !== 'string') { + throw { + message: 'Type error: second argument of split function must evaluate to a string', + stack: (new Error()).stack, + value: separator + }; + } + if(typeof limit !== 'undefined' && (typeof limit !== 'number' || limit < 0)) { + throw { + message: 'Type error: third argument of split function must evaluate to a positive number', + stack: (new Error()).stack, + value: limit + }; + } + + return str.split(separator, limit); + } + function functionJoin(strs, separator) { + if(arguments.length != 1 && arguments.length != 2) { + throw { + message: 'The join function expects one or two arguments', + stack: (new Error()).stack + }; + } + if(typeof strs === 'undefined') { + return undefined; + } + + if(!Array.isArray(strs)) { + strs = [strs]; + } + var nonStrings = strs.filter(function(val) {return (typeof val !== 'string');}); + if(nonStrings.length > 0) { + throw { + message: 'Type error: first argument of join function must be an array of strings', + stack: (new Error()).stack, + value: nonStrings + }; + } + if(typeof separator === 'undefined') { + separator = ""; + } + if(typeof separator !== 'string') { + throw { + message: 'Type error: second argument of split function must evaluate to a string', + stack: (new Error()).stack, + value: separator + }; + } + + return strs.join(separator); + } + function functionNumber(arg) { + var result; + + if(arguments.length != 1) { + throw { + message: 'The number function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof arg === 'undefined') { + return undefined; + } + + if (typeof arg === 'number') { + result = arg; + } else if(typeof arg === 'string' && /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?$/.test(arg) && !isNaN(parseFloat(arg)) && isFinite(arg)) { + result = parseFloat(arg); + } else { + throw { + message: "Unable to cast value to a number", + value: arg, + stack: (new Error()).stack + }; + } + return result; + } + function functionBoolean(arg) { + + if(arguments.length != 1) { + throw { + message: 'The boolean function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof arg === 'undefined') { + return undefined; + } + + var result = false; + if (Array.isArray(arg)) { + if (arg.length == 1) { + result = functionBoolean(arg[0]); + } else if (arg.length > 1) { + var trues = arg.filter(function(val) {return functionBoolean(val);}); + result = trues.length > 0; + } + } else if (typeof arg === 'string') { + if (arg.length > 0) { + result = true; + } + } else if (isNumeric(arg)) { + if (arg != 0) { + result = true; + } + } else if (arg != null && typeof arg === 'object') { + if (Object.keys(arg).length > 0) { + if (!(isLambda(arg))) { + result = true; + } + } + } else if (typeof arg === 'boolean' && arg == true) { + result = true; + } + return result; + } + function functionNot(arg) { + return !functionBoolean(arg); + } + function functionMap(func) { + var varargs = arguments; + var result = []; + var args = []; + for (var ii = 1; ii < varargs.length; ii++) { + if (Array.isArray(varargs[ii])) { + args.push(varargs[ii]); + } else { + args.push([varargs[ii]]); + } + + } + if (args.length > 0) { + for (var i = 0; i < args[0].length; i++) { + var func_args = []; + for (var j = 0; j < func.arguments.length; j++) { + func_args.push(args[j][i]); + } + result.push(apply(func, func_args, null, null)); + } + } + return result; + } + function functionFoldLeft(func, sequence, init) { + var result; + + if (!(func.length == 2 || func.arguments.length == 2)) { + throw { + message: 'The first argument of the reduce function must be a function of arity 2', + stack: (new Error()).stack + }; + } + + if (!Array.isArray(sequence)) { + sequence = [sequence]; + } + + var index; + if (typeof init === 'undefined' && sequence.length > 0) { + result = sequence[0]; + index = 1; + } else { + result = init; + index = 0; + } + + while (index < sequence.length) { + result = apply(func, [result, sequence[index]], null, null); + index++; + } + + return result; + } + function functionKeys(arg) { + var result = []; + + if(Array.isArray(arg)) { + var merge = {}; + arg.forEach(function(item) { + var keys = functionKeys(item); + if(Array.isArray(keys)) { + keys.forEach(function(key) { + merge[key] = true; + }); + } + }); + result = functionKeys(merge); + } else if(arg != null && typeof arg === 'object' && !(isLambda(arg))) { + result = Object.keys(arg); + if(result.length == 0) { + result = undefined; + } + } else { + result = undefined; + } + return result; + } + function functionLookup(object, key) { + var result = evaluateName({value: key}, object); + return result; + } + function functionAppend(arg1, arg2) { + if (typeof arg1 === 'undefined') { + return arg2; + } + if (typeof arg2 === 'undefined') { + return arg1; + } + if (!Array.isArray(arg1)) { + arg1 = [arg1]; + } + if (!Array.isArray(arg2)) { + arg2 = [arg2]; + } + Array.prototype.push.apply(arg1, arg2); + return arg1; + } + function functionExists(arg){ + if (arguments.length != 1) { + throw { + message: 'The exists function expects one argument', + stack: (new Error()).stack + }; + } + + if (typeof arg === 'undefined') { + return false; + } else { + return true; + } + } + function functionSpread(arg) { + var result = []; + + if(Array.isArray(arg)) { + arg.forEach(function(item) { + result = functionAppend(result, functionSpread(item)); + }); + } else if(arg != null && typeof arg === 'object' && !isLambda(arg)) { + for(var key in arg) { + var obj = {}; + obj[key] = arg[key]; + result.push(obj); + } + } else { + result = arg; + } + return result; + } + function createFrame(enclosingEnvironment) { + var bindings = {}; + return { + bind: function (name, value) { + bindings[name] = value; + }, + lookup: function (name) { + var value = bindings[name]; + if (typeof value === 'undefined' && enclosingEnvironment) { + value = enclosingEnvironment.lookup(name); + } + return value; + } + }; + } + + var staticFrame = createFrame(null); + + staticFrame.bind('sum', functionSum); + staticFrame.bind('count', functionCount); + staticFrame.bind('max', functionMax); + staticFrame.bind('min', functionMin); + staticFrame.bind('average', functionAverage); + staticFrame.bind('string', functionString); + staticFrame.bind('substring', functionSubstring); + staticFrame.bind('substringBefore', functionSubstringBefore); + staticFrame.bind('substringAfter', functionSubstringAfter); + staticFrame.bind('lowercase', functionLowercase); + staticFrame.bind('uppercase', functionUppercase); + staticFrame.bind('length', functionLength); + staticFrame.bind('split', functionSplit); + staticFrame.bind('join', functionJoin); + staticFrame.bind('number', functionNumber); + staticFrame.bind('boolean', functionBoolean); + staticFrame.bind('not', functionNot); + staticFrame.bind('map', functionMap); + staticFrame.bind('reduce', functionFoldLeft); + staticFrame.bind('keys', functionKeys); + staticFrame.bind('lookup', functionLookup); + staticFrame.bind('append', functionAppend); + staticFrame.bind('exists', functionExists); + staticFrame.bind('spread', functionSpread); + function jsonata(expr) { + var ast = parser(expr); + var environment = createFrame(staticFrame); + return { + evaluate: function (input, bindings) { + if (typeof bindings !== 'undefined') { + var exec_env; + exec_env = createFrame(environment); + for (var v in bindings) { + exec_env.bind(v, bindings[v]); + } + } else { + exec_env = environment; + } + exec_env.bind('$', input); + return evaluate(ast, input, exec_env); + }, + assign: function (name, value) { + environment.bind(name, value); + } + }; + } + + jsonata.parser = parser; + + return jsonata; + + })(); + + + + + var oop = require("../lib/oop"); + var Mirror = require("../worker/mirror").Mirror; + + var JSONataWorker = exports.JSONataWorker = function(sender) { + Mirror.call(this, sender); + this.setTimeout(200); + }; + + oop.inherits(JSONataWorker, Mirror); + + (function() { + + this.onUpdate = function() { + var value = this.doc.getValue(); + var errors = []; + try { + if (value) + jsonata(value); + } catch (e) { + var pos = this.doc.indexToPosition(e.position-1); + errors.push({ + row: pos.row, + column: pos.column, + text: e.message, + type: "error" + }); + } + this.sender.emit("annotate", errors); + }; + + }).call(JSONataWorker.prototype); + +}); + +define("ace/lib/es5-shim",["require","exports","module"], function(require, exports, module) { + +function Empty() {} + +if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { // .length is 1 + var target = this; + if (typeof target != "function") { + throw new TypeError("Function.prototype.bind called on incompatible " + target); + } + var args = slice.call(arguments, 1); // for normal call + var bound = function () { + + if (this instanceof bound) { + + var result = target.apply( + this, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return this; + + } else { + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + if(target.prototype) { + Empty.prototype = target.prototype; + bound.prototype = new Empty(); + Empty.prototype = null; + } + return bound; + }; +} +var call = Function.prototype.call; +var prototypeOfArray = Array.prototype; +var prototypeOfObject = Object.prototype; +var slice = prototypeOfArray.slice; +var _toString = call.bind(prototypeOfObject.toString); +var owns = call.bind(prototypeOfObject.hasOwnProperty); +var defineGetter; +var defineSetter; +var lookupGetter; +var lookupSetter; +var supportsAccessors; +if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) { + defineGetter = call.bind(prototypeOfObject.__defineGetter__); + defineSetter = call.bind(prototypeOfObject.__defineSetter__); + lookupGetter = call.bind(prototypeOfObject.__lookupGetter__); + lookupSetter = call.bind(prototypeOfObject.__lookupSetter__); +} +if ([1,2].splice(0).length != 2) { + if(function() { // test IE < 9 to splice bug - see issue #138 + function makeArray(l) { + var a = new Array(l+2); + a[0] = a[1] = 0; + return a; + } + var array = [], lengthBefore; + + array.splice.apply(array, makeArray(20)); + array.splice.apply(array, makeArray(26)); + + lengthBefore = array.length; //46 + array.splice(5, 0, "XXX"); // add one element + + lengthBefore + 1 == array.length + + if (lengthBefore + 1 == array.length) { + return true;// has right splice implementation without bugs + } + }()) {//IE 6/7 + var array_splice = Array.prototype.splice; + Array.prototype.splice = function(start, deleteCount) { + if (!arguments.length) { + return []; + } else { + return array_splice.apply(this, [ + start === void 0 ? 0 : start, + deleteCount === void 0 ? (this.length - start) : deleteCount + ].concat(slice.call(arguments, 2))) + } + }; + } else {//IE8 + Array.prototype.splice = function(pos, removeCount){ + var length = this.length; + if (pos > 0) { + if (pos > length) + pos = length; + } else if (pos == void 0) { + pos = 0; + } else if (pos < 0) { + pos = Math.max(length + pos, 0); + } + + if (!(pos+removeCount < length)) + removeCount = length - pos; + + var removed = this.slice(pos, pos+removeCount); + var insert = slice.call(arguments, 2); + var add = insert.length; + if (pos === length) { + if (add) { + this.push.apply(this, insert); + } + } else { + var remove = Math.min(removeCount, length - pos); + var tailOldPos = pos + remove; + var tailNewPos = tailOldPos + add - remove; + var tailCount = length - tailOldPos; + var lengthAfterRemove = length - remove; + + if (tailNewPos < tailOldPos) { // case A + for (var i = 0; i < tailCount; ++i) { + this[tailNewPos+i] = this[tailOldPos+i]; + } + } else if (tailNewPos > tailOldPos) { // case B + for (i = tailCount; i--; ) { + this[tailNewPos+i] = this[tailOldPos+i]; + } + } // else, add == remove (nothing to do) + + if (add && pos === lengthAfterRemove) { + this.length = lengthAfterRemove; // truncate array + this.push.apply(this, insert); + } else { + this.length = lengthAfterRemove + add; // reserves space + for (i = 0; i < add; ++i) { + this[pos+i] = insert[i]; + } + } + } + return removed; + }; + } +} +if (!Array.isArray) { + Array.isArray = function isArray(obj) { + return _toString(obj) == "[object Array]"; + }; +} +var boxedString = Object("a"), + splitString = boxedString[0] != "a" || !(0 in boxedString); + +if (!Array.prototype.forEach) { + Array.prototype.forEach = function forEach(fun /*, thisp*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + thisp = arguments[1], + i = -1, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + while (++i < length) { + if (i in self) { + fun.call(thisp, self[i], i, object); + } + } + }; +} +if (!Array.prototype.map) { + Array.prototype.map = function map(fun /*, thisp*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + result = Array(length), + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self) + result[i] = fun.call(thisp, self[i], i, object); + } + return result; + }; +} +if (!Array.prototype.filter) { + Array.prototype.filter = function filter(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + result = [], + value, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self) { + value = self[i]; + if (fun.call(thisp, value, i, object)) { + result.push(value); + } + } + } + return result; + }; +} +if (!Array.prototype.every) { + Array.prototype.every = function every(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self && !fun.call(thisp, self[i], i, object)) { + return false; + } + } + return true; + }; +} +if (!Array.prototype.some) { + Array.prototype.some = function some(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self && fun.call(thisp, self[i], i, object)) { + return true; + } + } + return false; + }; +} +if (!Array.prototype.reduce) { + Array.prototype.reduce = function reduce(fun /*, initial*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + if (!length && arguments.length == 1) { + throw new TypeError("reduce of empty array with no initial value"); + } + + var i = 0; + var result; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i++]; + break; + } + if (++i >= length) { + throw new TypeError("reduce of empty array with no initial value"); + } + } while (true); + } + + for (; i < length; i++) { + if (i in self) { + result = fun.call(void 0, result, self[i], i, object); + } + } + + return result; + }; +} +if (!Array.prototype.reduceRight) { + Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + if (!length && arguments.length == 1) { + throw new TypeError("reduceRight of empty array with no initial value"); + } + + var result, i = length - 1; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i--]; + break; + } + if (--i < 0) { + throw new TypeError("reduceRight of empty array with no initial value"); + } + } while (true); + } + + do { + if (i in this) { + result = fun.call(void 0, result, self[i], i, object); + } + } while (i--); + + return result; + }; +} +if (!Array.prototype.indexOf || ([0, 1].indexOf(1, 2) != -1)) { + Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) { + var self = splitString && _toString(this) == "[object String]" ? + this.split("") : + toObject(this), + length = self.length >>> 0; + + if (!length) { + return -1; + } + + var i = 0; + if (arguments.length > 1) { + i = toInteger(arguments[1]); + } + i = i >= 0 ? i : Math.max(0, length + i); + for (; i < length; i++) { + if (i in self && self[i] === sought) { + return i; + } + } + return -1; + }; +} +if (!Array.prototype.lastIndexOf || ([0, 1].lastIndexOf(0, -3) != -1)) { + Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) { + var self = splitString && _toString(this) == "[object String]" ? + this.split("") : + toObject(this), + length = self.length >>> 0; + + if (!length) { + return -1; + } + var i = length - 1; + if (arguments.length > 1) { + i = Math.min(i, toInteger(arguments[1])); + } + i = i >= 0 ? i : length - Math.abs(i); + for (; i >= 0; i--) { + if (i in self && sought === self[i]) { + return i; + } + } + return -1; + }; +} +if (!Object.getPrototypeOf) { + Object.getPrototypeOf = function getPrototypeOf(object) { + return object.__proto__ || ( + object.constructor ? + object.constructor.prototype : + prototypeOfObject + ); + }; +} +if (!Object.getOwnPropertyDescriptor) { + var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " + + "non-object: "; + Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT + object); + if (!owns(object, property)) + return; + + var descriptor, getter, setter; + descriptor = { enumerable: true, configurable: true }; + if (supportsAccessors) { + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + + var getter = lookupGetter(object, property); + var setter = lookupSetter(object, property); + object.__proto__ = prototype; + + if (getter || setter) { + if (getter) descriptor.get = getter; + if (setter) descriptor.set = setter; + return descriptor; + } + } + descriptor.value = object[property]; + return descriptor; + }; +} +if (!Object.getOwnPropertyNames) { + Object.getOwnPropertyNames = function getOwnPropertyNames(object) { + return Object.keys(object); + }; +} +if (!Object.create) { + var createEmpty; + if (Object.prototype.__proto__ === null) { + createEmpty = function () { + return { "__proto__": null }; + }; + } else { + createEmpty = function () { + var empty = {}; + for (var i in empty) + empty[i] = null; + empty.constructor = + empty.hasOwnProperty = + empty.propertyIsEnumerable = + empty.isPrototypeOf = + empty.toLocaleString = + empty.toString = + empty.valueOf = + empty.__proto__ = null; + return empty; + } + } + + Object.create = function create(prototype, properties) { + var object; + if (prototype === null) { + object = createEmpty(); + } else { + if (typeof prototype != "object") + throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'"); + var Type = function () {}; + Type.prototype = prototype; + object = new Type(); + object.__proto__ = prototype; + } + if (properties !== void 0) + Object.defineProperties(object, properties); + return object; + }; +} + +function doesDefinePropertyWork(object) { + try { + Object.defineProperty(object, "sentinel", {}); + return "sentinel" in object; + } catch (exception) { + } +} +if (Object.defineProperty) { + var definePropertyWorksOnObject = doesDefinePropertyWork({}); + var definePropertyWorksOnDom = typeof document == "undefined" || + doesDefinePropertyWork(document.createElement("div")); + if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) { + var definePropertyFallback = Object.defineProperty; + } +} + +if (!Object.defineProperty || definePropertyFallback) { + var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: "; + var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: " + var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " + + "on this javascript engine"; + + Object.defineProperty = function defineProperty(object, property, descriptor) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT_TARGET + object); + if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null) + throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor); + if (definePropertyFallback) { + try { + return definePropertyFallback.call(Object, object, property, descriptor); + } catch (exception) { + } + } + if (owns(descriptor, "value")) { + + if (supportsAccessors && (lookupGetter(object, property) || + lookupSetter(object, property))) + { + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + delete object[property]; + object[property] = descriptor.value; + object.__proto__ = prototype; + } else { + object[property] = descriptor.value; + } + } else { + if (!supportsAccessors) + throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); + if (owns(descriptor, "get")) + defineGetter(object, property, descriptor.get); + if (owns(descriptor, "set")) + defineSetter(object, property, descriptor.set); + } + + return object; + }; +} +if (!Object.defineProperties) { + Object.defineProperties = function defineProperties(object, properties) { + for (var property in properties) { + if (owns(properties, property)) + Object.defineProperty(object, property, properties[property]); + } + return object; + }; +} +if (!Object.seal) { + Object.seal = function seal(object) { + return object; + }; +} +if (!Object.freeze) { + Object.freeze = function freeze(object) { + return object; + }; +} +try { + Object.freeze(function () {}); +} catch (exception) { + Object.freeze = (function freeze(freezeObject) { + return function freeze(object) { + if (typeof object == "function") { + return object; + } else { + return freezeObject(object); + } + }; + })(Object.freeze); +} +if (!Object.preventExtensions) { + Object.preventExtensions = function preventExtensions(object) { + return object; + }; +} +if (!Object.isSealed) { + Object.isSealed = function isSealed(object) { + return false; + }; +} +if (!Object.isFrozen) { + Object.isFrozen = function isFrozen(object) { + return false; + }; +} +if (!Object.isExtensible) { + Object.isExtensible = function isExtensible(object) { + if (Object(object) === object) { + throw new TypeError(); // TODO message + } + var name = ''; + while (owns(object, name)) { + name += '?'; + } + object[name] = true; + var returnValue = owns(object, name); + delete object[name]; + return returnValue; + }; +} +if (!Object.keys) { + var hasDontEnumBug = true, + dontEnums = [ + "toString", + "toLocaleString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "constructor" + ], + dontEnumsLength = dontEnums.length; + + for (var key in {"toString": null}) { + hasDontEnumBug = false; + } + + Object.keys = function keys(object) { + + if ( + (typeof object != "object" && typeof object != "function") || + object === null + ) { + throw new TypeError("Object.keys called on a non-object"); + } + + var keys = []; + for (var name in object) { + if (owns(object, name)) { + keys.push(name); + } + } + + if (hasDontEnumBug) { + for (var i = 0, ii = dontEnumsLength; i < ii; i++) { + var dontEnum = dontEnums[i]; + if (owns(object, dontEnum)) { + keys.push(dontEnum); + } + } + } + return keys; + }; + +} +if (!Date.now) { + Date.now = function now() { + return new Date().getTime(); + }; +} +var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + + "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + + "\u2029\uFEFF"; +if (!String.prototype.trim || ws.trim()) { + ws = "[" + ws + "]"; + var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), + trimEndRegexp = new RegExp(ws + ws + "*$"); + String.prototype.trim = function trim() { + return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, ""); + }; +} + +function toInteger(n) { + n = +n; + if (n !== n) { // isNaN + n = 0; + } else if (n !== 0 && n !== (1/0) && n !== -(1/0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + return n; +} + +function isPrimitive(input) { + var type = typeof input; + return ( + input === null || + type === "undefined" || + type === "boolean" || + type === "number" || + type === "string" + ); +} + +function toPrimitive(input) { + var val, valueOf, toString; + if (isPrimitive(input)) { + return input; + } + valueOf = input.valueOf; + if (typeof valueOf === "function") { + val = valueOf.call(input); + if (isPrimitive(val)) { + return val; + } + } + toString = input.toString; + if (typeof toString === "function") { + val = toString.call(input); + if (isPrimitive(val)) { + return val; + } + } + throw new TypeError(); +} +var toObject = function (o) { + if (o == null) { // this matches both null and undefined + throw new TypeError("can't convert "+o+" to object"); + } + return Object(o); +}; + +}); From 26f5305593861b7bf416312400d43ca6b1d6e184 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 15 Nov 2016 23:22:25 +0000 Subject: [PATCH 03/44] Add jsonata function help --- Gruntfile.js | 6 +- editor/js/i18n.js | 2 +- editor/js/ui/common/typedInput.js | 5 +- editor/js/ui/editor.js | 23 ++++- editor/templates/index.mst | 8 +- editor/vendor/jsonata/formatter.js | 109 +++++++++++++++++++++ editor/vendor/jsonata/mode-jsonata.js | 14 ++- editor/vendor/jsonata/worker-jsonata.js | 121 ++++++++++++------------ red/api/index.js | 2 + red/api/locales/en-US/editor.json | 3 + red/api/locales/en-US/jsonata.json | 98 +++++++++++++++++++ 11 files changed, 318 insertions(+), 73 deletions(-) create mode 100644 editor/vendor/jsonata/formatter.js create mode 100644 red/api/locales/en-US/jsonata.json diff --git a/Gruntfile.js b/Gruntfile.js index 0e15c8dcd..dac1329a6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -152,6 +152,10 @@ module.exports = function(grunt) { "public/vendor/vendor.css": [ // TODO: resolve relative resource paths in // bootstrap/FA/jquery + ], + "public/vendor/jsonata/jsonata.js": [ + "editor/vendor/jsonata/jsonata.js", + "editor/vendor/jsonata/formatter.js" ] } } @@ -161,7 +165,7 @@ module.exports = function(grunt) { files: { 'public/red/red.min.js': 'public/red/red.js', 'public/red/main.min.js': 'public/red/main.js', - 'public/vendor/jsonata/jsonata.min.js': 'editor/vendor/jsonata/jsonata.js', + 'public/vendor/jsonata/jsonata.min.js': 'public/vendor/jsonata/jsonata.js', 'public/vendor/ace/mode-jsonata.js': 'editor/vendor/jsonata/mode-jsonata.js', 'public/vendor/ace/worker-jsonata.js': 'editor/vendor/jsonata/worker-jsonata.js' } diff --git a/editor/js/i18n.js b/editor/js/i18n.js index cf29a6b43..f03b89a7b 100644 --- a/editor/js/i18n.js +++ b/editor/js/i18n.js @@ -23,7 +23,7 @@ RED.i18n = (function() { dynamicLoad: false, load:'current', ns: { - namespaces: ["editor","node-red"], + namespaces: ["editor","node-red","jsonata"], defaultNs: "editor" }, fallbackLng: ['en-US'], diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index c433c0366..6f18a4884 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -85,6 +85,7 @@ } return true; } + var allOptions = { msg: {value:"msg",label:"msg.",validate:validateExpression}, flow: {value:"flow",label:"flow.",validate:validateExpression}, @@ -103,9 +104,9 @@ expand:function() { var that = this; RED.editor.editExpression({ - value: this.value(), + value: jsonata.format(this.value()), complete: function(v) { - that.value(v); + that.value(v.replace(/\s*\n\s*/g," ")); } }) } diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index cb821d731..23947b9fb 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -1392,6 +1392,7 @@ RED.editor = (function() { text: RED._("common.label.done"), class: "primary", click: function() { + $("#node-input-expression-help").html(""); onComplete(expressionEditor.getValue()); RED.tray.close(); } @@ -1412,8 +1413,19 @@ RED.editor = (function() { }, open: function(tray) { var trayBody = tray.find('.editor-tray-body'); - var dialogForm = buildEditForm(tray,'dialog-form','_expression'); + var dialogForm = buildEditForm(tray,'dialog-form','_expression','editor'); + var funcSelect = $("#node-input-expression-func"); + jsonata.functions.forEach(function(f) { + funcSelect.append($("").val(f).text(f)); + }) + funcSelect.change(function(e) { + var f = $(this).val(); + var args = RED._('jsonata:'+f+".args",{defaultValue:''}); + var title = "

"+f+"("+args+")

"; + var body = marked(RED._('jsonata:'+f+'.desc',{defaultValue:''})); + $("#node-input-expression-help").html(title+"

"+body+"

"); + }) expressionEditor = RED.editor.createEditor({ id: 'node-input-expression', value: "", @@ -1426,6 +1438,15 @@ RED.editor = (function() { }); expressionEditor.getSession().setValue(value||"",-1); + expressionEditor.on("changeSelection", function() { + var c = expressionEditor.getCursorPosition(); + var token = expressionEditor.getSession().getTokenAt(c.row,c.column); + //console.log(token); + if (token && token.type === 'keyword') { + funcSelect.val(token.value).change(); + } + }); + dialogForm.i18n(); }, diff --git a/editor/templates/index.mst b/editor/templates/index.mst index 8407774a3..7dc5fda50 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -161,7 +161,13 @@ diff --git a/editor/vendor/jsonata/formatter.js b/editor/vendor/jsonata/formatter.js new file mode 100644 index 000000000..036de6852 --- /dev/null +++ b/editor/vendor/jsonata/formatter.js @@ -0,0 +1,109 @@ +(function() { + function indentLine(str,length) { + if (length <= 0) { + return str; + } + var i = (new Array(length)).join(" "); + str = str.replace(/^\s*/,i); + return str; + } + function formatExpression(str) { + var length = str.length; + var start = 0; + var inString = false; + var inBox = false; + var quoteChar; + var list = []; + var stack = []; + var frame; + var v; + var matchingBrackets = { + "(":")", + "[":"]", + "{":"}" + } + for (var i=0;i 30) { + longStack.push(true); + indent += 4; + pre = result.substring(0,offset+f.pos+1); + post = result.substring(offset+f.pos+1); + indented = indentLine(post,indent); + result = pre+"\n"+indented; + offset += indented.length-post.length+1; + } else { + longStack.push(false); + } + } else if (f.type === "close-block") { + if (f.width > 30) { + indent -= 4; + pre = result.substring(0,offset+f.pos); + post = result.substring(offset+f.pos); + indented = indentLine(post,indent); + result = pre+"\n"+indented; + offset += indented.length-post.length+1; + } + longStack.pop(); + } + }) + //console.log(result); + return result; + } + + jsonata.format = formatExpression; + jsonata.functions = ["$sum", "$count", "$max", "$min", "$average", "$string", "$substring", "$substringBefore", "$substringAfter", "$lowercase", "$uppercase", "$length", "$split", "$join", "$number", "$boolean", "$not", "$map", "$reduce", "$keys", "$lookup", "$append", "$exists", "$spread"] +})(); diff --git a/editor/vendor/jsonata/mode-jsonata.js b/editor/vendor/jsonata/mode-jsonata.js index 24562c536..50190e7b3 100644 --- a/editor/vendor/jsonata/mode-jsonata.js +++ b/editor/vendor/jsonata/mode-jsonata.js @@ -1,8 +1,10 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules","ace/worker/worker_client","ace/mode/text"], function(require, exports, module) { + "use strict"; var oop = require("../lib/oop"); var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + var WorkerClient = require("../worker/worker_client").WorkerClient; var JSONataHighlightRules = function() { @@ -12,6 +14,8 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ "and|or|in", "constant.language": "null|Infinity|NaN|undefined", + "constant.language.boolean": + "true|false", "storage.type": "function", "keyword": @@ -39,16 +43,12 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ token : "constant.numeric", // float regex : /[+-]?\d[\d_]*(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/ }, - { - token : keywordMapper, - regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*" - }, { token: "keyword", regex: /λ/ }, { - token: "constant.language.boolean", - regex: "true|false" + token : keywordMapper, + regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*" }, { token : "punctuation.operator", @@ -119,8 +119,6 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ return worker; }; - - this.$id = "ace/mode/jsonata"; }).call(Mode.prototype); diff --git a/editor/vendor/jsonata/worker-jsonata.js b/editor/vendor/jsonata/worker-jsonata.js index b6803d3af..da292ea6d 100644 --- a/editor/vendor/jsonata/worker-jsonata.js +++ b/editor/vendor/jsonata/worker-jsonata.js @@ -11,7 +11,7 @@ if (!window.console) { postMessage({type: "log", data: msgs}); }; window.console.error = - window.console.warn = + window.console.warn = window.console.log = window.console.trace = window.console; } @@ -23,7 +23,7 @@ window.onerror = function(message, file, line, col, err) { message: message, data: err.data, file: file, - line: line, + line: line, col: col, stack: err.stack }}); @@ -39,13 +39,13 @@ window.normalizeModule = function(parentId, moduleName) { if (moduleName.charAt(0) == ".") { var base = parentId.split("/").slice(0, -1).join("/"); moduleName = (base ? base + "/" : "") + moduleName; - + while (moduleName.indexOf(".") !== -1 && previous != moduleName) { var previous = moduleName; moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, ""); } } - + return moduleName; }; @@ -67,13 +67,13 @@ window.require = function require(parentId, id) { } return module.exports; } - + if (!window.require.tlns) return console.log("unable to load " + id); - + var path = resolveModuleId(id, window.require.tlns); if (path.slice(-3) != ".js") path += ".js"; - + window.require.id = id; window.require.modules[id] = {}; // prevent infinite loop on broken modules importScripts(path); @@ -112,7 +112,7 @@ window.define = function(id, deps, factory) { deps = []; id = window.require.id; } - + if (typeof factory != "function") { window.require.modules[id] = { exports: factory, @@ -163,13 +163,13 @@ window.initSender = function initSender() { var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter; var oop = window.require("ace/lib/oop"); - + var Sender = function() {}; - + (function() { - + oop.implement(this, EventEmitter); - + this.callback = function(data, callbackId) { postMessage({ type: "call", @@ -177,7 +177,7 @@ window.initSender = function initSender() { data: data }); }; - + this.emit = function(name, data) { postMessage({ type: "event", @@ -185,9 +185,9 @@ window.initSender = function initSender() { data: data }); }; - + }).call(Sender.prototype); - + return new Sender(); }; @@ -517,7 +517,7 @@ function validateDelta(docLines, delta) { } exports.applyDelta = function(docLines, delta, doNotValidate) { - + var row = delta.start.row; var startColumn = delta.start.column; var line = docLines[row] || ""; @@ -582,7 +582,7 @@ EventEmitter._dispatchEvent = function(eventName, e) { if (e.propagationStopped) break; } - + if (defaultHandler && !e.defaultPrevented) return defaultHandler(e, this); }; @@ -610,7 +610,7 @@ EventEmitter.setDefaultHandler = function(eventName, callback) { var handlers = this._defaultHandlers if (!handlers) handlers = this._defaultHandlers = {_disabled_: {}}; - + if (handlers[eventName]) { var old = handlers[eventName]; var disabled = handlers._disabled_[eventName]; @@ -618,7 +618,7 @@ EventEmitter.setDefaultHandler = function(eventName, callback) { handlers._disabled_[eventName] = disabled = []; disabled.push(old); var i = disabled.indexOf(callback); - if (i != -1) + if (i != -1) disabled.splice(i, 1); } handlers[eventName] = callback; @@ -628,7 +628,7 @@ EventEmitter.removeDefaultHandler = function(eventName, callback) { if (!handlers) return; var disabled = handlers._disabled_[eventName]; - + if (handlers[eventName] == callback) { var old = handlers[eventName]; if (disabled) @@ -684,7 +684,7 @@ var EventEmitter = require("./lib/event_emitter").EventEmitter; var Anchor = exports.Anchor = function(doc, row, column) { this.$onChange = this.onChange.bind(this); this.attach(doc); - + if (typeof column == "undefined") this.setPosition(row.row, row.column); else @@ -707,16 +707,16 @@ var Anchor = exports.Anchor = function(doc, row, column) { if (delta.start.row > this.row) return; - + var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight); this.setPosition(point.row, point.column, true); }; - + function $pointsInOrder(point1, point2, equalPointsInOrder) { var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter); } - + function $getTransformedPoint(delta, point, moveIfEqual) { var deltaIsInsert = delta.action == "insert"; var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row); @@ -735,7 +735,7 @@ var Anchor = exports.Anchor = function(doc, row, column) { column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) }; } - + return { row: deltaStart.row, column: deltaStart.column @@ -919,23 +919,23 @@ var Document = function(textOrLines) { this.insert = function(position, text) { if (this.getLength() <= 1) this.$detectNewLine(text); - + return this.insertMergedLines(position, this.$split(text)); }; this.insertInLine = function(position, text) { var start = this.clippedPos(position.row, position.column); var end = this.pos(position.row, position.column + text.length); - + this.applyDelta({ start: start, end: end, action: "insert", lines: [text] }, true); - + return this.clonePos(end); }; - + this.clippedPos = function(row, column) { var length = this.getLength(); if (row === undefined) { @@ -952,15 +952,15 @@ var Document = function(textOrLines) { column = Math.min(Math.max(column, 0), line.length); return {row: row, column: column}; }; - + this.clonePos = function(pos) { return {row: pos.row, column: pos.column}; }; - + this.pos = function(row, column) { return {row: row, column: column}; }; - + this.$clipPosition = function(position) { var length = this.getLength(); if (position.row >= length) { @@ -984,21 +984,21 @@ var Document = function(textOrLines) { column = this.$lines[row].length; } this.insertMergedLines({row: row, column: column}, lines); - }; + }; this.insertMergedLines = function(position, lines) { var start = this.clippedPos(position.row, position.column); var end = { row: start.row + lines.length - 1, column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length }; - + this.applyDelta({ start: start, end: end, action: "insert", lines: lines }); - + return this.clonePos(end); }; this.remove = function(range) { @@ -1015,14 +1015,14 @@ var Document = function(textOrLines) { this.removeInLine = function(row, startColumn, endColumn) { var start = this.clippedPos(row, startColumn); var end = this.clippedPos(row, endColumn); - + this.applyDelta({ start: start, end: end, action: "remove", lines: this.getLinesForRange({start: start, end: end}) }, true); - + return this.clonePos(start); }; this.removeFullLines = function(firstRow, lastRow) { @@ -1033,10 +1033,10 @@ var Document = function(textOrLines) { var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow ); var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 ); var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow ); - var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); + var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); var range = new Range(startRow, startCol, endRow, endCol); var deletedLines = this.$lines.slice(firstRow, lastRow + 1); - + this.applyDelta({ start: range.start, end: range.end, @@ -1071,7 +1071,7 @@ var Document = function(textOrLines) { else { end = range.start; } - + return end; }; this.applyDeltas = function(deltas) { @@ -1090,17 +1090,17 @@ var Document = function(textOrLines) { : !Range.comparePoints(delta.start, delta.end)) { return; } - + if (isInsert && delta.lines.length > 20000) this.$splitAndapplyLargeDelta(delta, 20000); applyDelta(this.$lines, delta, doNotValidate); this._signal("change", delta); }; - + this.$splitAndapplyLargeDelta = function(delta, MAX) { var lines = delta.lines; var l = lines.length; - var row = delta.start.row; + var row = delta.start.row; var column = delta.start.column; var from = 0, to = 0; do { @@ -1203,7 +1203,7 @@ exports.copyArray = function(array){ for (var i=0, l=array.length; i Date: Wed, 16 Nov 2016 13:44:45 +0000 Subject: [PATCH 04/44] Preserve newlines in jsonata expression via tabs --- editor/js/ui/common/typedInput.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index 6f18a4884..e7a26cf3d 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -104,9 +104,9 @@ expand:function() { var that = this; RED.editor.editExpression({ - value: jsonata.format(this.value()), + value: this.value().replace(/\t/g,"\n"), complete: function(v) { - that.value(v.replace(/\s*\n\s*/g," ")); + that.value(v.replace(/\n/g,"\t")); } }) } From be18cc9f2dbc8b7e355ac472554532f0431197df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ho=C5=99=C4=8Dica?= Date: Wed, 16 Nov 2016 15:08:14 +0100 Subject: [PATCH 05/44] Add support for flow and global context in Template node (#1048) * Enable tests for flow and global context * Add support for flow and global context in Template node * Handle missing node context --- nodes/core/core/80-template.html | 1 + nodes/core/core/80-template.js | 35 ++++++++++- test/nodes/core/core/80-template_spec.js | 78 +++++++++++++++++++----- 3 files changed, 99 insertions(+), 15 deletions(-) diff --git a/nodes/core/core/80-template.html b/nodes/core/core/80-template.html index 446284dbf..0bbf4da42 100644 --- a/nodes/core/core/80-template.html +++ b/nodes/core/core/80-template.html @@ -67,6 +67,7 @@ }

The resulting property will be:

Hello Fred. Today is Monday
+

It is possible to use property from flow context or global context. Just use {{flow.name}} or {{global.name}}.

By default, mustache will escape any HTML entities in the values it substitutes. To prevent this, use {{{triple}}} braces. diff --git a/nodes/core/core/80-template.js b/nodes/core/core/80-template.js index 8e93b1530..ba571d458 100644 --- a/nodes/core/core/80-template.js +++ b/nodes/core/core/80-template.js @@ -18,6 +18,39 @@ module.exports = function(RED) { "use strict"; var mustache = require("mustache"); + /** + * Custom Mustache Context capable to resolve message property and node + * flow and global context + */ + function NodeContext(msg, nodeContext) { + this.msgContext = new mustache.Context(msg); + this.nodeContext = nodeContext; + } + + NodeContext.prototype = new mustache.Context(); + + NodeContext.prototype.lookup = function (name) { + // try message first: + var value = this.msgContext.lookup(name); + if (value !== undefined) { + return value; + } + + // try node context: + var dot = name.indexOf("."); + if (dot > 0) { + var contextName = name.substr(0, dot); + var variableName = name.substr(dot + 1); + + if (contextName === "flow" && this.nodeContext.flow) { + return this.nodeContext.flow.get(variableName); + } + else if (contextName === "global" && this.nodeContext.global) { + return this.nodeContext.global.get(variableName); + } + } + } + function TemplateNode(n) { RED.nodes.createNode(this,n); this.name = n.name; @@ -31,7 +64,7 @@ module.exports = function(RED) { try { var value; if (node.syntax === "mustache") { - value = mustache.render(node.template,msg); + value = mustache.render(node.template, new NodeContext(msg, node.context())); } else { value = node.template; } diff --git a/test/nodes/core/core/80-template_spec.js b/test/nodes/core/core/80-template_spec.js index b93601989..686f77ede 100644 --- a/test/nodes/core/core/80-template_spec.js +++ b/test/nodes/core/core/80-template_spec.js @@ -43,6 +43,52 @@ describe('template node', function() { }); }); + it('should modify payload from flow context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"payload", template:"payload={{flow.value}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; + helper.load(templateNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.context().flow.set("value","foo"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'payload=foo'); + done(); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + + it('should modify payload from global context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"payload", template:"payload={{global.value}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; + helper.load(templateNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.context().global.set("value","foo"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'payload=foo'); + done(); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + + it('should handle missing node context', function(done) { + // this is artificial test because in flow there is missing z property (probably never happen in real usage) + var flow = [{id:"n1",type:"template", field:"payload", template:"payload={{flow.value}},{{global.value}}",wires:[["n2"]]},{id:"n2",type:"helper"}]; + helper.load(templateNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'payload=,'); + done(); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + + it('should modify payload in plain text mode', function(done) { var flow = [{id:"n1", type:"template", field:"payload", syntax:"plain", template:"payload={{payload}}",wires:[["n2"]]},{id:"n2",type:"helper"}]; helper.load(templateNode, flow, function() { @@ -57,32 +103,36 @@ describe('template node', function() { }); }); - xit('should modify flow context', function(done) { - var flow = [{id:"n1", type:"template", field:"payload", fieldType:"flow", template:"payload={{payload}}",wires:[["n2"]]},{id:"n2",type:"helper"}]; + it('should modify flow context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"payload", fieldType:"flow", template:"payload={{payload}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; helper.load(templateNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); - setTimeout( function() { - console.log(n2); - console.log(n2.context().global.get("payload")); - //c.should.equal(1); // should only have had one output. + n2.on("input", function(msg) { + // mesage is intact + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + // result is in flow context + n2.context().flow.get("payload").should.equal("payload=foo"); done(); - },50); + }); n1.receive({payload:"foo",topic: "bar"}); }); }); - xit('should modify global context', function(done) { - var flow = [{id:"n1", type:"template", field:"payload", fieldType:"global", template:"payload={{payload}}",wires:[["n2"]]},{id:"n2",type:"helper"}]; + it('should modify global context', function(done) { + var flow = [{id:"n1",z:"t1", type:"template", field:"payload", fieldType:"global", template:"payload={{payload}}",wires:[["n2"]]},{id:"n2",z:"t1",type:"helper"}]; helper.load(templateNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); - setTimeout( function() { - console.log(n2); - console.log(n2.context().global.get("payload")); - //c.should.equal(1); // should only have had one output. + n2.on("input", function(msg) { + // mesage is intact + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + // result is in global context + n2.context().global.get("payload").should.equal("payload=foo"); done(); - },50); + }); n1.receive({payload:"foo",topic: "bar"}); }); }); From eeaff6b553dcd0a5e593885a182dea79e8f3b8c0 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 16 Nov 2016 14:54:51 +0000 Subject: [PATCH 06/44] Add insert-function button to expression editor --- Gruntfile.js | 3 ++- editor/js/ui/editor.js | 17 +++++++++++++---- editor/templates/index.mst | 2 +- editor/vendor/jsonata/jsonata.js | 3 +++ red/api/locales/en-US/editor.json | 3 ++- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index dac1329a6..9c44b782e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -167,7 +167,8 @@ module.exports = function(grunt) { 'public/red/main.min.js': 'public/red/main.js', 'public/vendor/jsonata/jsonata.min.js': 'public/vendor/jsonata/jsonata.js', 'public/vendor/ace/mode-jsonata.js': 'editor/vendor/jsonata/mode-jsonata.js', - 'public/vendor/ace/worker-jsonata.js': 'editor/vendor/jsonata/worker-jsonata.js' + 'public/vendor/ace/worker-jsonata.js': 'editor/vendor/jsonata/worker-jsonata.js', + 'public/vendor/ace/snippets/jsonata.js': 'editor/vendor/jsonata/snippets-jsonata.js' } } }, diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index 23947b9fb..5e9775ec8 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -1421,7 +1421,7 @@ RED.editor = (function() { funcSelect.change(function(e) { var f = $(this).val(); var args = RED._('jsonata:'+f+".args",{defaultValue:''}); - var title = "

"+f+"("+args+")

"; + var title = "
"+f+"("+args+")
"; var body = marked(RED._('jsonata:'+f+'.desc',{defaultValue:''})); $("#node-input-expression-help").html(title+"

"+body+"

"); @@ -1432,7 +1432,7 @@ RED.editor = (function() { mode:"ace/mode/jsonata", options: { enableBasicAutocompletion:true, - enableSnippets:false, + enableSnippets:true, enableLiveAutocompletion: true } }); @@ -1441,14 +1441,23 @@ RED.editor = (function() { expressionEditor.on("changeSelection", function() { var c = expressionEditor.getCursorPosition(); var token = expressionEditor.getSession().getTokenAt(c.row,c.column); - //console.log(token); + // console.log(token); if (token && token.type === 'keyword') { funcSelect.val(token.value).change(); } }); dialogForm.i18n(); - + $("#node-input-expression-func-insert").click(function(e) { + e.preventDefault(); + var pos = expressionEditor.getCursorPosition(); + var f = funcSelect.val(); + var args = RED._('jsonata:'+f+".args",{defaultValue:''}); + expressionEditor.insert(f+"("+args+")"); + pos.column += f.length+1; + expressionEditor.moveCursorToPosition(pos); + expressionEditor.focus(); + }) }, close: function() { editStack.pop(); diff --git a/editor/templates/index.mst b/editor/templates/index.mst index 7dc5fda50..d8cbb2537 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -166,7 +166,7 @@
- +
diff --git a/editor/vendor/jsonata/jsonata.js b/editor/vendor/jsonata/jsonata.js index 2883b5fc4..814bba28f 100644 --- a/editor/vendor/jsonata/jsonata.js +++ b/editor/vendor/jsonata/jsonata.js @@ -2788,6 +2788,9 @@ var jsonata = (function() { }, assign: function (name, value) { environment.bind(name, value); + }, + ast: function() { + return ast; } }; } diff --git a/red/api/locales/en-US/editor.json b/red/api/locales/en-US/editor.json index 5029b6182..dca0d7ad7 100644 --- a/red/api/locales/en-US/editor.json +++ b/red/api/locales/en-US/editor.json @@ -320,6 +320,7 @@ "empty": "No matches found" }, "expressionEditor": { - "functions": "Functions" + "functions": "Functions", + "insert": "Insert" } } From 1fd87bf66467dd9580efe3bb194dd546b9e443f1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 16 Nov 2016 15:05:04 +0000 Subject: [PATCH 07/44] Improve debug message meta data contrast and legibility --- editor/sass/debug.scss | 15 ++++++--------- nodes/core/core/lib/debug/debug-utils.js | 15 ++++++++------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/editor/sass/debug.scss b/editor/sass/debug.scss index dcc72ce8a..f56a8c7f3 100644 --- a/editor/sass/debug.scss +++ b/editor/sass/debug.scss @@ -51,24 +51,21 @@ border-right: 8px solid #eee; padding: 2px; } -.debug-message-date { +.debug-message-meta { background: #fff; - font-size: 9px; - color: #aaa; + font-size: 10px; + color: #777; +} +.debug-message-date { padding: 1px 5px 1px 1px; } .debug-message-topic { display: block; - background: #fff; - padding: 1px; - font-size: 10px; color: #a66; } .debug-message-name { - background: #fff; padding: 1px 5px; - font-size: 9px; - color: #aac; + color: #777; } .debug-message-payload { display: block; diff --git a/nodes/core/core/lib/debug/debug-utils.js b/nodes/core/core/lib/debug/debug-utils.js index edb1d6c37..4767733a2 100644 --- a/nodes/core/core/lib/debug/debug-utils.js +++ b/nodes/core/core/lib/debug/debug-utils.js @@ -202,16 +202,17 @@ RED.debug = (function() { var format = sanitize((o.format||"").toString()); msg.className = 'debug-message'+(o.level?(' debug-message-level-'+o.level):'') + ((sourceNode&&sourceNode.z)?((" debug-message-flow-"+sourceNode.z+((filter&&(activeWorkspace!==sourceNode.z))?" hide":""))):""); - $(''+ getTimestamp()+'').appendTo(msg); + var metaRow = $('
').appendTo(msg); + $(''+ getTimestamp()+'').appendTo(metaRow); if (sourceNode) { $('',{href:"#",class:"debug-message-name"}).html('node: '+sourceNode.id) - .appendTo(msg) + .appendTo(metaRow) .click(function(evt) { evt.preventDefault(); config.messageSourceClick(sourceNode.id); }); } else if (name) { - $(''+name+'').appendTo(msg); + $(''+name+'').appendTo(metaRow); } // NOTE: relying on function error to have a "type" that all other msgs don't if (o.hasOwnProperty("type") && (o.type === "function")) { @@ -222,12 +223,12 @@ RED.debug = (function() { errorLvlType = 'warn'; } $(msg).addClass('debug-message-level-' + errorLvl); - $('function : (' + errorLvlType + ')').appendTo(msg); + $('function : (' + errorLvlType + ')').appendTo(metaRow); } else { $(''+ - (o.topic?topic+' : ':'')+ - (o.property?'msg.'+property:'msg')+" : "+format+ - '').appendTo(msg); + (o.topic?topic+' : ':'')+ + (o.property?'msg.'+property:'msg')+" : "+format+ + '').appendTo(metaRow); } if (format === 'Object' || /^array/.test(format) || format === 'boolean' || format === 'number'||/error/i.test(format) ) { payload = JSON.parse(payload); From eaa4b76ede164ff84512a4dcd85c7053d3cea706 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 18 Nov 2016 16:38:48 +0000 Subject: [PATCH 08/44] Update jsonata version --- editor/vendor/jsonata/jsonata.js | 299 +++++++++++++++---------------- package.json | 2 +- 2 files changed, 150 insertions(+), 151 deletions(-) diff --git a/editor/vendor/jsonata/jsonata.js b/editor/vendor/jsonata/jsonata.js index 814bba28f..6c4ff877e 100644 --- a/editor/vendor/jsonata/jsonata.js +++ b/editor/vendor/jsonata/jsonata.js @@ -4,7 +4,6 @@ * This project is licensed under the MIT License, see LICENSE */ -'use strict'; /** * @module JSONata * @description JSON query and transformation language @@ -17,6 +16,8 @@ * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression */ var jsonata = (function() { + 'use strict'; + var operators = { '.': 75, '[': 80, @@ -141,7 +142,7 @@ var jsonata = (function() { position += 4; } else { throw { - message: "The escape sequence \\u must be followed by 4 hex digits at column " + position, + message: "The escape sequence \\u must be followed by 4 hex digits", stack: (new Error()).stack, position: position }; @@ -149,7 +150,7 @@ var jsonata = (function() { } else { // illegal escape sequence throw { - message: 'unsupported escape sequence: \\' + currentChar + ' at column ' + position, + message: 'unsupported escape sequence: \\' + currentChar, stack: (new Error()).stack, position: position, token: currentChar @@ -165,7 +166,7 @@ var jsonata = (function() { position++; } throw { - message: 'no terminating quote found in string literal at column ' + position, + message: 'no terminating quote found in string literal', stack: (new Error()).stack, position: position }; @@ -180,7 +181,7 @@ var jsonata = (function() { return create('number', num); } else { throw { - message: 'Number out of range: ' + match[0] + ' at column ' + position, + message: 'Number out of range: ' + match[0], stack: (new Error()).stack, position: position, token: match[0] @@ -270,7 +271,7 @@ var jsonata = (function() { // unexpected end of buffer msg = "Syntax error: expected '" + id + "' before end of expression"; } else { - msg = "Syntax error: expected '" + id + "', got '" + node.id + "' at column " + node.position; + msg = "Syntax error: expected '" + id + "', got '" + node.id; } throw { message: msg , @@ -297,7 +298,7 @@ var jsonata = (function() { symbol = symbol_table[value]; if (!symbol) { throw { - message: "Unknown operator: " + value + " at column " + next_token.position, + message: "Unknown operator: " + value, stack: (new Error()).stack, position: next_token.position, token: value @@ -310,10 +311,10 @@ var jsonata = (function() { type = "literal"; symbol = symbol_table["(literal)"]; break; - /* istanbul ignore next */ + /* istanbul ignore next */ default: throw { - message: "Unexpected token:" + value + " at column " + next_token.position, + message: "Unexpected token:" + value, stack: (new Error()).stack, position: next_token.position, token: value @@ -483,28 +484,7 @@ var jsonata = (function() { return this; }); - // object constructor - prefix("{", function () { - var a = []; - if (node.id !== "}") { - for (;;) { - var n = expression(0); - advance(":"); - var v = expression(0); - a.push([n, v]); // holds an array of name/value expression pairs - if (node.id !== ",") { - break; - } - advance(","); - } - } - advance("}"); - this.lhs = a; - this.type = "unary"; - return this; - }); - - // array constructor + // array constructor prefix("[", function () { var a = []; if (node.id !== "]") { @@ -532,21 +512,57 @@ var jsonata = (function() { // filter - predicate or array index infix("[", operators['['], function (left) { - this.lhs = left; - this.rhs = expression(operators[']']); - this.type = 'binary'; - advance("]"); - return this; + if(node.id === "]") { + // empty predicate means maintain singleton arrays in the output + var step = left; + while(step && step.type === 'binary' && step.value === '[') { + step = step.lhs; + } + step.keepArray = true; + advance("]"); + return left; + } else { + this.lhs = left; + this.rhs = expression(operators[']']); + this.type = 'binary'; + advance("]"); + return this; + } }); - // aggregator - infix("{", operators['{'], function (left) { - this.lhs = left; - this.rhs = expression(operators['}']); - this.type = 'binary'; + var objectParser = function (left) { + var a = []; + if (node.id !== "}") { + for (;;) { + var n = expression(0); + advance(":"); + var v = expression(0); + a.push([n, v]); // holds an array of name/value expression pairs + if (node.id !== ",") { + break; + } + advance(","); + } + } advance("}"); + if(typeof left === 'undefined') { + // NUD - unary prefix form + this.lhs = a; + this.type = "unary"; + } else { + // LED - binary infix form + this.lhs = left; + this.rhs = a; + this.type = 'binary'; + } return this; - }); + }; + + // object constructor + prefix("{", objectParser); + + // object grouping + infix("{", operators['{'], objectParser); // if/then/else ternary operator ?: infix("?", operators['?'], function (left) { @@ -600,13 +616,16 @@ var jsonata = (function() { case 'binary': switch (expr.value) { case '.': - var step = post_parse(expr.lhs); - if (Array.isArray(step)) { - Array.prototype.push.apply(result, step); + var lstep = post_parse(expr.lhs); + if (lstep.type === 'path') { + Array.prototype.push.apply(result, lstep); } else { - result.push(step); + result.push(lstep); + } + var rest = post_parse(expr.rhs); + if(rest.type !== 'path') { + rest = [rest]; } - var rest = [post_parse(expr.rhs)]; Array.prototype.push.apply(result, rest); result.type = 'path'; break; @@ -615,31 +634,41 @@ var jsonata = (function() { // LHS is a step or a predicated step // RHS is the predicate expr result = post_parse(expr.lhs); - if (typeof result.aggregate !== 'undefined') { + var step = result; + if(result.type === 'path') { + step = result[result.length - 1]; + } + if (typeof step.group !== 'undefined') { throw { - message: 'A predicate cannot follow an aggregate in a step. Error at column: ' + expr.position, + message: 'A predicate cannot follow a grouping expression in a step. Error at column: ' + expr.position, stack: (new Error()).stack, position: expr.position }; } - if (typeof result.predicate === 'undefined') { - result.predicate = []; + if (typeof step.predicate === 'undefined') { + step.predicate = []; } - result.predicate.push(post_parse(expr.rhs)); + step.predicate.push(post_parse(expr.rhs)); break; case '{': - // aggregate + // group-by // LHS is a step or a predicated step - // RHS is the predicate expr + // RHS is the object constructor expr result = post_parse(expr.lhs); - if (typeof result.aggregate !== 'undefined') { + if (typeof result.group !== 'undefined') { throw { - message: 'Each step can only have one aggregator. Error at column: ' + expr.position, + message: 'Each step can only have one grouping expression. Error at column: ' + expr.position, stack: (new Error()).stack, position: expr.position }; } - result.aggregate = post_parse(expr.rhs); + // object constructor - process each pair + result.group = { + lhs: expr.rhs.map(function (pair) { + return [post_parse(pair[0]), post_parse(pair[1])]; + }), + position: expr.position + }; break; default: result = {type: expr.type, value: expr.value, position: expr.position}; @@ -700,6 +729,9 @@ var jsonata = (function() { // if so, need to mark the block as one that needs to create a new frame break; case 'name': + result = [expr]; + result.type = 'path'; + break; case 'literal': case 'wildcard': case 'descendant': @@ -716,7 +748,7 @@ var jsonata = (function() { result = expr; } else { throw { - message: "Syntax error: " + expr.value + " at column " + expr.position, + message: "Syntax error: " + expr.value, stack: (new Error()).stack, position: expr.position, token: expr.value @@ -724,7 +756,7 @@ var jsonata = (function() { } break; default: - var reason = "Unknown expression type: " + expr.value + " at column " + expr.position; + var reason = "Unknown expression type: " + expr.value; /* istanbul ignore else */ if (expr.id === '(end)') { reason = "Syntax error: unexpected end of expression"; @@ -747,7 +779,7 @@ var jsonata = (function() { var expr = expression(0); if (node.id !== '(end)') { throw { - message: "Syntax error: " + node.value + " at column " + node.position, + message: "Syntax error: " + node.value, stack: (new Error()).stack, position: node.position, token: node.value @@ -755,12 +787,6 @@ var jsonata = (function() { } expr = post_parse(expr); - // a single name token is a single step location path - if (expr.type === 'name') { - expr = [expr]; - expr.type = 'path'; - } - return expr; }; @@ -865,8 +891,8 @@ var jsonata = (function() { if (expr.hasOwnProperty('predicate')) { result = applyPredicates(expr.predicate, result, environment); } - if (expr.hasOwnProperty('aggregate')) { - result = applyAggregate(expr.aggregate, result, environment); + if (expr.hasOwnProperty('group')) { + result = evaluateGroupExpression(expr.group, result, environment); } var exitCallback = environment.lookup('__evaluate_exit'); @@ -887,6 +913,7 @@ var jsonata = (function() { function evaluatePath(expr, input, environment) { var result; var inputSequence; + var keepSingletonArray = false; // expr is an array of steps // if the first step is a variable reference ($...), including root reference ($$), // then the path is absolute rather than relative @@ -898,7 +925,11 @@ var jsonata = (function() { } // evaluate each step in turn - expr.forEach(function (step) { + for(var ii = 0; ii < expr.length; ii++) { + var step = expr[ii]; + if(step.keepArray === true) { + keepSingletonArray = true; + } var resultSequence = []; result = undefined; // if input is not an array, make it so @@ -913,35 +944,36 @@ var jsonata = (function() { if (expr.length > 1 && step.type === 'literal') { step.type = 'name'; } - if (step.value === '{') { - if(typeof input !== 'undefined') { - result = evaluateGroupExpression(step, inputSequence, environment); - } - } else { - inputSequence.forEach(function (item) { - var res = evaluate(step, item, environment); - if (typeof res !== 'undefined') { - if (Array.isArray(res)) { - // is res an array - if so, flatten it into the parent array - res.forEach(function (innerRes) { - if (typeof innerRes !== 'undefined') { - resultSequence.push(innerRes); - } - }); - } else { - resultSequence.push(res); - } + inputSequence.forEach(function (item) { + var res = evaluate(step, item, environment); + if (typeof res !== 'undefined') { + if (Array.isArray(res) && (step.value !== '[' )) { + // is res an array - if so, flatten it into the parent array + res.forEach(function (innerRes) { + if (typeof innerRes !== 'undefined') { + resultSequence.push(innerRes); + } + }); + } else { + resultSequence.push(res); } - }); - if (resultSequence.length == 1) { - result = resultSequence[0]; - } else if (resultSequence.length > 1) { - result = resultSequence; } + }); + if (resultSequence.length == 1) { + if(keepSingletonArray) { + result = resultSequence; + } else { + result = resultSequence[0]; + } + } else if (resultSequence.length > 1) { + result = resultSequence; } + if(typeof result === 'undefined') { + break; + } input = result; - }); + } return result; } @@ -1016,35 +1048,6 @@ var jsonata = (function() { return result; } - /** - * Apply aggregate to input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {{}} Result after applying aggregate - */ - function applyAggregate(expr, input, environment) { - var result = {}; - // this is effectively a 'reduce' HOF (fold left) - // if the input is a singleton, then just return this as the result - // otherwise iterate over the input array and aggregate the result - if (Array.isArray(input)) { - // create a new frame to limit the scope - var aggEnv = createFrame(environment); - // the variable $@ will hold the aggregated value, initialize this to the first array item - aggEnv.bind('_', input[0]); - // loop over the remainder of the array - for (var index = 1; index < input.length; index++) { - var reduce = evaluate(expr, input[index], aggEnv); - aggEnv.bind('_', reduce); - } - result = aggEnv.lookup('_'); - } else { - result = input; - } - return result; - } - /** * Evaluate binary expression against input data * @param {Object} expr - JSONata expression @@ -1108,7 +1111,7 @@ var jsonata = (function() { result = -result; } else { throw { - message: "Cannot negate a non-numeric value: " + result + " at column " + expr.position, + message: "Cannot negate a non-numeric value: " + result, stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1122,11 +1125,10 @@ var jsonata = (function() { expr.lhs.forEach(function (item) { var value = evaluate(item, input, environment); if (typeof value !== 'undefined') { - if (item.value === '..') { - // array generated by the range operator - merge into results - result = functionAppend(result, value); - } else { + if(item.value === '[') { result.push(value); + } else { + result = functionAppend(result, value); } } }); @@ -1289,7 +1291,7 @@ var jsonata = (function() { if (!isNumeric(lhs)) { throw { - message: 'LHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position, + message: 'LHS of ' + expr.value + ' operator must evaluate to a number', stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1298,7 +1300,7 @@ var jsonata = (function() { } if (!isNumeric(rhs)) { throw { - message: 'RHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position, + message: 'RHS of ' + expr.value + ' operator must evaluate to a number', stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1413,11 +1415,11 @@ var jsonata = (function() { switch (expr.value) { case 'and': result = functionBoolean(evaluate(expr.lhs, input, environment)) && - functionBoolean(evaluate(expr.rhs, input, environment)); + functionBoolean(evaluate(expr.rhs, input, environment)); break; case 'or': result = functionBoolean(evaluate(expr.lhs, input, environment)) || - functionBoolean(evaluate(expr.rhs, input, environment)); + functionBoolean(evaluate(expr.rhs, input, environment)); break; } return result; @@ -1468,7 +1470,7 @@ var jsonata = (function() { // key has to be a string if (typeof key !== 'string') { throw { - message: 'Key in object structure must evaluate to a string. Got: ' + key + ' at column ' + expr.position, + message: 'Key in object structure must evaluate to a string. Got: ' + key, stack: (new Error()).stack, position: expr.position, value: key @@ -1518,7 +1520,7 @@ var jsonata = (function() { if (!Number.isInteger(lhs)) { throw { - message: 'LHS of range operator (..) must evaluate to an integer at column ' + expr.position, + message: 'LHS of range operator (..) must evaluate to an integer', stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1527,7 +1529,7 @@ var jsonata = (function() { } if (!Number.isInteger(rhs)) { throw { - message: 'RHS of range operator (..) must evaluate to an integer at column ' + expr.position, + message: 'RHS of range operator (..) must evaluate to an integer', stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1555,11 +1557,11 @@ var jsonata = (function() { var value = evaluate(expr.rhs, input, environment); if (expr.lhs.type !== 'variable') { throw { - message: "Left hand side of := must be a variable name (start with $) at column " + expr.position, + message: "Left hand side of := must be a variable name (start with $)", stack: (new Error()).stack, position: expr.position, token: expr.value, - value: expr.lhs.value + value: expr.lhs.type === 'path' ? expr.lhs[0].value : expr.lhs.value }; } environment.bind(expr.lhs.value, value); @@ -1645,13 +1647,13 @@ var jsonata = (function() { // evaluate it generically first, then check that it is a function. Throw error if not. var proc = evaluate(expr.procedure, input, environment); - if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) { + if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure[0].value)) { // help the user out here if they simply forgot the leading $ throw { - message: 'Attempted to invoke a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + message: 'Attempted to invoke a non-function. Did you mean \'$' + expr.procedure[0].value + '\'?', stack: (new Error()).stack, position: expr.position, - token: expr.procedure.value + token: expr.procedure[0].value }; } // apply the procedure @@ -1673,7 +1675,7 @@ var jsonata = (function() { // add the position field to the error err.position = expr.position; // and the function identifier - err.token = expr.procedure.value; + err.token = expr.procedure.type === 'path' ? expr.procedure[0].value : expr.procedure.value; throw err; } return result; @@ -1745,13 +1747,13 @@ var jsonata = (function() { }); // lookup the procedure var proc = evaluate(expr.procedure, input, environment); - if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) { + if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure[0].value)) { // help the user out here if they simply forgot the leading $ throw { - message: 'Attempted to partially apply a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + message: 'Attempted to partially apply a non-function. Did you mean \'$' + expr.procedure[0].value + '\'?', stack: (new Error()).stack, position: expr.position, - token: expr.procedure.value + token: expr.procedure[0].value }; } if (proc && proc.lambda) { @@ -1760,10 +1762,10 @@ var jsonata = (function() { result = partialApplyNativeFunction(proc, evaluatedArgs); } else { throw { - message: 'Attempted to partially apply a non-function at column ' + expr.position, + message: 'Attempted to partially apply a non-function', stack: (new Error()).stack, position: expr.position, - token: expr.procedure.value + token: expr.procedure.type === 'path' ? expr.procedure[0].value : expr.procedure.value }; } return result; @@ -2097,8 +2099,8 @@ var jsonata = (function() { } else str = JSON.stringify(arg, function (key, val) { return (typeof val !== 'undefined' && val !== null && val.toPrecision && isNumeric(val)) ? Number(val.toPrecision(13)) : - (val && isLambda(val)) ? '' : - (typeof val === 'function') ? '' : val; + (val && isLambda(val)) ? '' : + (typeof val === 'function') ? '' : val; }); return str; } @@ -2788,9 +2790,6 @@ var jsonata = (function() { }, assign: function (name, value) { environment.bind(name, value); - }, - ast: function() { - return ast; } }; } diff --git a/package.json b/package.json index 33de70e92..819697fec 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "fs.notify":"0.0.4", "i18next":"1.10.6", "is-utf8":"0.2.1", - "jsonata":"1.0.7", + "jsonata":"1.0.10", "media-typer": "0.3.0", "mqtt": "1.14.1", "mustache": "2.2.1", From 95b2675f03b5ceb0b2b414f4fb6fdf4e7f23d497 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 22 Nov 2016 13:14:52 +0000 Subject: [PATCH 09/44] Support query and search paths in url when opening debug sub window --- nodes/core/core/58-debug.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nodes/core/core/58-debug.html b/nodes/core/core/58-debug.html index 45d00f909..c784bbdf2 100644 --- a/nodes/core/core/58-debug.html +++ b/nodes/core/core/58-debug.html @@ -184,9 +184,10 @@ RED.comms.subscribe("debug",this.handleDebugMessage); RED.events.on("workspace:change", this.refreshMessageList); + $("#debug-tab-open").click(function(e) { e.preventDefault(); - subWindow = window.open(document.location.toString().replace(/#.*$/,"")+"debug/view/view.html","nodeREDDebugView","menubar=no,location=no,toolbar=no,chrome,height=500,width=600"); + subWindow = window.open(document.location.toString().replace(/[?#].*$/,"")+"debug/view/view.html"+document.location.search,"nodeREDDebugView","menubar=no,location=no,toolbar=no,chrome,height=500,width=600"); subWindow.onload = function() { subWindow.postMessage({event:"workspaceChange",activeWorkspace:RED.workspaces.active()},"*"); } From fa9a7e725be27b388fee3253f0d70d901c8174ab Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 22 Nov 2016 22:57:05 +0000 Subject: [PATCH 10/44] Sort quick-add types and add most-recent used type section --- editor/js/ui/typeSearch.js | 140 +++++++++++++++++++++++-------------- editor/sass/search.scss | 23 +++++- 2 files changed, 107 insertions(+), 56 deletions(-) diff --git a/editor/js/ui/typeSearch.js b/editor/js/ui/typeSearch.js index 7ce14e810..d9b02338e 100644 --- a/editor/js/ui/typeSearch.js +++ b/editor/js/ui/typeSearch.js @@ -13,6 +13,8 @@ RED.typeSearch = (function() { var activeFilter = ""; var addCallback; + var typesUsed = {}; + function search(val) { activeFilter = val.toLowerCase(); var visible = searchResults.editableList('filter'); @@ -93,12 +95,17 @@ RED.typeSearch = (function() { if (activeFilter === "" ) { return true; } - + if (data.recent) { + return false; + } return (activeFilter==="")||(data.index.indexOf(activeFilter) > -1); }, addItem: function(container,i,object) { var def = object.def; object.index = object.type.toLowerCase(); + if (object.separator) { + container.addClass("red-ui-search-result-separator") + } var div = $('',{href:'#',class:"red-ui-search-result"}).appendTo(container); var nodeDiv = $('
',{class:"red-ui-search-result-node"}).appendTo(div); @@ -118,19 +125,17 @@ RED.typeSearch = (function() { var iconContainer = $('
',{class:"palette_icon_container"}).appendTo(nodeDiv); $('
',{class:"palette_icon",style:"background-image: url(icons/"+icon_url+")"}).appendTo(iconContainer); - var contentDiv = $('
',{class:"red-ui-search-result-description"}).appendTo(div); - - var label = object.type; - if (typeof def.paletteLabel !== "undefined") { - try { - label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||""; - label += " ("+object.type+")"; - object.index += "|"+label.toLowerCase(); - } catch(err) { - console.log("Definition error: "+object.type+".paletteLabel",err); - } + if (def.inputs > 0) { + $('
',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv); + } + if (def.outputs > 0) { + $('
',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv); } + var contentDiv = $('
',{class:"red-ui-search-result-description"}).appendTo(div); + + var label = object.label; + object.index += "|"+label.toLowerCase(); $('
',{class:"red-ui-search-result-node-label"}).html(label).appendTo(contentDiv); @@ -145,6 +150,7 @@ RED.typeSearch = (function() { } function confirm(def) { hide(); + typesUsed[def.type] = Date.now(); addCallback(def.type); } @@ -205,41 +211,83 @@ RED.typeSearch = (function() { $(document).off('click.type-search'); } } + + function getTypeLabel(type, def) { + var label = type; + if (typeof def.paletteLabel !== "undefined") { + try { + label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||""; + label += " ("+type+")"; + } catch(err) { + console.log("Definition error: "+type+".paletteLabel",err); + } + } + return label; + } + function refreshTypeList() { + var i; searchResults.editableList('empty'); searchInput.searchBox('value',''); selected = -1; - var common = { - "debug" : false, - "inject" : false, - "function": false - }; - var nodeTypes = RED.nodes.registry.getNodeTypes().filter(function(n) { - if (common.hasOwnProperty(n)) { - common[n] = true; - return false; - } - return true; + var common = [ + 'debug','inject','function','change','switch' + ]; + + var recentlyUsed = Object.keys(typesUsed); + recentlyUsed.sort(function(a,b) { + return typesUsed[b]-typesUsed[a]; + }); + recentlyUsed = recentlyUsed.filter(function(t) { + return common.indexOf(t) === -1; }); - // Just in case a core node has been disabled - if (common["function"]) { - nodeTypes.unshift("function"); - } - if (common["inject"]) { - nodeTypes.unshift("inject"); - } - if (common["debug"]) { - nodeTypes.unshift("debug"); - } - - var i; - for (i=0;i Date: Wed, 23 Nov 2016 10:58:19 +0000 Subject: [PATCH 11/44] Add editableList api doc comments --- editor/js/ui/common/editableList.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/editor/js/ui/common/editableList.js b/editor/js/ui/common/editableList.js index 87939dca5..db1d861f0 100644 --- a/editor/js/ui/common/editableList.js +++ b/editor/js/ui/common/editableList.js @@ -27,6 +27,9 @@ * - removable : boolean - whether to display delete button on items * - addItem : function(row,index,itemData) - when an item is added * - removeItem : function(itemData) - called when an item is removed + * - filter : function(itemData) - called for each item to determine if it should be shown + * - sort : function(itemDataA,itemDataB) - called to sort items + * - scrollOnAdd : boolean - whether to scroll to newly added items * methods: * - addItem(itemData) * - removeItem(itemData) @@ -34,6 +37,9 @@ * - height(height) * - items() * - empty() + * - filter(filter) + * - sort(sort) + * - length() */ $.widget( "nodered.editableList", { _create: function() { From de64fc8b8d2e22c696861fc5f22ca31c8d09b7c7 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 23 Nov 2016 10:58:38 +0000 Subject: [PATCH 12/44] Update rather than hide install button after success install --- editor/js/ui/palette-editor.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/editor/js/ui/palette-editor.js b/editor/js/ui/palette-editor.js index 1157ffcd6..9caf1024d 100644 --- a/editor/js/ui/palette-editor.js +++ b/editor/js/ui/palette-editor.js @@ -754,7 +754,9 @@ RED.palette.editor = (function() { refreshNodeModule(ns.module); for (var i=0;i Date: Wed, 23 Nov 2016 23:15:30 +0000 Subject: [PATCH 13/44] Include jsonata from dependency on build and improve func highlight --- Gruntfile.js | 2 +- editor/images/typedInput/expr.png | Bin 786 -> 563 bytes editor/js/ui/editor.js | 78 +- editor/vendor/jsonata/formatter.js | 40 +- editor/vendor/jsonata/jsonata.js | 2807 ------------------------- editor/vendor/jsonata/mode-jsonata.js | 17 +- 6 files changed, 120 insertions(+), 2824 deletions(-) delete mode 100644 editor/vendor/jsonata/jsonata.js diff --git a/Gruntfile.js b/Gruntfile.js index 9c44b782e..bf6994aca 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -154,7 +154,7 @@ module.exports = function(grunt) { // bootstrap/FA/jquery ], "public/vendor/jsonata/jsonata.js": [ - "editor/vendor/jsonata/jsonata.js", + "node_modules/jsonata/jsonata.js", "editor/vendor/jsonata/formatter.js" ] } diff --git a/editor/images/typedInput/expr.png b/editor/images/typedInput/expr.png index 74d9516aee3c9d64b8f0455bdab7f82b94208433..704105ce5846cbcd5233b418fdecf8bb7b6da5f7 100644 GIT binary patch delta 512 zcmV+b0{{Jz2D1c^J%0h20004*0oOVK{{R302XskIMF-#u7Y-pAg5-ZZ0005NNklQ8+S=1kV4H9fLlN;1lbN|#GHHe} ziapqm!!LC+m7V!#qa1GO$IajnRM?_z>DgC!U-@XBxEO-CF6w<{20000VMMNkR7YeOb+I|VYgC9a^f`1h^y6R&k4M=8)VL;J^ zVsK#>N{lWWnCKu zDEt^To6Q02!K#Y(U3xz5>{k&oT4z*nj{agm~Ch3!-JeB^e1AiA1)T`HKWR zXPV~au>p(4Vt<>6@=BDeV*>&}tJQj>sBUMo*<)h^Rw|W`%=}V{@F+9i8XK?!i7Tqr zzz1Ji044xiQ8R#qWm%sBkFd||5rCDzQ<}@=rT{Dqa6|+ObY1r*5gqSS*BQewt_BKJ z)5*+llufh}Skq*)*)yK!?GjPwU%<>fOGIx0L^`@Z&3|U|wBtBG4;&#gFG(;k^M-BP zADDSdGCQJa+Cm^HRnsYiSnJfTFzih33p7nrC!z}yyw+$mwgI44t8D=IAvKSgPN&Zw z2q=V5XK&qcoIL=jR;w<6r^7@!5SU0LGysdr2z0#|?RGn_9J`mx*id&!}*uIpYO3D|kn<>txvG44ZS T0R{}Q00000NkvXXu0mjfEtEpG diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index 5e9775ec8..915c29649 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -1415,7 +1415,7 @@ RED.editor = (function() { var trayBody = tray.find('.editor-tray-body'); var dialogForm = buildEditForm(tray,'dialog-form','_expression','editor'); var funcSelect = $("#node-input-expression-func"); - jsonata.functions.forEach(function(f) { + Object.keys(jsonata.functions).forEach(function(f) { funcSelect.append($("").val(f).text(f)); }) funcSelect.change(function(e) { @@ -1436,14 +1436,75 @@ RED.editor = (function() { enableLiveAutocompletion: true } }); - expressionEditor.getSession().setValue(value||"",-1); + var currentToken = null; + var currentTokenPos = -1; + var currentFunctionMarker = null; + expressionEditor.getSession().setValue(value||"",-1); expressionEditor.on("changeSelection", function() { var c = expressionEditor.getCursorPosition(); var token = expressionEditor.getSession().getTokenAt(c.row,c.column); - // console.log(token); - if (token && token.type === 'keyword') { - funcSelect.val(token.value).change(); + if (token !== currentToken || (token && /paren/.test(token.type) && c.column !== currentTokenPos)) { + currentToken = token; + var r,p; + var scopedFunction = null; + if (token && token.type === 'keyword') { + r = c.row; + scopedFunction = token; + } else { + var depth = 0; + var next = false; + if (token) { + if (token.type === 'paren.rparen') { + // If this is a block of parens ')))', set + // depth to offset against the cursor position + // within the block + currentTokenPos = c.column; + depth = c.column - (token.start + token.value.length); + } + r = c.row; + p = token.index; + } else { + r = c.row-1; + p = -1; + } + while ( scopedFunction === null && r > -1) { + var rowTokens = expressionEditor.getSession().getTokens(r); + if (p === -1) { + p = rowTokens.length-1; + } + while (p > -1) { + var type = rowTokens[p].type; + if (next) { + if (type === 'keyword') { + scopedFunction = rowTokens[p]; + // console.log("HIT",scopedFunction); + break; + } + next = false; + } + if (type === 'paren.lparen') { + depth-=rowTokens[p].value.length; + } else if (type === 'paren.rparen') { + depth+=rowTokens[p].value.length; + } + if (depth < 0) { + next = true; + depth = 0; + } + // console.log(r,p,depth,next,rowTokens[p]); + p--; + } + if (!scopedFunction) { + r--; + } + } + } + expressionEditor.session.removeMarker(currentFunctionMarker); + if (scopedFunction) { + //console.log(token,.map(function(t) { return t.type})); + funcSelect.val(scopedFunction.value).change(); + } } }); @@ -1452,10 +1513,8 @@ RED.editor = (function() { e.preventDefault(); var pos = expressionEditor.getCursorPosition(); var f = funcSelect.val(); - var args = RED._('jsonata:'+f+".args",{defaultValue:''}); - expressionEditor.insert(f+"("+args+")"); - pos.column += f.length+1; - expressionEditor.moveCursorToPosition(pos); + var snippet = jsonata.getFunctionSnippet(f); + expressionEditor.insertSnippet(snippet); expressionEditor.focus(); }) }, @@ -1521,7 +1580,6 @@ RED.editor = (function() { } },100); } - return editor; } } diff --git a/editor/vendor/jsonata/formatter.js b/editor/vendor/jsonata/formatter.js index 036de6852..f0d231a79 100644 --- a/editor/vendor/jsonata/formatter.js +++ b/editor/vendor/jsonata/formatter.js @@ -105,5 +105,43 @@ } jsonata.format = formatExpression; - jsonata.functions = ["$sum", "$count", "$max", "$min", "$average", "$string", "$substring", "$substringBefore", "$substringAfter", "$lowercase", "$uppercase", "$length", "$split", "$join", "$number", "$boolean", "$not", "$map", "$reduce", "$keys", "$lookup", "$append", "$exists", "$spread"] + jsonata.functions = + { + '$append':{ args:['array','array'] }, + '$average':{ args:['value'] }, + '$boolean':{ args:['value'] }, + '$count':{ args:['array'] }, + '$exists':{ args:['value'] }, + '$join':{ args:['array','separator'] }, + '$keys':{ args:['object'] }, + '$length':{ args:['string'] }, + '$lookup':{ args:['object','key'] }, + '$lowercase':{ args:['string'] }, + '$map':{ args:[] }, + '$max':{ args:['array'] }, + '$min':{ args:['array'] }, + '$not':{ args:['value'] }, + '$number':{ args:['value'] }, + '$reduce':{ args:[] }, + '$split':{ args:['string','separator','limit'] }, + '$spread':{ args:['object'] }, + '$string':{ args:['value'] }, + '$substring':{ args:['string','start','length'] }, + '$substringAfter':{ args:['string','chars'] }, + '$substringBefore':{ args:['string','chars'] }, + '$sum':{ args:['array'] }, + '$uppercase':{ args:['string'] } + } + jsonata.getFunctionSnippet = function(fn) { + var snippetText = ""; + if (jsonata.functions.hasOwnProperty(fn)) { + var def = jsonata.functions[fn]; + snippetText = "\\"+fn+"("; + if (def.args) { + snippetText += def.args.map(function(a,i) { return "${"+(i+1)+":"+a+"}"}).join(", "); + } + snippetText += ")\n" + } + return snippetText; + } })(); diff --git a/editor/vendor/jsonata/jsonata.js b/editor/vendor/jsonata/jsonata.js deleted file mode 100644 index 6c4ff877e..000000000 --- a/editor/vendor/jsonata/jsonata.js +++ /dev/null @@ -1,2807 +0,0 @@ -/** - * © Copyright IBM Corp. 2016 All Rights Reserved - * Project name: JSONata - * This project is licensed under the MIT License, see LICENSE - */ - -/** - * @module JSONata - * @description JSON query and transformation language - */ - -/** - * jsonata - * @function - * @param {Object} expr - JSONata expression - * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression - */ -var jsonata = (function() { - 'use strict'; - - var operators = { - '.': 75, - '[': 80, - ']': 0, - '{': 70, - '}': 0, - '(': 80, - ')': 0, - ',': 0, - '@': 75, - '#': 70, - ';': 80, - ':': 80, - '?': 20, - '+': 50, - '-': 50, - '*': 60, - '/': 60, - '%': 60, - '|': 20, - '=': 40, - '<': 40, - '>': 40, - '`': 80, - '**': 60, - '..': 20, - ':=': 10, - '!=': 40, - '<=': 40, - '>=': 40, - 'and': 30, - 'or': 25, - 'in': 40, - '&': 50, - '!': 0 // not an operator, but needed as a stop character for name tokens - }; - - var escapes = { // JSON string escape sequences - see json.org - '"': '"', - '\\': '\\', - '/': '/', - 'b': '\b', - 'f': '\f', - 'n': '\n', - 'r': '\r', - 't': '\t' - }; - - // Tokenizer (lexer) - invoked by the parser to return one token at a time - var tokenizer = function (path) { - var position = 0; - var length = path.length; - - var create = function (type, value) { - var obj = {type: type, value: value, position: position}; - return obj; - }; - - var next = function () { - if (position >= length) return null; - var currentChar = path.charAt(position); - // skip whitespace - while (position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { - position++; - currentChar = path.charAt(position); - } - // handle double-char operators - if (currentChar === '.' && path.charAt(position + 1) === '.') { - // double-dot .. range operator - position += 2; - return create('operator', '..'); - } - if (currentChar === ':' && path.charAt(position + 1) === '=') { - // := assignment - position += 2; - return create('operator', ':='); - } - if (currentChar === '!' && path.charAt(position + 1) === '=') { - // != - position += 2; - return create('operator', '!='); - } - if (currentChar === '>' && path.charAt(position + 1) === '=') { - // >= - position += 2; - return create('operator', '>='); - } - if (currentChar === '<' && path.charAt(position + 1) === '=') { - // <= - position += 2; - return create('operator', '<='); - } - if (currentChar === '*' && path.charAt(position + 1) === '*') { - // ** descendant wildcard - position += 2; - return create('operator', '**'); - } - // test for operators - if (operators.hasOwnProperty(currentChar)) { - position++; - return create('operator', currentChar); - } - // test for string literals - if (currentChar === '"' || currentChar === "'") { - var quoteType = currentChar; - // double quoted string literal - find end of string - position++; - var qstr = ""; - while (position < length) { - currentChar = path.charAt(position); - if (currentChar === '\\') { // escape sequence - position++; - currentChar = path.charAt(position); - if (escapes.hasOwnProperty(currentChar)) { - qstr += escapes[currentChar]; - } else if (currentChar === 'u') { - // \u should be followed by 4 hex digits - var octets = path.substr(position + 1, 4); - if (/^[0-9a-fA-F]+$/.test(octets)) { - var codepoint = parseInt(octets, 16); - qstr += String.fromCharCode(codepoint); - position += 4; - } else { - throw { - message: "The escape sequence \\u must be followed by 4 hex digits", - stack: (new Error()).stack, - position: position - }; - } - } else { - // illegal escape sequence - throw { - message: 'unsupported escape sequence: \\' + currentChar, - stack: (new Error()).stack, - position: position, - token: currentChar - }; - - } - } else if (currentChar === quoteType) { - position++; - return create('string', qstr); - } else { - qstr += currentChar; - } - position++; - } - throw { - message: 'no terminating quote found in string literal', - stack: (new Error()).stack, - position: position - }; - } - // test for numbers - var numregex = /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?/; - var match = numregex.exec(path.substring(position)); - if (match !== null) { - var num = parseFloat(match[0]); - if (!isNaN(num) && isFinite(num)) { - position += match[0].length; - return create('number', num); - } else { - throw { - message: 'Number out of range: ' + match[0], - stack: (new Error()).stack, - position: position, - token: match[0] - }; - } - } - // test for names - var i = position; - var ch; - var name; - for (;;) { - ch = path.charAt(i); - if (i == length || ' \t\n\r\v'.indexOf(ch) > -1 || operators.hasOwnProperty(ch)) { - if (path.charAt(position) === '$') { - // variable reference - name = path.substring(position + 1, i); - position = i; - return create('variable', name); - } else { - name = path.substring(position, i); - position = i; - switch (name) { - case 'and': - case 'or': - case 'in': - return create('operator', name); - case 'true': - return create('value', true); - case 'false': - return create('value', false); - case 'null': - return create('value', null); - default: - if (position == length && name === '') { - // whitespace at end of input - return null; - } - return create('name', name); - } - } - } else { - i++; - } - } - }; - - return next; - }; - - - // This parser implements the 'Top down operator precedence' algorithm developed by Vaughan R Pratt; http://dl.acm.org/citation.cfm?id=512931. - // and builds on the Javascript framework described by Douglas Crockford at http://javascript.crockford.com/tdop/tdop.html - // and in 'Beautiful Code', edited by Andy Oram and Greg Wilson, Copyright 2007 O'Reilly Media, Inc. 798-0-596-51004-6 - - var parser = function (source) { - var node; - var lexer; - - var symbol_table = {}; - - var base_symbol = { - nud: function () { - return this; - } - }; - - var symbol = function (id, bp) { - var s = symbol_table[id]; - bp = bp || 0; - if (s) { - if (bp >= s.lbp) { - s.lbp = bp; - } - } else { - s = Object.create(base_symbol); - s.id = s.value = id; - s.lbp = bp; - symbol_table[id] = s; - } - return s; - }; - - var advance = function (id) { - if (id && node.id !== id) { - var msg; - if(node.id === '(end)') { - // unexpected end of buffer - msg = "Syntax error: expected '" + id + "' before end of expression"; - } else { - msg = "Syntax error: expected '" + id + "', got '" + node.id; - } - throw { - message: msg , - stack: (new Error()).stack, - position: node.position, - token: node.id, - value: id - }; - } - var next_token = lexer(); - if (next_token === null) { - node = symbol_table["(end)"]; - return node; - } - var value = next_token.value; - var type = next_token.type; - var symbol; - switch (type) { - case 'name': - case 'variable': - symbol = symbol_table["(name)"]; - break; - case 'operator': - symbol = symbol_table[value]; - if (!symbol) { - throw { - message: "Unknown operator: " + value, - stack: (new Error()).stack, - position: next_token.position, - token: value - }; - } - break; - case 'string': - case 'number': - case 'value': - type = "literal"; - symbol = symbol_table["(literal)"]; - break; - /* istanbul ignore next */ - default: - throw { - message: "Unexpected token:" + value, - stack: (new Error()).stack, - position: next_token.position, - token: value - }; - } - - node = Object.create(symbol); - node.value = value; - node.type = type; - node.position = next_token.position; - return node; - }; - - // Pratt's algorithm - var expression = function (rbp) { - var left; - var t = node; - advance(); - left = t.nud(); - while (rbp < node.lbp) { - t = node; - advance(); - left = t.led(left); - } - return left; - }; - - // match infix operators - // - // left associative - var infix = function (id, bp, led) { - var bindingPower = bp || operators[id]; - var s = symbol(id, bindingPower); - s.led = led || function (left) { - this.lhs = left; - this.rhs = expression(bindingPower); - this.type = "binary"; - return this; - }; - return s; - }; - - // match infix operators - // - // right associative - var infixr = function (id, bp, led) { - var bindingPower = bp || operators[id]; - var s = symbol(id, bindingPower); - s.led = led || function (left) { - this.lhs = left; - this.rhs = expression(bindingPower - 1); // subtract 1 from bindingPower for right associative operators - this.type = "binary"; - return this; - }; - return s; - }; - - // match prefix operators - // - var prefix = function (id, nud) { - var s = symbol(id); - s.nud = nud || function () { - this.expression = expression(70); - this.type = "unary"; - return this; - }; - return s; - }; - - symbol("(end)"); - symbol("(name)"); - symbol("(literal)"); - symbol(":"); - symbol(";"); - symbol(","); - symbol(")"); - symbol("]"); - symbol("}"); - symbol(".."); // range operator - infix("."); // field reference - infix("+"); // numeric addition - infix("-"); // numeric subtraction - infix("*"); // numeric multiplication - infix("/"); // numeric division - infix("%"); // numeric modulus - infix("="); // equality - infix("<"); // less than - infix(">"); // greater than - infix("!="); // not equal to - infix("<="); // less than or equal - infix(">="); // greater than or equal - infix("&"); // string concatenation - infix("and"); // Boolean AND - infix("or"); // Boolean OR - infix("in"); // is member of array - infixr(":="); // bind variable - prefix("-"); // unary numeric negation - - // field wildcard (single level) - prefix('*', function () { - this.type = "wildcard"; - return this; - }); - - // descendant wildcard (multi-level) - prefix('**', function () { - this.type = "descendant"; - return this; - }); - - // function invocation - infix("(", operators['('], function (left) { - // left is is what we are trying to invoke - this.procedure = left; - this.type = 'function'; - this.arguments = []; - if (node.id !== ')') { - for (;;) { - if (node.type === 'operator' && node.id === '?') { - // partial function application - this.type = 'partial'; - this.arguments.push(node); - advance('?'); - } else { - this.arguments.push(expression(0)); - } - if (node.id !== ',') break; - advance(','); - } - } - advance(")"); - // if the name of the function is 'function' or λ, then this is function definition (lambda function) - if (left.type === 'name' && (left.value === 'function' || left.value === '\u03BB')) { - // all of the args must be VARIABLE tokens - this.arguments.forEach(function (arg, index) { - if (arg.type !== 'variable') { - throw { - message: 'Parameter ' + (index + 1) + ' of function definition must be a variable name (start with $)', - stack: (new Error()).stack, - position: arg.position, - token: arg.value - }; - } - }); - this.type = 'lambda'; - // parse the function body - advance('{'); - this.body = expression(0); - advance('}'); - } - return this; - }); - - // parenthesis - block expression - prefix("(", function () { - var expressions = []; - while (node.id !== ")") { - expressions.push(expression(0)); - if (node.id !== ";") { - break; - } - advance(";"); - } - advance(")"); - this.type = 'block'; - this.expressions = expressions; - return this; - }); - - // array constructor - prefix("[", function () { - var a = []; - if (node.id !== "]") { - for (;;) { - var item = expression(0); - if (node.id === "..") { - // range operator - var range = {type: "binary", value: "..", position: node.position, lhs: item}; - advance(".."); - range.rhs = expression(0); - item = range; - } - a.push(item); - if (node.id !== ",") { - break; - } - advance(","); - } - } - advance("]"); - this.lhs = a; - this.type = "unary"; - return this; - }); - - // filter - predicate or array index - infix("[", operators['['], function (left) { - if(node.id === "]") { - // empty predicate means maintain singleton arrays in the output - var step = left; - while(step && step.type === 'binary' && step.value === '[') { - step = step.lhs; - } - step.keepArray = true; - advance("]"); - return left; - } else { - this.lhs = left; - this.rhs = expression(operators[']']); - this.type = 'binary'; - advance("]"); - return this; - } - }); - - var objectParser = function (left) { - var a = []; - if (node.id !== "}") { - for (;;) { - var n = expression(0); - advance(":"); - var v = expression(0); - a.push([n, v]); // holds an array of name/value expression pairs - if (node.id !== ",") { - break; - } - advance(","); - } - } - advance("}"); - if(typeof left === 'undefined') { - // NUD - unary prefix form - this.lhs = a; - this.type = "unary"; - } else { - // LED - binary infix form - this.lhs = left; - this.rhs = a; - this.type = 'binary'; - } - return this; - }; - - // object constructor - prefix("{", objectParser); - - // object grouping - infix("{", operators['{'], objectParser); - - // if/then/else ternary operator ?: - infix("?", operators['?'], function (left) { - this.type = 'condition'; - this.condition = left; - this.then = expression(0); - if (node.id === ':') { - // else condition - advance(":"); - this.else = expression(0); - } - return this; - }); - - // tail call optimization - // this is invoked by the post parser to analyse lambda functions to see - // if they make a tail call. If so, it is replaced by a thunk which will - // be invoked by the trampoline loop during function application. - // This enables tail-recursive functions to be written without growing the stack - var tail_call_optimize = function(expr) { - var result; - if(expr.type === 'function') { - var thunk = {type: 'lambda', thunk: true, arguments: [], position: expr.position}; - thunk.body = expr; - result = thunk; - } else if(expr.type === 'condition') { - // analyse both branches - expr.then = tail_call_optimize(expr.then); - expr.else = tail_call_optimize(expr.else); - result = expr; - } else if(expr.type === 'block') { - // only the last expression in the block - var length = expr.expressions.length; - if(length > 0) { - expr.expressions[length - 1] = tail_call_optimize(expr.expressions[length - 1]); - } - result = expr; - } else { - result = expr; - } - return result; - }; - - // post-parse stage - // the purpose of this is flatten the parts of the AST representing location paths, - // converting them to arrays of steps which in turn may contain arrays of predicates. - // following this, nodes containing '.' and '[' should be eliminated from the AST. - var post_parse = function (expr) { - var result = []; - switch (expr.type) { - case 'binary': - switch (expr.value) { - case '.': - var lstep = post_parse(expr.lhs); - if (lstep.type === 'path') { - Array.prototype.push.apply(result, lstep); - } else { - result.push(lstep); - } - var rest = post_parse(expr.rhs); - if(rest.type !== 'path') { - rest = [rest]; - } - Array.prototype.push.apply(result, rest); - result.type = 'path'; - break; - case '[': - // predicated step - // LHS is a step or a predicated step - // RHS is the predicate expr - result = post_parse(expr.lhs); - var step = result; - if(result.type === 'path') { - step = result[result.length - 1]; - } - if (typeof step.group !== 'undefined') { - throw { - message: 'A predicate cannot follow a grouping expression in a step. Error at column: ' + expr.position, - stack: (new Error()).stack, - position: expr.position - }; - } - if (typeof step.predicate === 'undefined') { - step.predicate = []; - } - step.predicate.push(post_parse(expr.rhs)); - break; - case '{': - // group-by - // LHS is a step or a predicated step - // RHS is the object constructor expr - result = post_parse(expr.lhs); - if (typeof result.group !== 'undefined') { - throw { - message: 'Each step can only have one grouping expression. Error at column: ' + expr.position, - stack: (new Error()).stack, - position: expr.position - }; - } - // object constructor - process each pair - result.group = { - lhs: expr.rhs.map(function (pair) { - return [post_parse(pair[0]), post_parse(pair[1])]; - }), - position: expr.position - }; - break; - default: - result = {type: expr.type, value: expr.value, position: expr.position}; - result.lhs = post_parse(expr.lhs); - result.rhs = post_parse(expr.rhs); - } - break; - case 'unary': - result = {type: expr.type, value: expr.value, position: expr.position}; - if (expr.value === '[') { - // array constructor - process each item - result.lhs = expr.lhs.map(function (item) { - return post_parse(item); - }); - } else if (expr.value === '{') { - // object constructor - process each pair - result.lhs = expr.lhs.map(function (pair) { - return [post_parse(pair[0]), post_parse(pair[1])]; - }); - } else { - // all other unary expressions - just process the expression - result.expression = post_parse(expr.expression); - // if unary minus on a number, then pre-process - if (expr.value === '-' && result.expression.type === 'literal' && isNumeric(result.expression.value)) { - result = result.expression; - result.value = -result.value; - } - } - break; - case 'function': - case 'partial': - result = {type: expr.type, name: expr.name, value: expr.value, position: expr.position}; - result.arguments = expr.arguments.map(function (arg) { - return post_parse(arg); - }); - result.procedure = post_parse(expr.procedure); - break; - case 'lambda': - result = {type: expr.type, arguments: expr.arguments, position: expr.position}; - var body = post_parse(expr.body); - result.body = tail_call_optimize(body); - break; - case 'condition': - result = {type: expr.type, position: expr.position}; - result.condition = post_parse(expr.condition); - result.then = post_parse(expr.then); - if (typeof expr.else !== 'undefined') { - result.else = post_parse(expr.else); - } - break; - case 'block': - result = {type: expr.type, position: expr.position}; - // array of expressions - process each one - result.expressions = expr.expressions.map(function (item) { - return post_parse(item); - }); - // TODO scan the array of expressions to see if any of them assign variables - // if so, need to mark the block as one that needs to create a new frame - break; - case 'name': - result = [expr]; - result.type = 'path'; - break; - case 'literal': - case 'wildcard': - case 'descendant': - case 'variable': - result = expr; - break; - case 'operator': - // the tokens 'and' and 'or' might have been used as a name rather than an operator - if (expr.value === 'and' || expr.value === 'or' || expr.value === 'in') { - expr.type = 'name'; - result = post_parse(expr); - } else if (expr.value === '?') { - // partial application - result = expr; - } else { - throw { - message: "Syntax error: " + expr.value, - stack: (new Error()).stack, - position: expr.position, - token: expr.value - }; - } - break; - default: - var reason = "Unknown expression type: " + expr.value; - /* istanbul ignore else */ - if (expr.id === '(end)') { - reason = "Syntax error: unexpected end of expression"; - } - throw { - message: reason, - stack: (new Error()).stack, - position: expr.position, - token: expr.value - }; - } - return result; - }; - - // now invoke the tokenizer and the parser and return the syntax tree - - lexer = tokenizer(source); - advance(); - // parse the tokens - var expr = expression(0); - if (node.id !== '(end)') { - throw { - message: "Syntax error: " + node.value, - stack: (new Error()).stack, - position: node.position, - token: node.value - }; - } - expr = post_parse(expr); - - return expr; - }; - - /** - * Check if value is a finite number - * @param {float} n - number to evaluate - * @returns {boolean} True if n is a finite number - */ - function isNumeric(n) { - var isNum = false; - if(typeof n === 'number') { - var num = parseFloat(n); - isNum = !isNaN(num); - if (isNum && !isFinite(num)) { - throw { - message: "Number out of range", - value: n, - stack: (new Error()).stack - }; - } - } - return isNum; - } - - /** - * Returns true if the arg is an array of numbers - * @param {*} arg - the item to test - * @returns {boolean} True if arg is an array of numbers - */ - function isArrayOfNumbers(arg) { - var result = false; - if(Array.isArray(arg)) { - result = (arg.filter(function(item){return !isNumeric(item);}).length == 0); - } - return result; - } - - // Polyfill - /* istanbul ignore next */ - Number.isInteger = Number.isInteger || function(value) { - return typeof value === "number" && - isFinite(value) && - Math.floor(value) === value; - }; - - /** - * Evaluate expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluate(expr, input, environment) { - var result; - - var entryCallback = environment.lookup('__evaluate_entry'); - if(entryCallback) { - entryCallback(expr, input, environment); - } - - switch (expr.type) { - case 'path': - result = evaluatePath(expr, input, environment); - break; - case 'binary': - result = evaluateBinary(expr, input, environment); - break; - case 'unary': - result = evaluateUnary(expr, input, environment); - break; - case 'name': - result = evaluateName(expr, input, environment); - break; - case 'literal': - result = evaluateLiteral(expr, input, environment); - break; - case 'wildcard': - result = evaluateWildcard(expr, input, environment); - break; - case 'descendant': - result = evaluateDescendants(expr, input, environment); - break; - case 'condition': - result = evaluateCondition(expr, input, environment); - break; - case 'block': - result = evaluateBlock(expr, input, environment); - break; - case 'function': - result = evaluateFunction(expr, input, environment); - break; - case 'variable': - result = evaluateVariable(expr, input, environment); - break; - case 'lambda': - result = evaluateLambda(expr, input, environment); - break; - case 'partial': - result = evaluatePartialApplication(expr, input, environment); - break; - } - if (expr.hasOwnProperty('predicate')) { - result = applyPredicates(expr.predicate, result, environment); - } - if (expr.hasOwnProperty('group')) { - result = evaluateGroupExpression(expr.group, result, environment); - } - - var exitCallback = environment.lookup('__evaluate_exit'); - if(exitCallback) { - exitCallback(expr, input, environment, result); - } - - return result; - } - - /** - * Evaluate path expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluatePath(expr, input, environment) { - var result; - var inputSequence; - var keepSingletonArray = false; - // expr is an array of steps - // if the first step is a variable reference ($...), including root reference ($$), - // then the path is absolute rather than relative - if (expr[0].type === 'variable') { - expr[0].absolute = true; - } else if(expr[0].type === 'unary' && expr[0].value === '[') { - // array constructor - not relative to the input - input = [null];// dummy singleton sequence for first step - } - - // evaluate each step in turn - for(var ii = 0; ii < expr.length; ii++) { - var step = expr[ii]; - if(step.keepArray === true) { - keepSingletonArray = true; - } - var resultSequence = []; - result = undefined; - // if input is not an array, make it so - if (step.absolute === true) { - inputSequence = [input]; // dummy singleton sequence for first (absolute) step - } else if (Array.isArray(input)) { - inputSequence = input; - } else { - inputSequence = [input]; - } - // if there is more than one step in the path, handle quoted field names as names not literals - if (expr.length > 1 && step.type === 'literal') { - step.type = 'name'; - } - inputSequence.forEach(function (item) { - var res = evaluate(step, item, environment); - if (typeof res !== 'undefined') { - if (Array.isArray(res) && (step.value !== '[' )) { - // is res an array - if so, flatten it into the parent array - res.forEach(function (innerRes) { - if (typeof innerRes !== 'undefined') { - resultSequence.push(innerRes); - } - }); - } else { - resultSequence.push(res); - } - } - }); - if (resultSequence.length == 1) { - if(keepSingletonArray) { - result = resultSequence; - } else { - result = resultSequence[0]; - } - } else if (resultSequence.length > 1) { - result = resultSequence; - } - - if(typeof result === 'undefined') { - break; - } - input = result; - } - return result; - } - - /** - * Apply predicates to input data - * @param {Object} predicates - Predicates - * @param {Object} input - Input data to apply predicates against - * @param {Object} environment - Environment - * @returns {*} Result after applying predicates - */ - function applyPredicates(predicates, input, environment) { - var result; - var inputSequence = input; - // lhs potentially holds an array - // we want to iterate over the array, and only keep the items that are - // truthy when applied to the predicate. - // if the predicate evaluates to an integer, then select that index - - var results = []; - predicates.forEach(function (predicate) { - // if it's not an array, turn it into one - // since in XPath >= 2.0 an item is equivalent to a singleton sequence of that item - // if input is not an array, make it so - if (!Array.isArray(inputSequence)) { - inputSequence = [inputSequence]; - } - results = []; - result = undefined; - if (predicate.type === 'literal' && isNumeric(predicate.value)) { - var index = predicate.value; - if (!Number.isInteger(index)) { - // round it down - index = Math.floor(index); - } - if (index < 0) { - // count in from end of array - index = inputSequence.length + index; - } - result = inputSequence[index]; - } else { - inputSequence.forEach(function (item, index) { - var res = evaluate(predicate, item, environment); - if (isNumeric(res)) { - res = [res]; - } - if(isArrayOfNumbers(res)) { - res.forEach(function(ires) { - if (!Number.isInteger(ires)) { - // round it down - ires = Math.floor(ires); - } - if (ires < 0) { - // count in from end of array - ires = inputSequence.length + ires; - } - if (ires == index) { - results.push(item); - } - }); - } else if (functionBoolean(res)) { // truthy - results.push(item); - } - }); - } - if (results.length == 1) { - result = results[0]; - } else if (results.length > 1) { - result = results; - } - inputSequence = result; - }); - return result; - } - - /** - * Evaluate binary expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateBinary(expr, input, environment) { - var result; - - switch (expr.value) { - case '+': - case '-': - case '*': - case '/': - case '%': - result = evaluateNumericExpression(expr, input, environment); - break; - case '=': - case '!=': - case '<': - case '<=': - case '>': - case '>=': - result = evaluateComparisonExpression(expr, input, environment); - break; - case '&': - result = evaluateStringConcat(expr, input, environment); - break; - case 'and': - case 'or': - result = evaluateBooleanExpression(expr, input, environment); - break; - case '..': - result = evaluateRangeExpression(expr, input, environment); - break; - case ':=': - result = evaluateBindExpression(expr, input, environment); - break; - case 'in': - result = evaluateIncludesExpression(expr, input, environment); - break; - } - return result; - } - - /** - * Evaluate unary expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateUnary(expr, input, environment) { - var result; - - switch (expr.value) { - case '-': - result = evaluate(expr.expression, input, environment); - if (isNumeric(result)) { - result = -result; - } else { - throw { - message: "Cannot negate a non-numeric value: " + result, - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: result - }; - } - break; - case '[': - // array constructor - evaluate each item - result = []; - expr.lhs.forEach(function (item) { - var value = evaluate(item, input, environment); - if (typeof value !== 'undefined') { - if(item.value === '[') { - result.push(value); - } else { - result = functionAppend(result, value); - } - } - }); - break; - case '{': - // object constructor - apply grouping - result = evaluateGroupExpression(expr, input, environment); - break; - - } - return result; - } - - /** - * Evaluate name object against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateName(expr, input, environment) { - // lookup the 'name' item in the input - var result; - if (Array.isArray(input)) { - var results = []; - input.forEach(function (item) { - var res = evaluateName(expr, item, environment); - if (typeof res !== 'undefined') { - results.push(res); - } - }); - if (results.length == 1) { - result = results[0]; - } else if (results.length > 1) { - result = results; - } - } else if (input !== null && typeof input === 'object') { - result = input[expr.value]; - } - return result; - } - - /** - * Evaluate literal against input data - * @param {Object} expr - JSONata expression - * @returns {*} Evaluated input data - */ - function evaluateLiteral(expr) { - return expr.value; - } - - /** - * Evaluate wildcard against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @returns {*} Evaluated input data - */ - function evaluateWildcard(expr, input) { - var result; - var results = []; - if (input !== null && typeof input === 'object') { - Object.keys(input).forEach(function (key) { - var value = input[key]; - if(Array.isArray(value)) { - value = flatten(value); - results = functionAppend(results, value); - } else { - results.push(value); - } - }); - } - - if (results.length == 1) { - result = results[0]; - } else if (results.length > 1) { - result = results; - } - return result; - } - - /** - * Returns a flattened array - * @param {Array} arg - the array to be flatten - * @param {Array} flattened - carries the flattened array - if not defined, will initialize to [] - * @returns {Array} - the flattened array - */ - function flatten(arg, flattened) { - if(typeof flattened === 'undefined') { - flattened = []; - } - if(Array.isArray(arg)) { - arg.forEach(function (item) { - flatten(item, flattened); - }); - } else { - flattened.push(arg); - } - return flattened; - } - - /** - * Evaluate descendants against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @returns {*} Evaluated input data - */ - function evaluateDescendants(expr, input) { - var result; - var resultSequence = []; - if (typeof input !== 'undefined') { - // traverse all descendants of this object/array - recurseDescendants(input, resultSequence); - if (resultSequence.length == 1) { - result = resultSequence[0]; - } else { - result = resultSequence; - } - } - return result; - } - - /** - * Recurse through descendants - * @param {Object} input - Input data - * @param {Object} results - Results - */ - function recurseDescendants(input, results) { - // this is the equivalent of //* in XPath - if (!Array.isArray(input)) { - results.push(input); - } - if (Array.isArray(input)) { - input.forEach(function (member) { - recurseDescendants(member, results); - }); - } else if (input !== null && typeof input === 'object') { - Object.keys(input).forEach(function (key) { - recurseDescendants(input[key], results); - }); - } - } - - /** - * Evaluate numeric expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateNumericExpression(expr, input, environment) { - var result; - - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { - // if either side is undefined, the result is undefined - return result; - } - - if (!isNumeric(lhs)) { - throw { - message: 'LHS of ' + expr.value + ' operator must evaluate to a number', - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: lhs - }; - } - if (!isNumeric(rhs)) { - throw { - message: 'RHS of ' + expr.value + ' operator must evaluate to a number', - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: rhs - }; - } - - switch (expr.value) { - case '+': - result = lhs + rhs; - break; - case '-': - result = lhs - rhs; - break; - case '*': - result = lhs * rhs; - break; - case '/': - result = lhs / rhs; - break; - case '%': - result = lhs % rhs; - break; - } - return result; - } - - /** - * Evaluate comparison expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateComparisonExpression(expr, input, environment) { - var result; - - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { - // if either side is undefined, the result is false - return false; - } - - switch (expr.value) { - case '=': - result = lhs == rhs; - break; - case '!=': - result = (lhs != rhs); - break; - case '<': - result = lhs < rhs; - break; - case '<=': - result = lhs <= rhs; - break; - case '>': - result = lhs > rhs; - break; - case '>=': - result = lhs >= rhs; - break; - } - return result; - } - - /** - * Inclusion operator - in - * - * @param {Object} expr - AST - * @param {*} input - input context - * @param {Object} environment - frame - * @returns {boolean} - true if lhs is a member of rhs - */ - function evaluateIncludesExpression(expr, input, environment) { - var result = false; - - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { - // if either side is undefined, the result is false - return false; - } - - if(!Array.isArray(rhs)) { - rhs = [rhs]; - } - - for(var i = 0; i < rhs.length; i++) { - if(rhs[i] === lhs) { - result = true; - break; - } - } - - return result; - } - - /** - * Evaluate boolean expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateBooleanExpression(expr, input, environment) { - var result; - - switch (expr.value) { - case 'and': - result = functionBoolean(evaluate(expr.lhs, input, environment)) && - functionBoolean(evaluate(expr.rhs, input, environment)); - break; - case 'or': - result = functionBoolean(evaluate(expr.lhs, input, environment)) || - functionBoolean(evaluate(expr.rhs, input, environment)); - break; - } - return result; - } - - /** - * Evaluate string concatenation against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {string|*} Evaluated input data - */ - function evaluateStringConcat(expr, input, environment) { - var result; - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - var lstr = ''; - var rstr = ''; - if (typeof lhs !== 'undefined') { - lstr = functionString(lhs); - } - if (typeof rhs !== 'undefined') { - rstr = functionString(rhs); - } - - result = lstr.concat(rstr); - return result; - } - - /** - * Evaluate group expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {{}} Evaluated input data - */ - function evaluateGroupExpression(expr, input, environment) { - var result = {}; - var groups = {}; - // group the input sequence by 'key' expression - if (!Array.isArray(input)) { - input = [input]; - } - input.forEach(function (item) { - expr.lhs.forEach(function (pair) { - var key = evaluate(pair[0], item, environment); - // key has to be a string - if (typeof key !== 'string') { - throw { - message: 'Key in object structure must evaluate to a string. Got: ' + key, - stack: (new Error()).stack, - position: expr.position, - value: key - }; - } - var entry = {data: item, expr: pair[1]}; - if (groups.hasOwnProperty(key)) { - // a value already exists in this slot - // append it as an array - groups[key].data = functionAppend(groups[key].data, item); - } else { - groups[key] = entry; - } - }); - }); - // iterate over the groups to evaluate the 'value' expression - for (var key in groups) { - var entry = groups[key]; - var value = evaluate(entry.expr, entry.data, environment); - result[key] = value; - } - return result; - } - - /** - * Evaluate range expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateRangeExpression(expr, input, environment) { - var result; - - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { - // if either side is undefined, the result is undefined - return result; - } - - if (lhs > rhs) { - // if the lhs is greater than the rhs, return undefined - return result; - } - - if (!Number.isInteger(lhs)) { - throw { - message: 'LHS of range operator (..) must evaluate to an integer', - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: lhs - }; - } - if (!Number.isInteger(rhs)) { - throw { - message: 'RHS of range operator (..) must evaluate to an integer', - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: rhs - }; - } - - result = new Array(rhs - lhs + 1); - for (var item = lhs, index = 0; item <= rhs; item++, index++) { - result[index] = item; - } - return result; - } - - /** - * Evaluate bind expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateBindExpression(expr, input, environment) { - // The RHS is the expression to evaluate - // The LHS is the name of the variable to bind to - should be a VARIABLE token - var value = evaluate(expr.rhs, input, environment); - if (expr.lhs.type !== 'variable') { - throw { - message: "Left hand side of := must be a variable name (start with $)", - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: expr.lhs.type === 'path' ? expr.lhs[0].value : expr.lhs.value - }; - } - environment.bind(expr.lhs.value, value); - return value; - } - - /** - * Evaluate condition against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateCondition(expr, input, environment) { - var result; - var condition = evaluate(expr.condition, input, environment); - if (functionBoolean(condition)) { - result = evaluate(expr.then, input, environment); - } else if (typeof expr.else !== 'undefined') { - result = evaluate(expr.else, input, environment); - } - return result; - } - - /** - * Evaluate block against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateBlock(expr, input, environment) { - var result; - // create a new frame to limit the scope of variable assignments - // TODO, only do this if the post-parse stage has flagged this as required - var frame = createFrame(environment); - // invoke each expression in turn - // only return the result of the last one - expr.expressions.forEach(function (expression) { - result = evaluate(expression, input, frame); - }); - - return result; - } - - /** - * Evaluate variable against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateVariable(expr, input, environment) { - // lookup the variable value in the environment - var result; - // if the variable name is empty string, then it refers to context value - if (expr.value === '') { - result = input; - } else { - result = environment.lookup(expr.value); - } - return result; - } - - /** - * Evaluate function against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateFunction(expr, input, environment) { - var result; - // evaluate the arguments - var evaluatedArgs = []; - expr.arguments.forEach(function (arg) { - evaluatedArgs.push(evaluate(arg, input, environment)); - }); - // lambda function on lhs - // create the procedure - // can't assume that expr.procedure is a lambda type directly - // could be an expression that evaluates to a function (e.g. variable reference, parens expr etc. - // evaluate it generically first, then check that it is a function. Throw error if not. - var proc = evaluate(expr.procedure, input, environment); - - if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure[0].value)) { - // help the user out here if they simply forgot the leading $ - throw { - message: 'Attempted to invoke a non-function. Did you mean \'$' + expr.procedure[0].value + '\'?', - stack: (new Error()).stack, - position: expr.position, - token: expr.procedure[0].value - }; - } - // apply the procedure - try { - result = apply(proc, evaluatedArgs, environment, input); - while(typeof result === 'object' && result.lambda == true && result.thunk == true) { - // trampoline loop - this gets invoked as a result of tail-call optimization - // the function returned a tail-call thunk - // unpack it, evaluate its arguments, and apply the tail call - var next = evaluate(result.body.procedure, result.input, result.environment); - evaluatedArgs = []; - result.body.arguments.forEach(function (arg) { - evaluatedArgs.push(evaluate(arg, result.input, result.environment)); - }); - - result = apply(next, evaluatedArgs); - } - } catch (err) { - // add the position field to the error - err.position = expr.position; - // and the function identifier - err.token = expr.procedure.type === 'path' ? expr.procedure[0].value : expr.procedure.value; - throw err; - } - return result; - } - - /** - * Apply procedure or function - * @param {Object} proc - Procedure - * @param {Array} args - Arguments - * @param {Object} environment - Environment - * @param {Object} self - Self - * @returns {*} Result of procedure - */ - function apply(proc, args, environment, self) { - var result; - if (proc && proc.lambda) { - result = applyProcedure(proc, args); - } else if (typeof proc === 'function') { - result = proc.apply(self, args); - } else { - throw { - message: 'Attempted to invoke a non-function', - stack: (new Error()).stack - }; - } - return result; - } - - /** - * Evaluate lambda against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {{lambda: boolean, input: *, environment: *, arguments: *, body: *}} Evaluated input data - */ - function evaluateLambda(expr, input, environment) { - // make a function (closure) - var procedure = { - lambda: true, - input: input, - environment: environment, - arguments: expr.arguments, - body: expr.body - }; - if(expr.thunk == true) { - procedure.thunk = true; - } - return procedure; - } - - /** - * Evaluate partial application - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluatePartialApplication(expr, input, environment) { - // partially apply a function - var result; - // evaluate the arguments - var evaluatedArgs = []; - expr.arguments.forEach(function (arg) { - if (arg.type === 'operator' && arg.value === '?') { - evaluatedArgs.push(arg); - } else { - evaluatedArgs.push(evaluate(arg, input, environment)); - } - }); - // lookup the procedure - var proc = evaluate(expr.procedure, input, environment); - if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure[0].value)) { - // help the user out here if they simply forgot the leading $ - throw { - message: 'Attempted to partially apply a non-function. Did you mean \'$' + expr.procedure[0].value + '\'?', - stack: (new Error()).stack, - position: expr.position, - token: expr.procedure[0].value - }; - } - if (proc && proc.lambda) { - result = partialApplyProcedure(proc, evaluatedArgs); - } else if (typeof proc === 'function') { - result = partialApplyNativeFunction(proc, evaluatedArgs); - } else { - throw { - message: 'Attempted to partially apply a non-function', - stack: (new Error()).stack, - position: expr.position, - token: expr.procedure.type === 'path' ? expr.procedure[0].value : expr.procedure.value - }; - } - return result; - } - - /** - * Apply procedure - * @param {Object} proc - Procedure - * @param {Array} args - Arguments - * @returns {*} Result of procedure - */ - function applyProcedure(proc, args) { - var result; - var env = createFrame(proc.environment); - proc.arguments.forEach(function (param, index) { - env.bind(param.value, args[index]); - }); - if (typeof proc.body === 'function') { - // this is a lambda that wraps a native function - generated by partially evaluating a native - result = applyNativeFunction(proc.body, env); - } else { - result = evaluate(proc.body, proc.input, env); - } - return result; - } - - /** - * Partially apply procedure - * @param {Object} proc - Procedure - * @param {Array} args - Arguments - * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applied procedure - */ - function partialApplyProcedure(proc, args) { - // create a closure, bind the supplied parameters and return a function that takes the remaining (?) parameters - var env = createFrame(proc.environment); - var unboundArgs = []; - proc.arguments.forEach(function (param, index) { - var arg = args[index]; - if (arg && arg.type === 'operator' && arg.value === '?') { - unboundArgs.push(param); - } else { - env.bind(param.value, arg); - } - }); - var procedure = { - lambda: true, - input: proc.input, - environment: env, - arguments: unboundArgs, - body: proc.body - }; - return procedure; - } - - /** - * Partially apply native function - * @param {Function} native - Native function - * @param {Array} args - Arguments - * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applying native function - */ - function partialApplyNativeFunction(native, args) { - // create a lambda function that wraps and invokes the native function - // get the list of declared arguments from the native function - // this has to be picked out from the toString() value - var sigArgs = getNativeFunctionArguments(native); - sigArgs = sigArgs.map(function (sigArg) { - return '$' + sigArg.trim(); - }); - var body = 'function(' + sigArgs.join(', ') + '){ _ }'; - - var bodyAST = parser(body); - bodyAST.body = native; - - var partial = partialApplyProcedure(bodyAST, args); - return partial; - } - - /** - * Apply native function - * @param {Object} proc - Procedure - * @param {Object} env - Environment - * @returns {*} Result of applying native function - */ - function applyNativeFunction(proc, env) { - var sigArgs = getNativeFunctionArguments(proc); - // generate the array of arguments for invoking the function - look them up in the environment - var args = sigArgs.map(function (sigArg) { - return env.lookup(sigArg.trim()); - }); - - var result = proc.apply(null, args); - return result; - } - - /** - * Get native function arguments - * @param {Function} func - Function - * @returns {*|Array} Native function arguments - */ - function getNativeFunctionArguments(func) { - var signature = func.toString(); - var sigParens = /\(([^\)]*)\)/.exec(signature)[1]; // the contents of the parens - var sigArgs = sigParens.split(','); - return sigArgs; - } - - /** - * Tests whether arg is a lambda function - * @param {*} arg - the value to test - * @returns {boolean} - true if it is a lambda function - */ - function isLambda(arg) { - var result = false; - if(arg && typeof arg === 'object' && - arg.lambda === true && - arg.hasOwnProperty('input') && - arg.hasOwnProperty('arguments') && - arg.hasOwnProperty('environment') && - arg.hasOwnProperty('body')) { - result = true; - } - - return result; - } - - /** - * Sum function - * @param {Object} args - Arguments - * @returns {number} Total value of arguments - */ - function functionSum(args) { - var total = 0; - - if (arguments.length != 1) { - throw { - message: 'The sum function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return undefined; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - // it must be an array of numbers - var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); - if(nonNumerics.length > 0) { - throw { - message: 'Type error: argument of sum function must be an array of numbers', - stack: (new Error()).stack, - value: nonNumerics - }; - } - args.forEach(function(num){total += num;}); - return total; - } - - /** - * Count function - * @param {Object} args - Arguments - * @returns {number} Number of elements in the array - */ - function functionCount(args) { - if (arguments.length != 1) { - throw { - message: 'The count function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return 0; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - return args.length; - } - - /** - * Max function - * @param {Object} args - Arguments - * @returns {number} Max element in the array - */ - function functionMax(args) { - var max; - - if (arguments.length != 1) { - throw { - message: 'The max function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return undefined; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - // it must be an array of numbers - var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); - if(nonNumerics.length > 0) { - throw { - message: 'Type error: argument of max function must be an array of numbers', - stack: (new Error()).stack, - value: nonNumerics - }; - } - max = Math.max.apply(Math, args); - return max; - } - - /** - * Min function - * @param {Object} args - Arguments - * @returns {number} Min element in the array - */ - function functionMin(args) { - var min; - - if (arguments.length != 1) { - throw { - message: 'The min function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return undefined; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - // it must be an array of numbers - var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); - if(nonNumerics.length > 0) { - throw { - message: 'Type error: argument of min function must be an array of numbers', - stack: (new Error()).stack, - value: nonNumerics - }; - } - min = Math.min.apply(Math, args); - return min; - } - - /** - * Average function - * @param {Object} args - Arguments - * @returns {number} Average element in the array - */ - function functionAverage(args) { - var total = 0; - - if (arguments.length != 1) { - throw { - message: 'The average function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return undefined; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - // it must be an array of numbers - var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); - if(nonNumerics.length > 0) { - throw { - message: 'Type error: argument of average function must be an array of numbers', - stack: (new Error()).stack, - value: nonNumerics - }; - } - args.forEach(function(num){total += num;}); - return total/args.length; - } - - /** - * Stingify arguments - * @param {Object} arg - Arguments - * @returns {String} String from arguments - */ - function functionString(arg) { - var str; - - if(arguments.length != 1) { - throw { - message: 'The string function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof arg === 'undefined') { - return undefined; - } - - if (typeof arg === 'string') { - // already a string - str = arg; - } else if(typeof arg === 'function' || isLambda(arg)) { - // functions (built-in and lambda convert to empty string - str = ''; - } else if (typeof arg === 'number' && !isFinite(arg)) { - throw { - message: "Attempting to invoke string function on Infinity or NaN", - value: arg, - stack: (new Error()).stack - }; - } else - str = JSON.stringify(arg, function (key, val) { - return (typeof val !== 'undefined' && val !== null && val.toPrecision && isNumeric(val)) ? Number(val.toPrecision(13)) : - (val && isLambda(val)) ? '' : - (typeof val === 'function') ? '' : val; - }); - return str; - } - - /** - * Create substring based on character number and length - * @param {String} str - String to evaluate - * @param {Integer} start - Character number to start substring - * @param {Integer} [length] - Number of characters in substring - * @returns {string|*} Substring - */ - function functionSubstring(str, start, length) { - if(arguments.length != 2 && arguments.length != 3) { - throw { - message: 'The substring function expects two or three arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: first argument of substring function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - if(typeof start !== 'number') { - throw { - message: 'Type error: second argument of substring function must evaluate to a number', - stack: (new Error()).stack, - value: start - }; - } - - if(typeof length !== 'undefined' && typeof length !== 'number') { - throw { - message: 'Type error: third argument of substring function must evaluate to a number', - stack: (new Error()).stack, - value: length - }; - } - - return str.substr(start, length); - } - - /** - * Create substring up until a character - * @param {String} str - String to evaluate - * @param {String} chars - Character to define substring boundary - * @returns {*} Substring - */ - function functionSubstringBefore(str, chars) { - if(arguments.length != 2) { - throw { - message: 'The substringBefore function expects two arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: first argument of substringBefore function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - if(typeof chars !== 'string') { - throw { - message: 'Type error: second argument of substringBefore function must evaluate to a string', - stack: (new Error()).stack, - value: chars - }; - } - - var pos = str.indexOf(chars); - if (pos > -1) { - return str.substr(0, pos); - } else { - return str; - } - } - - /** - * Create substring after a character - * @param {String} str - String to evaluate - * @param {String} chars - Character to define substring boundary - * @returns {*} Substring - */ - function functionSubstringAfter(str, chars) { - if(arguments.length != 2) { - throw { - message: 'The substringAfter function expects two arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: first argument of substringAfter function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - if(typeof chars !== 'string') { - throw { - message: 'Type error: second argument of substringAfter function must evaluate to a string', - stack: (new Error()).stack, - value: chars - }; - } - - var pos = str.indexOf(chars); - if (pos > -1) { - return str.substr(pos + chars.length); - } else { - return str; - } - } - - /** - * Lowercase a string - * @param {String} str - String to evaluate - * @returns {string} Lowercase string - */ - function functionLowercase(str) { - if(arguments.length != 1) { - throw { - message: 'The lowercase function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: argument of lowercase function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - return str.toLowerCase(); - } - - /** - * Uppercase a string - * @param {String} str - String to evaluate - * @returns {string} Uppercase string - */ - function functionUppercase(str) { - if(arguments.length != 1) { - throw { - message: 'The uppercase function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: argument of uppercase function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - return str.toUpperCase(); - } - - /** - * length of a string - * @param {String} str - string - * @returns {Number} The number of characters in the string - */ - function functionLength(str) { - if(arguments.length != 1) { - throw { - message: 'The length function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: argument of length function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - return str.length; - } - - /** - * Split a string into an array of substrings - * @param {String} str - string - * @param {String} separator - the token that splits the string - * @param {Integer} [limit] - max number of substrings - * @returns {Array} The array of string - */ - function functionSplit(str, separator, limit) { - if(arguments.length != 2 && arguments.length != 3) { - throw { - message: 'The split function expects two or three arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: first argument of split function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - // separator must be a string - if(typeof separator !== 'string') { - throw { - message: 'Type error: second argument of split function must evaluate to a string', - stack: (new Error()).stack, - value: separator - }; - } - - // limit, if specified, must be a number - if(typeof limit !== 'undefined' && (typeof limit !== 'number' || limit < 0)) { - throw { - message: 'Type error: third argument of split function must evaluate to a positive number', - stack: (new Error()).stack, - value: limit - }; - } - - return str.split(separator, limit); - } - - /** - * Join an array of strings - * @param {Array} strs - array of string - * @param {String} [separator] - the token that splits the string - * @returns {String} The concatenated string - */ - function functionJoin(strs, separator) { - if(arguments.length != 1 && arguments.length != 2) { - throw { - message: 'The join function expects one or two arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof strs === 'undefined') { - return undefined; - } - - if(!Array.isArray(strs)) { - strs = [strs]; - } - - // it must be an array of strings - var nonStrings = strs.filter(function(val) {return (typeof val !== 'string');}); - if(nonStrings.length > 0) { - throw { - message: 'Type error: first argument of join function must be an array of strings', - stack: (new Error()).stack, - value: nonStrings - }; - } - - - // if separator is not specified, default to empty string - if(typeof separator === 'undefined') { - separator = ""; - } - - // separator, if specified, must be a string - if(typeof separator !== 'string') { - throw { - message: 'Type error: second argument of split function must evaluate to a string', - stack: (new Error()).stack, - value: separator - }; - } - - return strs.join(separator); - } - - /** - * Cast argument to number - * @param {Object} arg - Argument - * @returns {Number} numeric value of argument - */ - function functionNumber(arg) { - var result; - - if(arguments.length != 1) { - throw { - message: 'The number function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof arg === 'undefined') { - return undefined; - } - - if (typeof arg === 'number') { - // already a number - result = arg; - } else if(typeof arg === 'string' && /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?$/.test(arg) && !isNaN(parseFloat(arg)) && isFinite(arg)) { - result = parseFloat(arg); - } else { - throw { - message: "Unable to cast value to a number", - value: arg, - stack: (new Error()).stack - }; - } - return result; - } - - - /** - * Evaluate an input and return a boolean - * @param {*} arg - Arguments - * @returns {boolean} Boolean - */ - function functionBoolean(arg) { - // cast arg to its effective boolean value - // boolean: unchanged - // string: zero-length -> false; otherwise -> true - // number: 0 -> false; otherwise -> true - // null -> false - // array: empty -> false; length > 1 -> true - // object: empty -> false; non-empty -> true - // function -> false - - if(arguments.length != 1) { - throw { - message: 'The boolean function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof arg === 'undefined') { - return undefined; - } - - var result = false; - if (Array.isArray(arg)) { - if (arg.length == 1) { - result = functionBoolean(arg[0]); - } else if (arg.length > 1) { - var trues = arg.filter(function(val) {return functionBoolean(val);}); - result = trues.length > 0; - } - } else if (typeof arg === 'string') { - if (arg.length > 0) { - result = true; - } - } else if (isNumeric(arg)) { - if (arg != 0) { - result = true; - } - } else if (arg != null && typeof arg === 'object') { - if (Object.keys(arg).length > 0) { - // make sure it's not a lambda function - if (!(isLambda(arg))) { - result = true; - } - } - } else if (typeof arg === 'boolean' && arg == true) { - result = true; - } - return result; - } - - /** - * returns the Boolean NOT of the arg - * @param {*} arg - argument - * @returns {boolean} - NOT arg - */ - function functionNot(arg) { - return !functionBoolean(arg); - } - - /** - * Create a map from an array of arguments - * @param {Function} func - function to apply - * @returns {Array} Map array - */ - function functionMap(func) { - // this can take a variable number of arguments - each one should be mapped to the equivalent arg of func - // assert that func is a function - var varargs = arguments; - var result = []; - - // each subsequent arg must be an array - coerce if not - var args = []; - for (var ii = 1; ii < varargs.length; ii++) { - if (Array.isArray(varargs[ii])) { - args.push(varargs[ii]); - } else { - args.push([varargs[ii]]); - } - - } - // do the map - iterate over the arrays, and invoke func - if (args.length > 0) { - for (var i = 0; i < args[0].length; i++) { - var func_args = []; - for (var j = 0; j < func.arguments.length; j++) { - func_args.push(args[j][i]); - } - // invoke func - result.push(apply(func, func_args, null, null)); - } - } - return result; - } - - /** - * Fold left function - * @param {Function} func - Function - * @param {Array} sequence - Sequence - * @param {Object} init - Initial value - * @returns {*} Result - */ - function functionFoldLeft(func, sequence, init) { - var result; - - if (!(func.length == 2 || func.arguments.length == 2)) { - throw { - message: 'The first argument of the reduce function must be a function of arity 2', - stack: (new Error()).stack - }; - } - - if (!Array.isArray(sequence)) { - sequence = [sequence]; - } - - var index; - if (typeof init === 'undefined' && sequence.length > 0) { - result = sequence[0]; - index = 1; - } else { - result = init; - index = 0; - } - - while (index < sequence.length) { - result = apply(func, [result, sequence[index]], null, null); - index++; - } - - return result; - } - - /** - * Return keys for an object - * @param {Object} arg - Object - * @returns {Array} Array of keys - */ - function functionKeys(arg) { - var result = []; - - if(Array.isArray(arg)) { - // merge the keys of all of the items in the array - var merge = {}; - arg.forEach(function(item) { - var keys = functionKeys(item); - if(Array.isArray(keys)) { - keys.forEach(function(key) { - merge[key] = true; - }); - } - }); - result = functionKeys(merge); - } else if(arg != null && typeof arg === 'object' && !(isLambda(arg))) { - result = Object.keys(arg); - if(result.length == 0) { - result = undefined; - } - } else { - result = undefined; - } - return result; - } - - /** - * Return value from an object for a given key - * @param {Object} object - Object - * @param {String} key - Key in object - * @returns {*} Value of key in object - */ - function functionLookup(object, key) { - var result = evaluateName({value: key}, object); - return result; - } - - /** - * Append second argument to first - * @param {Array|Object} arg1 - First argument - * @param {Array|Object} arg2 - Second argument - * @returns {*} Appended arguments - */ - function functionAppend(arg1, arg2) { - // disregard undefined args - if (typeof arg1 === 'undefined') { - return arg2; - } - if (typeof arg2 === 'undefined') { - return arg1; - } - // if either argument is not an array, make it so - if (!Array.isArray(arg1)) { - arg1 = [arg1]; - } - if (!Array.isArray(arg2)) { - arg2 = [arg2]; - } - Array.prototype.push.apply(arg1, arg2); - return arg1; - } - - /** - * Determines if the argument is undefined - * @param {*} arg - argument - * @returns {boolean} False if argument undefined, otherwise true - */ - function functionExists(arg){ - if (arguments.length != 1) { - throw { - message: 'The exists function expects one argument', - stack: (new Error()).stack - }; - } - - if (typeof arg === 'undefined') { - return false; - } else { - return true; - } - } - - /** - * Splits an object into an array of object with one property each - * @param {*} arg - the object to split - * @returns {*} - the array - */ - function functionSpread(arg) { - var result = []; - - if(Array.isArray(arg)) { - // spread all of the items in the array - arg.forEach(function(item) { - result = functionAppend(result, functionSpread(item)); - }); - } else if(arg != null && typeof arg === 'object' && !isLambda(arg)) { - for(var key in arg) { - var obj = {}; - obj[key] = arg[key]; - result.push(obj); - } - } else { - result = arg; - } - return result; - } - - /** - * Create frame - * @param {Object} enclosingEnvironment - Enclosing environment - * @returns {{bind: bind, lookup: lookup}} Created frame - */ - function createFrame(enclosingEnvironment) { - var bindings = {}; - return { - bind: function (name, value) { - bindings[name] = value; - }, - lookup: function (name) { - var value = bindings[name]; - if (typeof value === 'undefined' && enclosingEnvironment) { - value = enclosingEnvironment.lookup(name); - } - return value; - } - }; - } - - var staticFrame = createFrame(null); - - staticFrame.bind('sum', functionSum); - staticFrame.bind('count', functionCount); - staticFrame.bind('max', functionMax); - staticFrame.bind('min', functionMin); - staticFrame.bind('average', functionAverage); - staticFrame.bind('string', functionString); - staticFrame.bind('substring', functionSubstring); - staticFrame.bind('substringBefore', functionSubstringBefore); - staticFrame.bind('substringAfter', functionSubstringAfter); - staticFrame.bind('lowercase', functionLowercase); - staticFrame.bind('uppercase', functionUppercase); - staticFrame.bind('length', functionLength); - staticFrame.bind('split', functionSplit); - staticFrame.bind('join', functionJoin); - staticFrame.bind('number', functionNumber); - staticFrame.bind('boolean', functionBoolean); - staticFrame.bind('not', functionNot); - staticFrame.bind('map', functionMap); - staticFrame.bind('reduce', functionFoldLeft); - staticFrame.bind('keys', functionKeys); - staticFrame.bind('lookup', functionLookup); - staticFrame.bind('append', functionAppend); - staticFrame.bind('exists', functionExists); - staticFrame.bind('spread', functionSpread); - - /** - * JSONata - * @param {Object} expr - JSONata expression - * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression - */ - function jsonata(expr) { - var ast = parser(expr); - var environment = createFrame(staticFrame); - return { - evaluate: function (input, bindings) { - if (typeof bindings !== 'undefined') { - var exec_env; - // the variable bindings have been passed in - create a frame to hold these - exec_env = createFrame(environment); - for (var v in bindings) { - exec_env.bind(v, bindings[v]); - } - } else { - exec_env = environment; - } - // put the input document into the environment as the root object - exec_env.bind('$', input); - return evaluate(ast, input, exec_env); - }, - assign: function (name, value) { - environment.bind(name, value); - } - }; - } - - jsonata.parser = parser; - - return jsonata; - -})(); - -// node.js only - export the jsonata and parser functions -// istanbul ignore else -if(typeof module !== 'undefined') { - module.exports = jsonata; -} diff --git a/editor/vendor/jsonata/mode-jsonata.js b/editor/vendor/jsonata/mode-jsonata.js index 50190e7b3..fb5883db7 100644 --- a/editor/vendor/jsonata/mode-jsonata.js +++ b/editor/vendor/jsonata/mode-jsonata.js @@ -6,6 +6,13 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; var WorkerClient = require("../worker/worker_client").WorkerClient; + var jsonataFunctions = Object.keys(jsonata.functions); + // sort in length order (long->short) otherwise substringAfter gets matched + // as substring etc. + jsonataFunctions.sort(function(A,B) { + return B.length-A.length; + }); + jsonataFunctions = jsonataFunctions.join("|").replace(/\$/g,"\\$"); var JSONataHighlightRules = function() { @@ -17,11 +24,7 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ "constant.language.boolean": "true|false", "storage.type": - "function", - "keyword": - "$sum|$count|$max|$min|$average|$string|$substring|$substringBefore|"+ - "$substringAfter|$lowercase|$uppercase|$length|$split|$join|$number|"+ - "$boolean|$not|$map|$reduce|$keys|$lookup|$append|$exists|$spread" + "function" }, "identifier"); this.$rules = { "start" : [ @@ -46,6 +49,10 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ { token: "keyword", regex: /λ/ }, + { + token: "keyword", + regex: jsonataFunctions + }, { token : keywordMapper, regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*" From 659c326f8977bc0edb6270dd762f488dd031389d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 23 Nov 2016 23:16:17 +0000 Subject: [PATCH 14/44] Add jsonata snippets --- editor/vendor/jsonata/snippets-jsonata.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 editor/vendor/jsonata/snippets-jsonata.js diff --git a/editor/vendor/jsonata/snippets-jsonata.js b/editor/vendor/jsonata/snippets-jsonata.js new file mode 100644 index 000000000..3fa8b7e82 --- /dev/null +++ b/editor/vendor/jsonata/snippets-jsonata.js @@ -0,0 +1,11 @@ +define("ace/snippets/jsonata",["require","exports","module"], function(require, exports, module) { +"use strict"; +var snippetText = ""; +for (var fn in jsonata.functions) { + if (jsonata.functions.hasOwnProperty(fn)) { + snippetText += "# "+fn+"\nsnippet "+fn+"\n\t"+jsonata.getFunctionSnippet(fn)+"\n" + } +} +exports.snippetText = snippetText; +exports.scope = "jsonata"; +}); From 52fc4974120a538cecfde277109a2de5c68f97cc Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 27 Nov 2016 21:51:34 +0000 Subject: [PATCH 15/44] Properly escape html strings passed to debug --- editor/js/ui/utils.js | 40 ++++++++++++++---------- editor/sass/debug.scss | 3 +- nodes/core/core/lib/debug/debug-utils.js | 2 +- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/editor/js/ui/utils.js b/editor/js/ui/utils.js index 24227dd04..58af98b75 100644 --- a/editor/js/ui/utils.js +++ b/editor/js/ui/utils.js @@ -19,6 +19,9 @@ RED.utils = (function() { function formatString(str) { return str.replace(/\r?\n/g,"↵").replace(/\t/g,"→"); } + function sanitize(m) { + return m.replace(/&/g,"&").replace(//g,">"); + } function buildMessageSummaryValue(value) { var result; @@ -35,9 +38,11 @@ RED.utils = (function() { result = $('object'); } } else if (typeof value === 'string') { - subvalue = value; - if (subvalue.length > 30) { - subvalue = subvalue.substring(0,30)+"…"; + var subvalue; + if (value.length > 30) { + subvalue = sanitize(value.substring(0,30))+"…"; + } else { + subvalue = sanitize(value); } result = $('').html('"'+formatString(subvalue)+'"'); } else { @@ -68,7 +73,7 @@ RED.utils = (function() { var entryObj; var header; var headerHead; - var value,subvalue; + var value; var element = $(''); if (!key) { element.addClass("debug-message-top-level"); @@ -98,23 +103,26 @@ RED.utils = (function() { makeExpandable(header, function() { $('').html(typeHint||'string').appendTo(header); var row = $('').appendTo(element); - $('
').html(obj).appendTo(row);
+                    $('
').text(obj).appendTo(row);
                 });
             }
-            $('').html('"'+formatString(obj)+'"').appendTo(entryObj);
+            $('').html('"'+formatString(sanitize(obj))+'"').appendTo(entryObj);
 
 
         } else if (typeof obj === 'number') {
             e = $('').text(""+obj).appendTo(entryObj);
-            e.click(function(evt) {
-                var format = $(this).data('format');
-                if (format === 'hex') {
-                    $(this).text(""+obj).data('format','dec');
-                } else {
-                    $(this).text("0x"+(obj).toString(16)).data('format','hex');
-                }
-                evt.preventDefault();
-            });
+            if ((obj^0)===obj) {
+                e.addClass("debug-message-type-number-toggle");
+                e.click(function(evt) {
+                    var format = $(this).data('format');
+                    if (format === 'hex') {
+                        $(this).text(""+obj).data('format','dec');
+                    } else {
+                        $(this).text("0x"+(obj).toString(16)).data('format','hex');
+                    }
+                    evt.preventDefault();
+                });
+            }
         } else if (isArray) {
             element.addClass('collapsed');
 
@@ -155,7 +163,7 @@ RED.utils = (function() {
                         } catch(err) {
                             console.log(err);
                         }
-                        $('
').html(stringEncoding).appendTo(sr);
+                        $('
').text(stringEncoding).appendTo(sr);
                         var bufferOpts = $('').appendTo(headerHead);
                         $('').addClass('selected').html('raw').appendTo(bufferOpts).click(function(e) {
                             if ($(this).text() === 'raw') {
diff --git a/editor/sass/debug.scss b/editor/sass/debug.scss
index f56a8c7f3..7e3db9d10 100644
--- a/editor/sass/debug.scss
+++ b/editor/sass/debug.scss
@@ -150,7 +150,8 @@
 .debug-message-type-string { color: #b72828; }
 .debug-message-type-null { color: #666; font-style: italic;}
 .debug-message-type-meta { color: #666; font-style: italic;}
-.debug-message-type-number { color: #2033d6;cursor: pointer;}
+.debug-message-type-number { color: #2033d6; };
+.debug-message-type-number-toggle { cursor: pointer;}
 
 .debug-message-expandable {
     cursor: pointer;
diff --git a/nodes/core/core/lib/debug/debug-utils.js b/nodes/core/core/lib/debug/debug-utils.js
index 4767733a2..5322266a5 100644
--- a/nodes/core/core/lib/debug/debug-utils.js
+++ b/nodes/core/core/lib/debug/debug-utils.js
@@ -198,7 +198,7 @@ RED.debug = (function() {
         var name = sanitize(((o.name?o.name:o.id)||"").toString());
         var topic = sanitize((o.topic||"").toString());
         var property = sanitize(o.property?o.property:'');
-        var payload = sanitize((o.msg||"").toString());
+        var payload = o.msg;
         var format = sanitize((o.format||"").toString());
         msg.className = 'debug-message'+(o.level?(' debug-message-level-'+o.level):'') +
         ((sourceNode&&sourceNode.z)?((" debug-message-flow-"+sourceNode.z+((filter&&(activeWorkspace!==sourceNode.z))?" hide":""))):"");

From 671d7e2bebbbb890289432e2fbb1fe24f7f0d812 Mon Sep 17 00:00:00 2001
From: Dave Conway-Jones 
Date: Mon, 28 Nov 2016 17:28:49 +0000
Subject: [PATCH 16/44] debug - format if time if correct length/range

---
 editor/js/ui/utils.js | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/editor/js/ui/utils.js b/editor/js/ui/utils.js
index 58af98b75..8a232f644 100644
--- a/editor/js/ui/utils.js
+++ b/editor/js/ui/utils.js
@@ -111,14 +111,18 @@ RED.utils = (function() {
 
         } else if (typeof obj === 'number') {
             e = $('').text(""+obj).appendTo(entryObj);
-            if ((obj^0)===obj) {
+            if (Number.isInteger(obj) && (obj >= 0)) { // if it's a +ve integer
                 e.addClass("debug-message-type-number-toggle");
                 e.click(function(evt) {
-                    var format = $(this).data('format');
-                    if (format === 'hex') {
-                        $(this).text(""+obj).data('format','dec');
+                    var format = $(this).data('format') || "date";
+                    if (format === 'dec') {
+                        $(this).text(""+obj).data('format','date');
+                    } else if ((format === 'date') && (obj.toString().length===13) && (obj<=2147483647000)) {
+                        $(this).text((new Date(obj)).toISOString()).data('format','hex');
+                    } else if ((format === 'date') && (obj.toString().length===10) && (obj<=2147483647)) {
+                        $(this).text((new Date(obj*1000)).toISOString()).data('format','hex');
                     } else {
-                        $(this).text("0x"+(obj).toString(16)).data('format','hex');
+                        $(this).text("0x"+(obj).toString(16)).data('format','dec');
                     }
                     evt.preventDefault();
                 });

From f68acca42757a8f095e2712e40af495cdeaa5e7a Mon Sep 17 00:00:00 2001
From: Nick O'Leary 
Date: Thu, 1 Dec 2016 15:27:29 +0000
Subject: [PATCH 17/44] Fix dynamically loading multiple node-sets from palette
 editor

---
 editor/js/main.js | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/editor/js/main.js b/editor/js/main.js
index 22726bec0..d2e438fed 100644
--- a/editor/js/main.js
+++ b/editor/js/main.js
@@ -98,11 +98,9 @@
                     var i,m;
                     var typeList;
                     var info;
-
                     if (topic == "node/added") {
                         var addedTypes = [];
-                        for (i=0;i
  • ")+"
  • "; RED.notify(RED._("palette.event.nodeAdded", {count:addedTypes.length})+typeList,"success"); From 16ecb1a9cb0838ba3629d47c45681008f613dd7c Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 4 Dec 2016 22:59:43 +0000 Subject: [PATCH 18/44] Overhaul keyboard handling and introduce editor actions --- Gruntfile.js | 20 ++- editor/js/keymap.json | 38 +++++ editor/js/main.js | 46 +++--- editor/js/ui/actions.js | 35 +++++ editor/js/ui/clipboard.js | 10 +- editor/js/ui/common/menu.js | 45 +++--- editor/js/ui/common/tabs.js | 14 ++ editor/js/ui/editor.js | 15 +- editor/js/ui/keyboard.js | 260 ++++++++++++++++++++++++++------- editor/js/ui/library.js | 3 + editor/js/ui/palette-editor.js | 6 +- editor/js/ui/search.js | 10 +- editor/js/ui/sidebar.js | 10 +- editor/js/ui/subflow.js | 2 + editor/js/ui/tab-config.js | 5 +- editor/js/ui/tab-info.js | 2 +- editor/js/ui/typeSearch.js | 4 +- editor/js/ui/view.js | 115 +++++++++------ editor/js/ui/workspaces.js | 15 +- nodes/core/core/58-debug.html | 5 +- 20 files changed, 490 insertions(+), 170 deletions(-) create mode 100644 editor/js/keymap.json create mode 100644 editor/js/ui/actions.js diff --git a/Gruntfile.js b/Gruntfile.js index bf6994aca..f9531d1c2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -117,6 +117,7 @@ module.exports = function(grunt) { "editor/js/ui/common/tabs.js", "editor/js/ui/common/typedInput.js", "editor/js/ui/utils.js", + "editor/js/ui/actions.js", "editor/js/ui/deploy.js", "editor/js/ui/keyboard.js", "editor/js/ui/workspaces.js", @@ -194,6 +195,11 @@ module.exports = function(grunt) { 'red/api/locales/en-US/editor.json', 'red/runtime/locales/en-US/runtime.json' ] + }, + keymaps: { + src: [ + 'editor/js/keymap.json' + ] } }, attachCopyright: { @@ -230,7 +236,7 @@ module.exports = function(grunt) { files: [ 'editor/js/**/*.js' ], - tasks: ['concat','uglify','attachCopyright:js'] + tasks: ['copy:build','concat','uglify','attachCopyright:js'] }, sass: { files: [ @@ -246,6 +252,12 @@ module.exports = function(grunt) { ], tasks: ['jsonlint:messages'] }, + keymaps: { + files: [ + 'editor/js/keymap.json' + ], + tasks: ['jsonlint:keymaps','copy:build'] + }, misc: { files: [ 'CHANGELOG.md' @@ -284,6 +296,10 @@ module.exports = function(grunt) { src: 'editor/js/main.js', dest: 'public/red/main.js' }, + { + src: 'editor/js/keymap.json', + dest: 'public/red/keymap.json' + }, { cwd: 'editor/images', src: '**', @@ -443,7 +459,7 @@ module.exports = function(grunt) { grunt.registerTask('build', 'Builds editor content', - ['clean:build','concat:build','concat:vendor','copy:build','uglify:build','sass:build','jsonlint:messages','attachCopyright']); + ['clean:build','jsonlint','concat:build','concat:vendor','copy:build','uglify:build','sass:build','attachCopyright']); grunt.registerTask('dev', 'Developer mode: run node-red, watch for source changes and build/restart', diff --git a/editor/js/keymap.json b/editor/js/keymap.json new file mode 100644 index 000000000..2923e5e97 --- /dev/null +++ b/editor/js/keymap.json @@ -0,0 +1,38 @@ +{ + "*": { + "ctrl-shift-p":"core:manage-palette", + "ctrl-f": "core:search", + "ctrl-=": "core:zoom-in", + "ctrl--": "core:zoom-out", + "ctrl-0": "core:zoom-reset", + "ctrl-enter": "core:confirm-edit-tray", + "ctrl-escape": "core:cancel-edit-tray", + "ctrl-g i": "core:show-info-tab", + "ctrl-g d": "core:show-debug-tab", + "ctrl-g c": "core:show-config-tab" + }, + "workspace": { + "ctrl-e": "core:export", + "ctrl-i": "core:import", + "backspace": "core:delete", + "delete": "core:delete", + "enter": "core:edit", + "ctrl-c": "core:copy", + "ctrl-x": "core:cut", + "ctrl-v": "core:paste", + "ctrl-z": "core:undo", + "ctrl-a": "core:select-all", + "shift-?": "core:show-help", + "ctrl-space": "core:toggle-sidebar", + "up": "core:move-selection-up", + "right": "core:move-selection-right", + "down": "core:move-selection-down", + "left": "core:move-selection-left", + "shift-up": "core:step-selection-up", + "shift-right": "core:step-selection-right", + "shift-down": "core:step-selection-down", + "shift-left": "core:step-selection-left", + "ctrl-shift-j": "core:show-previous-tab", + "ctrl-shift-k": "core:show-next-tab" + } +} diff --git a/editor/js/main.js b/editor/js/main.js index d2e438fed..1038facda 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -15,7 +15,6 @@ **/ (function() { - function loadNodeList() { $.ajax({ headers: { @@ -170,12 +169,11 @@ } function loadEditor() { - var menuOptions = []; menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[ - {id:"menu-item-view-show-grid",label:RED._("menu.label.view.showGrid"),toggle:true,onselect:RED.view.toggleShowGrid}, - {id:"menu-item-view-snap-grid",label:RED._("menu.label.view.snapGrid"),toggle:true,onselect:RED.view.toggleSnapGrid}, - {id:"menu-item-status",label:RED._("menu.label.displayStatus"),toggle:true,onselect:toggleStatus, selected: true}, + {id:"menu-item-view-show-grid",label:RED._("menu.label.view.showGrid"),toggle:true,onselect:"core:toggle-show-grid"}, + {id:"menu-item-view-snap-grid",label:RED._("menu.label.view.snapGrid"),toggle:true,onselect:"core:toggle-snap-grid"}, + {id:"menu-item-status",label:RED._("menu.label.displayStatus"),toggle:true,onselect:"core:toggle-status", selected: true}, null, // {id:"menu-item-bidi",label:RED._("menu.label.view.textDir"),options:[ // {id:"menu-item-bidi-default",toggle:"text-direction",label:RED._("menu.label.view.defaultDir"),selected: true, onselect:function(s) { if(s){RED.text.bidi.setTextDirection("")}}}, @@ -184,48 +182,46 @@ // {id:"menu-item-bidi-auto",toggle:"text-direction",label:RED._("menu.label.view.auto"), onselect:function(s) { if(s){RED.text.bidi.setTextDirection("auto")}}} // ]}, // null, - {id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:RED.sidebar.toggleSidebar, selected: true} + {id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:"core:toggle-sidebar", selected: true} ]}); menuOptions.push(null); menuOptions.push({id:"menu-item-import",label:RED._("menu.label.import"),options:[ - {id:"menu-item-import-clipboard",label:RED._("menu.label.clipboard"),onselect:RED.clipboard.import}, + {id:"menu-item-import-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:import"}, {id:"menu-item-import-library",label:RED._("menu.label.library"),options:[]} ]}); menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),disabled:true,options:[ - {id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),disabled:true,onselect:RED.clipboard.export}, - {id:"menu-item-export-library",label:RED._("menu.label.library"),disabled:true,onselect:RED.library.export} + {id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),disabled:true,onselect:"core:export"}, + {id:"menu-item-export-library",label:RED._("menu.label.library"),disabled:true,onselect:"core:library-export"} ]}); menuOptions.push(null); - menuOptions.push({id:"menu-item-search",label:RED._("menu.label.search"),onselect:RED.search.show}); + menuOptions.push({id:"menu-item-search",label:RED._("menu.label.search"),onselect:"core:search"}); menuOptions.push(null); - menuOptions.push({id:"menu-item-config-nodes",label:RED._("menu.label.displayConfig"),onselect:function() {}}); + menuOptions.push({id:"menu-item-config-nodes",label:RED._("menu.label.displayConfig"),onselect:"core:show-config-tab"}); menuOptions.push({id:"menu-item-workspace",label:RED._("menu.label.flows"),options:[ - {id:"menu-item-workspace-add",label:RED._("menu.label.add"),onselect:RED.workspaces.add}, - {id:"menu-item-workspace-edit",label:RED._("menu.label.rename"),onselect:RED.workspaces.edit}, - {id:"menu-item-workspace-delete",label:RED._("menu.label.delete"),onselect:RED.workspaces.remove} + {id:"menu-item-workspace-add",label:RED._("menu.label.add"),onselect:"core:add-flow"}, + {id:"menu-item-workspace-edit",label:RED._("menu.label.rename"),onselect:"core:edit-flow"}, + {id:"menu-item-workspace-delete",label:RED._("menu.label.delete"),onselect:"core:remove-flow"} ]}); menuOptions.push({id:"menu-item-subflow",label:RED._("menu.label.subflows"), options: [ - {id:"menu-item-subflow-create",label:RED._("menu.label.createSubflow"),onselect:RED.subflow.createSubflow}, - {id:"menu-item-subflow-convert",label:RED._("menu.label.selectionToSubflow"),disabled:true,onselect:RED.subflow.convertToSubflow}, + {id:"menu-item-subflow-create",label:RED._("menu.label.createSubflow"),onselect:"core:create-subflow"}, + {id:"menu-item-subflow-convert",label:RED._("menu.label.selectionToSubflow"),disabled:true,onselect:"core:convert-to-subflow"}, ]}); menuOptions.push(null); if (RED.settings.theme('palette.editable') !== false) { RED.palette.editor.init(); - menuOptions.push({id:"menu-item-edit-palette",label:RED._("menu.label.editPalette"),onselect:RED.palette.editor.show}); + menuOptions.push({id:"menu-item-edit-palette",label:RED._("menu.label.editPalette"),onselect:"core:manage-palette"}); menuOptions.push(null); } - menuOptions.push({id:"menu-item-keyboard-shortcuts",label:RED._("menu.label.keyboardShortcuts"),onselect:RED.keyboard.showHelp}); + menuOptions.push({id:"menu-item-keyboard-shortcuts",label:RED._("menu.label.keyboardShortcuts"),onselect:"core:show-help"}); menuOptions.push({id:"menu-item-help", label: RED.settings.theme("menu.menu-item-help.label","Node-RED website"), href: RED.settings.theme("menu.menu-item-help.url","http://nodered.org/docs") }); - menuOptions.push({id:"menu-item-node-red-version", label:"v"+RED.settings.version, onselect: showAbout }); + menuOptions.push({id:"menu-item-node-red-version", label:"v"+RED.settings.version, onselect: "core:show-about" }); - RED.menu.init({id:"btn-sidemenu",options: menuOptions}); RED.user.init(); - RED.library.init(); RED.palette.init(); RED.sidebar.init(); @@ -235,15 +231,21 @@ RED.search.init(); RED.view.init(); RED.editor.init(); + RED.keyboard.init(); + + RED.menu.init({id:"btn-sidemenu",options: menuOptions}); + RED.deploy.init(RED.settings.theme("deployButton",null)); - RED.keyboard.add("workspace", /* ? */ 191,{shift:true},function() {RED.keyboard.showHelp();d3.event.preventDefault();}); + RED.actions.add("core:show-about", showAbout); + RED.comms.connect(); $("#main-container").show(); $(".header-toolbar").show(); + loadNodeList(); } diff --git a/editor/js/ui/actions.js b/editor/js/ui/actions.js new file mode 100644 index 000000000..78b9a4c99 --- /dev/null +++ b/editor/js/ui/actions.js @@ -0,0 +1,35 @@ +RED.actions = (function() { + var actions = { + + } + + function addAction(name,handler) { + actions[name] = handler; + } + function removeAction(name) { + delete actions[name]; + } + function getAction(name) { + return actions[name]; + } + function invokeAction(name) { + if (actions.hasOwnProperty(name)) { + actions[name](); + } + } + function listActions() { + var result = []; + Object.keys(actions).forEach(function(action) { + var shortcut = RED.keyboard.getShortcut(action); + result.push({id:action,scope:shortcut?shortcut.scope:undefined,key:shortcut?shortcut.key:undefined}) + }) + return result; + } + return { + add: addAction, + remove: removeAction, + get: getAction, + invoke: invokeAction, + list: listActions + } +})(); diff --git a/editor/js/ui/clipboard.js b/editor/js/ui/clipboard.js index b648e19ff..b446b40cd 100644 --- a/editor/js/ui/clipboard.js +++ b/editor/js/ui/clipboard.js @@ -257,7 +257,7 @@ RED.clipboard = (function() { function hideDropTarget() { $("#dropTarget").hide(); - RED.keyboard.remove(/* ESCAPE */ 27); + RED.keyboard.remove("escape"); } return { @@ -274,13 +274,15 @@ RED.clipboard = (function() { RED.menu.setDisabled("menu-item-export-library",false); } }); - RED.keyboard.add("workspace", /* e */ 69,{ctrl:true},function(){exportNodes();d3.event.preventDefault();}); - RED.keyboard.add("workspace", /* i */ 73,{ctrl:true},function(){importNodes();d3.event.preventDefault();}); + + RED.actions.add("core:export",exportNodes); + RED.actions.add("core:import",importNodes); + $('#chart').on("dragenter",function(event) { if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) { $("#dropTarget").css({display:'table'}); - RED.keyboard.add("*", /* ESCAPE */ 27,hideDropTarget); + RED.keyboard.add("*", "escape" ,hideDropTarget); } }); diff --git a/editor/js/ui/common/menu.js b/editor/js/ui/common/menu.js index b2a7ee6c6..79fc3a286 100644 --- a/editor/js/ui/common/menu.js +++ b/editor/js/ui/common/menu.js @@ -13,9 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ - - - RED.menu = (function() { var menuItems = {}; @@ -34,17 +31,17 @@ RED.menu = (function() { var savedStateActive = isSavedStateActive(opt.id); if (savedStateActive) { link.addClass("active"); - opt.onselect.call(opt, true); + triggerAction(opt.id,true); } else if (savedStateActive === false) { link.removeClass("active"); - opt.onselect.call(opt, false); + triggerAction(opt.id,false); } else if (opt.hasOwnProperty("selected")) { if (opt.selected) { link.addClass("active"); } else { link.removeClass("active"); } - opt.onselect.call(opt, opt.selected); + triggerAction(opt.id,opt.selected); } } @@ -107,10 +104,12 @@ RED.menu = (function() { setSelected(opt.id, !selected); } } else { - opt.onselect.call(opt); + triggerAction(opt.id); } }); - setInitialState(); + if (opt.toggle) { + setInitialState(); + } } else if (opt.href) { link.attr("target","_blank").attr("href",opt.href); } else if (!opt.options) { @@ -164,6 +163,19 @@ RED.menu = (function() { } } + function triggerAction(id, args) { + var opt = menuItems[id]; + var callback = opt.onselect; + if (typeof opt.onselect === 'string') { + callback = RED.actions.get(opt.onselect); + } + if (callback) { + callback.call(opt,args); + } else { + console.log("No callback for",id,opt.onselect); + } + } + function isSavedStateActive(id) { return RED.settings.get("menu-" + id); } @@ -187,11 +199,15 @@ RED.menu = (function() { $("#"+id).removeClass("active"); } if (opt && opt.onselect) { - opt.onselect.call(opt,state); + triggerAction(opt.id,state); } setSavedState(id, state); } + function toggleSelected(id) { + setSelected(id,!isSelected(id)); + } + function setDisabled(id,state) { if (state) { $("#"+id).parent().addClass("disabled"); @@ -231,16 +247,6 @@ RED.menu = (function() { var opt = menuItems[id]; if (opt) { opt.onselect = action; - // $("#"+id).click(function() { - // if ($(this).parent().hasClass("disabled")) { - // return; - // } - // if (menuItems[id].toggle) { - // setSelected(id,!isSelected(id)); - // } else { - // menuItems[id].onselect.call(menuItems[id]); - // } - // }); } } @@ -248,6 +254,7 @@ RED.menu = (function() { init: createMenu, setSelected: setSelected, isSelected: isSelected, + toggleSelected: toggleSelected, setDisabled: setDisabled, addItem: addItem, removeItem: removeItem, diff --git a/editor/js/ui/common/tabs.js b/editor/js/ui/common/tabs.js index 783db3d41..3f96d074b 100644 --- a/editor/js/ui/common/tabs.js +++ b/editor/js/ui/common/tabs.js @@ -126,6 +126,18 @@ RED.tabs = (function() { },100); } } + function activatePreviousTab() { + var previous = ul.find("li.active").prev(); + if (previous.length > 0) { + activateTab(previous.find("a")); + } + } + function activateNextTab() { + var next = ul.find("li.active").next(); + if (next.length > 0) { + activateTab(next.find("a")); + } + } function updateTabWidths() { var tabs = ul.find("li.red-ui-tab"); @@ -303,6 +315,8 @@ RED.tabs = (function() { }, removeTab: removeTab, activateTab: activateTab, + nextTab: activateNextTab, + previousTab: activatePreviousTab, resize: updateTabWidths, count: function() { return ul.find("li.red-ui-tab").size(); diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index 915c29649..90a55e1d2 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -1532,14 +1532,13 @@ RED.editor = (function() { return { init: function() { RED.tray.init(); - $(window).on('keydown', function(evt) { - if (evt.keyCode === $.ui.keyCode.ESCAPE && (evt.metaKey || evt.ctrlKey)) { - $("#node-dialog-cancel").click(); - $("#node-config-dialog-cancel").click(); - } else if (evt.keyCode === $.ui.keyCode.ENTER && (evt.metaKey || evt.ctrlKey)) { - $("#node-dialog-ok").click(); - $("#node-config-dialog-ok").click(); - } + RED.actions.add("core:confirm-edit-tray", function() { + $("#node-dialog-ok").click(); + $("#node-config-dialog-ok").click(); + }); + RED.actions.add("core:cancel-edit-tray", function() { + $("#node-dialog-cancel").click(); + $("#node-config-dialog-cancel").click(); }); }, edit: showEditDialog, diff --git a/editor/js/ui/keyboard.js b/editor/js/ui/keyboard.js index e70e7d731..a7f815b0f 100644 --- a/editor/js/ui/keyboard.js +++ b/editor/js/ui/keyboard.js @@ -16,9 +16,98 @@ RED.keyboard = (function() { var handlers = {}; + var partialState; + + var keyMap = { + "left":37, + "up":38, + "right":39, + "down":40, + "escape":27, + "enter": 13, + "backspace": 8, + "delete": 46, + "space": 32, + ";":186, + "=":187, + ",":188, + "-":189, + ".":190, + "/":191, + "\\":220, + "'":222, + "?":191 // <- QWERTY specific + } + var metaKeyCodes = { + 16:true, + 17:true, + 18: true, + 91:true, + 93: true + } + var actionToKeyMap = {} + + // FF generates some different keycodes because reasons. + var firefoxKeyCodeMap = { + 59:186, + 61:187, + 173:189 + } + + function init() { + $.getJSON("red/keymap.json",function(data) { + for (var scope in data) { + if (data.hasOwnProperty(scope)) { + var keys = data[scope]; + for (var key in keys) { + if (keys.hasOwnProperty(key)) { + addHandler(scope,key,keys[key]); + } + } + } + } + }) + RED.actions.add("core:show-help", showKeyboardHelp); + + } + function parseKeySpecifier(key) { + var parts = key.toLowerCase().split("-"); + var modifiers = {}; + var keycode; + var blank = 0; + for (var i=0;i 1) { + return null; + } else { + keycode = parts[i].toUpperCase().charCodeAt(0); + } + break; + } + } + return [keycode,modifiers]; + } function resolveKeyEvent(evt) { - var slot = handlers; + var slot = partialState||handlers; if (evt.ctrlKey || evt.metaKey) { slot = slot.ctrl; } @@ -28,9 +117,19 @@ RED.keyboard = (function() { if (slot && evt.altKey) { slot = slot.alt; } - if (slot && slot[evt.keyCode]) { - var handler = slot[evt.keyCode]; - if (handler.scope && handler.scope !== "*") { + var keyCode = firefoxKeyCodeMap[evt.keyCode] || evt.keyCode; + if (slot && slot[keyCode]) { + var handler = slot[keyCode]; + if (!handler.scope) { + if (partialState) { + partialState = null; + return resolveKeyEvent(evt); + } else { + partialState = handler; + evt.preventDefault(); + return null; + } + } else if (handler.scope && handler.scope !== "*") { var target = evt.target; while (target.nodeName !== 'BODY' && target.id !== handler.scope) { target = target.parentElement; @@ -39,66 +138,124 @@ RED.keyboard = (function() { handler = null; } } + partialState = null; return handler; + } else if (partialState) { + partialState = null; + return resolveKeyEvent(evt); } } d3.select(window).on("keydown",function() { + if (metaKeyCodes[d3.event.keyCode]) { + return; + } var handler = resolveKeyEvent(d3.event); if (handler && handler.ondown) { - handler.ondown(); - } - }); - d3.select(window).on("keyup",function() { - var handler = resolveKeyEvent(d3.event); - if (handler && handler.onup) { - handler.onup(); + if (typeof handler.ondown === "string") { + RED.actions.invoke(handler.ondown); + } else { + handler.ondown(); + } + d3.event.preventDefault(); } }); - function addHandler(scope,key,modifiers,ondown,onup) { + function addHandler(scope,key,modifiers,ondown) { var mod = modifiers; var cbdown = ondown; - var cbup = onup; - if (typeof modifiers == "function") { + if (typeof modifiers == "function" || typeof modifiers === "string") { mod = {}; cbdown = modifiers; - cbup = ondown; + } + var keys = []; + var i=0; + if (typeof key === 'string') { + if (typeof cbdown === 'string') { + actionToKeyMap[cbdown] = {scope:scope,key:key}; + } + var parts = key.split(" "); + for (i=0;i'; + function showKeyboardHelp() { if (!RED.settings.theme("menu.menu-item-keyboard-shortcuts",true)) { return; @@ -107,30 +264,30 @@ RED.keyboard = (function() { dialog = $('
    '+ '
    '+ ''+ - ''+ + ''+ ''+ - ''+ + ''+ ''+ ''+ ''+ ''+ - ''+ - ''+ + ''+ + ''+ '
    Ctrl/⌘ + a'+RED._("keyboard.selectAll")+'
    '+cmdCtrlKey+' + a'+RED._("keyboard.selectAll")+'
    Shift + Click'+RED._("keyboard.selectAllConnected")+'
    Ctrl/⌘ + Click'+RED._("keyboard.addRemoveNode")+'
    '+cmdCtrlKey+' + Click'+RED._("keyboard.addRemoveNode")+'
     
    Enter'+RED._("keyboard.editSelected")+'
    Delete / Backspace'+RED._("keyboard.deleteSelected")+'
     
    Ctrl/⌘ + i'+RED._("keyboard.importNode")+'
    Ctrl/⌘ + e'+RED._("keyboard.exportNode")+'
    '+cmdCtrlKey+' + i'+RED._("keyboard.importNode")+'
    '+cmdCtrlKey+' + e'+RED._("keyboard.exportNode")+'
    '+ '
    '+ '
    '+ ''+ - ''+ - ''+ - ''+ + ''+ + ''+ + ''+ ''+ ''+ ''+ ''+ - ''+ - ''+ - ''+ - ''+ + ''+ + ''+ + ''+ + ''+ '
    Ctrl/⌘ + Space'+RED._("keyboard.toggleSidebar")+'
    Ctrl/⌘ + .'+RED._("keyboard.searchBox")+'
    Ctrl/⌘ + Shift + p'+RED._("keyboard.managePalette")+'
    '+cmdCtrlKey+' + Space'+RED._("keyboard.toggleSidebar")+'
    '+cmdCtrlKey+' + f'+RED._("keyboard.searchBox")+'
    '+cmdCtrlKey+' + Shift + p'+RED._("keyboard.managePalette")+'
     
    '+RED._("keyboard.nudgeNode")+'
    Shift + '+RED._("keyboard.moveNode")+'
     
    Ctrl/⌘ + c'+RED._("keyboard.copyNode")+'
    Ctrl/⌘ + x'+RED._("keyboard.cutNode")+'
    Ctrl/⌘ + v'+RED._("keyboard.pasteNode")+'
    Ctrl/⌘ + z'+RED._("keyboard.undoChange")+'
    '+cmdCtrlKey+' + c'+RED._("keyboard.copyNode")+'
    '+cmdCtrlKey+' + x'+RED._("keyboard.cutNode")+'
    '+cmdCtrlKey+' + v'+RED._("keyboard.pasteNode")+'
    '+cmdCtrlKey+' + z'+RED._("keyboard.undoChange")+'
    '+ '
    '+ '
    ') @@ -148,9 +305,12 @@ RED.keyboard = (function() { } return { + init: init, add: addHandler, remove: removeHandler, - showHelp: showKeyboardHelp + getShortcut: function(actionName) { + return actionToKeyMap[actionName]; + } } })(); diff --git a/editor/js/ui/library.js b/editor/js/ui/library.js index e9435317b..4291bf4a1 100644 --- a/editor/js/ui/library.js +++ b/editor/js/ui/library.js @@ -410,6 +410,9 @@ RED.library = (function() { return { init: function() { + + RED.actions.add("core:library-export",exportFlow); + RED.events.on("view:selection-changed",function(selection) { if (!selection.nodes) { RED.menu.setDisabled("menu-item-export",true); diff --git a/editor/js/ui/palette-editor.js b/editor/js/ui/palette-editor.js index 9caf1024d..d5a537699 100644 --- a/editor/js/ui/palette-editor.js +++ b/editor/js/ui/palette-editor.js @@ -302,10 +302,10 @@ RED.palette.editor = (function() { filterInput.focus(); },250); RED.events.emit("palette-editor:open"); - RED.keyboard.add("*",/* ESCAPE */ 27,function(){hidePaletteEditor();d3.event.preventDefault();}); + RED.keyboard.add("*","escape",function(){hidePaletteEditor()}); } function hidePaletteEditor() { - RED.keyboard.remove("*"); + RED.keyboard.remove("escape"); $("#main-container").removeClass("palette-expanded"); $("#header-shade").hide(); $("#editor-shade").hide(); @@ -425,7 +425,7 @@ RED.palette.editor = (function() { RED.events.on("type-search:open",function() { disabled = true; }); RED.events.on("type-search:close",function() { disabled = false; }); - RED.keyboard.add("*", /* p */ 80,{shift:true,ctrl:true},function() {RED.palette.editor.show();d3.event.preventDefault();}); + RED.actions.add("core:manage-palette",RED.palette.editor.show); editorTabs = RED.tabs.create({ id:"palette-editor-tabs", diff --git a/editor/js/ui/search.js b/editor/js/ui/search.js index 032fc69b7..9f30dbd4f 100644 --- a/editor/js/ui/search.js +++ b/editor/js/ui/search.js @@ -238,8 +238,11 @@ RED.search = (function() { } function show() { + if (disabled) { + return; + } if (!visible) { - RED.keyboard.add("*",/* ESCAPE */ 27,function(){hide();d3.event.preventDefault();}); + RED.keyboard.add("*","escape",function(){hide()}); $("#header-shade").show(); $("#editor-shade").show(); $("#palette-shade").show(); @@ -257,7 +260,7 @@ RED.search = (function() { } function hide() { if (visible) { - RED.keyboard.remove(/* ESCAPE */ 27); + RED.keyboard.remove("escape"); visible = false; $("#header-shade").hide(); $("#editor-shade").hide(); @@ -274,7 +277,8 @@ RED.search = (function() { } function init() { - RED.keyboard.add("*",/* . */ 190,{ctrl:true},function(){if (!disabled) { show(); } d3.event.preventDefault();}); + RED.actions.add("core:search",show); + RED.events.on("editor:open",function() { disabled = true; }); RED.events.on("editor:close",function() { disabled = false; }); RED.events.on("palette-editor:open",function() { disabled = true; }); diff --git a/editor/js/ui/sidebar.js b/editor/js/ui/sidebar.js index 9a3e75dae..3100476b0 100644 --- a/editor/js/ui/sidebar.js +++ b/editor/js/ui/sidebar.js @@ -202,12 +202,18 @@ RED.sidebar = (function() { } function init () { - RED.keyboard.add("*",/* SPACE */ 32,{ctrl:true},function(){RED.menu.setSelected("menu-item-sidebar",!RED.menu.isSelected("menu-item-sidebar"));d3.event.preventDefault();}); + RED.actions.add("core:toggle-sidebar",function(state){ + if (state === undefined) { + RED.menu.toggleSelected("menu-item-sidebar"); + } else { + toggleSidebar(state); + } + }); showSidebar(); RED.sidebar.info.init(); RED.sidebar.config.init(); // hide info bar at start if screen rather narrow... - if ($(window).width() < 600) { toggleSidebar(); } + if ($(window).width() < 600) { RED.menu.setSelected("menu-item-sidebar",false); } } return { diff --git a/editor/js/ui/subflow.js b/editor/js/ui/subflow.js index 8b7d2b03a..995cb3df5 100644 --- a/editor/js/ui/subflow.js +++ b/editor/js/ui/subflow.js @@ -380,6 +380,8 @@ RED.subflow = (function() { } }); + RED.actions.add("core:create-subflow",createSubflow); + RED.actions.add("core:convert-to-subflow",convertToSubflow); } function createSubflow() { diff --git a/editor/js/ui/tab-config.js b/editor/js/ui/tab-config.js index 68b7dc90e..41516029e 100644 --- a/editor/js/ui/tab-config.js +++ b/editor/js/ui/tab-config.js @@ -236,10 +236,7 @@ RED.sidebar.config = (function() { visible: false, onchange: function() { refreshConfigNodeList(); } }); - - RED.menu.setAction('menu-item-config-nodes',function() { - RED.sidebar.show('config'); - }) + RED.actions.add("core:show-config-tab",function() {RED.sidebar.show('config')}); $("#workspace-config-node-collapse-all").on("click", function(e) { e.preventDefault(); diff --git a/editor/js/ui/tab-info.js b/editor/js/ui/tab-info.js index 1daf683ff..5f4b5406d 100644 --- a/editor/js/ui/tab-info.js +++ b/editor/js/ui/tab-info.js @@ -42,7 +42,7 @@ RED.sidebar.info = (function() { content: content, enableOnEdit: true }); - + RED.actions.add("core:show-info-tab",show); } function show() { diff --git a/editor/js/ui/typeSearch.js b/editor/js/ui/typeSearch.js index d9b02338e..0105d1799 100644 --- a/editor/js/ui/typeSearch.js +++ b/editor/js/ui/typeSearch.js @@ -168,7 +168,7 @@ RED.typeSearch = (function() { } function show(opts) { if (!visible) { - RED.keyboard.add("*",/* ESCAPE */ 27,function(){hide();d3.event.preventDefault();}); + RED.keyboard.add("*","escape",function(){hide()}); if (dialog === null) { createDialog(); } @@ -195,7 +195,7 @@ RED.typeSearch = (function() { } function hide(fast) { if (visible) { - RED.keyboard.remove(/* ESCAPE */ 27); + RED.keyboard.remove("escape"); visible = false; if (dialog !== null) { searchResultsDiv.slideUp(fast?50:200,function() { diff --git a/editor/js/ui/view.js b/editor/js/ui/view.js index 18e9f0471..4a40c28cf 100644 --- a/editor/js/ui/view.js +++ b/editor/js/ui/view.js @@ -267,6 +267,7 @@ RED.view = (function() { } function init() { + RED.events.on("workspace:change",function(event) { var chart = $("#chart"); if (event.old !== 0) { @@ -384,28 +385,47 @@ RED.view = (function() { } }); - RED.keyboard.add("workspace",/* backspace */ 8,function(){deleteSelection();d3.event.preventDefault();}); - RED.keyboard.add("workspace",/* delete */ 46,function(){deleteSelection();d3.event.preventDefault();}); - RED.keyboard.add("workspace",/* enter */ 13, function() { editSelection(); d3.event.preventDefault();}); + RED.actions.add("core:copy",copySelection); + RED.actions.add("core:cut",function(){copySelection();deleteSelection();}); + RED.actions.add("core:paste",function(){importNodes(clipboard);}); + RED.actions.add("core:delete",deleteSelection); + RED.actions.add("core:edit",editSelection); + RED.actions.add("core:undo",RED.history.pop); + RED.actions.add("core:select-all",selectAll); + RED.actions.add("core:zoom-in",zoomIn); + RED.actions.add("core:zoom-out",zoomOut); + RED.actions.add("core:zoom-reset",zoomZero); - RED.keyboard.add("workspace",/* c */ 67,{ctrl:true},function(){copySelection();d3.event.preventDefault();}); - RED.keyboard.add("workspace",/* x */ 88,{ctrl:true},function(){copySelection();deleteSelection();d3.event.preventDefault();}); + RED.actions.add("core:toggle-show-grid",function(state) { + if (state === undefined) { + RED.menu.toggleSelected("menu-item-view-show-grid"); + } else { + toggleShowGrid(state); + } + }); + RED.actions.add("core:toggle-snap-grid",function(state) { + if (state === undefined) { + RED.menu.toggleSelected("menu-item-view-snap-grid"); + } else { + toggleSnapGrid(state); + } + }); + RED.actions.add("core:toggle-status",function(state) { + if (state === undefined) { + RED.menu.toggleSelected("menu-item-status"); + } else { + toggleStatus(state); + } + }); - RED.keyboard.add("workspace",/* z */ 90,{ctrl:true},function(){RED.history.pop();}); - RED.keyboard.add("workspace",/* a */ 65,{ctrl:true},function(){selectAll();d3.event.preventDefault();}); - RED.keyboard.add("*",/* = */ 187,{ctrl:true},function(){zoomIn();d3.event.preventDefault();}); - RED.keyboard.add("*",/* - */ 189,{ctrl:true},function(){zoomOut();d3.event.preventDefault();}); - RED.keyboard.add("*",/* 0 */ 48,{ctrl:true},function(){zoomZero();d3.event.preventDefault();}); - RED.keyboard.add("workspace",/* v */ 86,{ctrl:true},function(){importNodes(clipboard);d3.event.preventDefault();}); - - RED.keyboard.add("workspace",/* up */ 38, function() { moveSelection(0,-1);d3.event.preventDefault();},endKeyboardMove); - RED.keyboard.add("workspace",/* up */ 38, {shift:true}, function() { moveSelection(0,-20); d3.event.preventDefault();},endKeyboardMove); - RED.keyboard.add("workspace",/* down */ 40, function() { moveSelection(0,1);d3.event.preventDefault();},endKeyboardMove); - RED.keyboard.add("workspace",/* down */ 40, {shift:true}, function() { moveSelection(0,20); d3.event.preventDefault();},endKeyboardMove); - RED.keyboard.add("workspace",/* left */ 37, function() { moveSelection(-1,0);d3.event.preventDefault();},endKeyboardMove); - RED.keyboard.add("workspace",/* left */ 37, {shift:true}, function() { moveSelection(-20,0); d3.event.preventDefault();},endKeyboardMove); - RED.keyboard.add("workspace",/* right */ 39, function() { moveSelection(1,0);d3.event.preventDefault();},endKeyboardMove); - RED.keyboard.add("workspace",/* right */ 39, {shift:true}, function() { moveSelection(20,0); d3.event.preventDefault();},endKeyboardMove); + RED.actions.add("core:move-selection-up", function() { moveSelection(0,-1);}); + RED.actions.add("core:step-selection-up", function() { moveSelection(0,-20);}); + RED.actions.add("core:move-selection-right", function() { moveSelection(1,0);}); + RED.actions.add("core:step-selection-right", function() { moveSelection(20,0);}); + RED.actions.add("core:move-selection-down", function() { moveSelection(0,1);}); + RED.actions.add("core:step-selection-down", function() { moveSelection(0,20);}); + RED.actions.add("core:move-selection-left", function() { moveSelection(-1,0);}); + RED.actions.add("core:step-selection-left", function() { moveSelection(-20,0);}); } @@ -959,7 +979,7 @@ RED.view = (function() { } } if (mouse_mode == RED.state.IMPORT_DRAGGING) { - RED.keyboard.remove(/* ESCAPE */ 27); + RED.keyboard.remove("escape"); updateActiveNodes(); RED.nodes.dirty(true); } @@ -1103,6 +1123,7 @@ RED.view = (function() { } function endKeyboardMove() { + endMoveSet = false; if (moving_set.length > 0) { var ns = []; for (var i=0;i 0) { + if (!endMoveSet) { + $(document).one('keyup',endKeyboardMove); + endMoveSet = true; + } var minX = 0; var minY = 0; var node; for (var i=0;i 0; } } - RED.keyboard.add("*",/* ESCAPE */ 27,function(){ - RED.keyboard.remove(/* ESCAPE */ 27); + RED.keyboard.add("*","escape",function(){ + RED.keyboard.remove("escape"); clearSelection(); RED.history.pop(); mouse_mode = 0; @@ -2483,6 +2511,24 @@ RED.view = (function() { } } + function toggleShowGrid(state) { + if (state) { + grid.style("visibility","visible"); + } else { + grid.style("visibility","hidden"); + } + } + function toggleSnapGrid(state) { + snapGrid = state; + redraw(); + } + function toggleStatus(s) { + showStatus = s; + RED.nodes.eachNode(function(n) { n.dirty = true;}); + //TODO: subscribe/unsubscribe here + redraw(); + } + return { init: init, state:function(state) { @@ -2502,16 +2548,6 @@ RED.view = (function() { }, focus: focusView, importNodes: importNodes, - status: function(s) { - if (s == null) { - return showStatus; - } else { - showStatus = s; - RED.nodes.eachNode(function(n) { n.dirty = true;}); - //TODO: subscribe/unsubscribe here - redraw(); - } - }, calculateTextWidth: calculateTextWidth, select: function(selection) { if (typeof selection !== "undefined") { @@ -2538,17 +2574,6 @@ RED.view = (function() { } return selection; }, - toggleShowGrid: function(state) { - if (state) { - grid.style("visibility","visible"); - } else { - grid.style("visibility","hidden"); - } - }, - toggleSnapGrid: function(state) { - snapGrid = state; - redraw(); - }, scale: function() { return scaleFactor; }, diff --git a/editor/js/ui/workspaces.js b/editor/js/ui/workspaces.js index f19e49543..5bfe7f4ca 100644 --- a/editor/js/ui/workspaces.js +++ b/editor/js/ui/workspaces.js @@ -170,6 +170,9 @@ RED.workspaces = (function() { createWorkspaceTabs(); RED.events.on("sidebar:resize",workspace_tabs.resize); + RED.actions.add("core:show-next-tab",workspace_tabs.nextTab); + RED.actions.add("core:show-previous-tab",workspace_tabs.previousTab); + RED.menu.setAction('menu-item-workspace-delete',function() { deleteWorkspace(RED.nodes.workspace(activeWorkspace)); }); @@ -177,6 +180,14 @@ RED.workspaces = (function() { $(window).resize(function() { workspace_tabs.resize(); }); + + RED.actions.add("core:add-flow",addWorkspace); + RED.actions.add("core:edit-flow",editWorkspace); + RED.actions.add("core:remove-flow",removeWorkspace); + } + + function editWorkspace(id) { + showRenameWorkspaceDialog(id||activeWorkspace); } function removeWorkspace(ws) { @@ -201,9 +212,7 @@ RED.workspaces = (function() { add: addWorkspace, remove: removeWorkspace, order: setWorkspaceOrder, - edit: function(id) { - showRenameWorkspaceDialog(id||activeWorkspace); - }, + edit: editWorkspace, contains: function(id) { return workspace_tabs.contains(id); }, diff --git a/nodes/core/core/58-debug.html b/nodes/core/core/58-debug.html index c784bbdf2..ada8a1c12 100644 --- a/nodes/core/core/58-debug.html +++ b/nodes/core/core/58-debug.html @@ -144,8 +144,7 @@ toolbar: uiComponents.footer, enableOnEdit: true }); - - + RED.actions.add("core:show-debug-tab",function() { RED.sidebar.show('debug')}); var that = this; RED._debug = function(msg) { @@ -222,6 +221,8 @@ RED.sidebar.removeTab("debug"); RED.events.off("workspace:change", this.refreshMessageList); window.removeEventListener("message",this.handleWindowMessage); + RED.actions.remove("core:show-debug"); + delete RED._debug; }, oneditprepare: function() { From 8d21e441a0e9bd8083d3603207e0251c3cfb859e Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 5 Dec 2016 13:24:24 +0000 Subject: [PATCH 19/44] Add notification when runtime stopped due to missing types Part of #832 --- editor/js/main.js | 17 +++++++++++++++++ editor/js/ui/notifications.js | 10 ++++++++-- red/api/comms.js | 7 ++++++- red/api/locales/en-US/editor.json | 3 ++- red/runtime/nodes/flows/index.js | 2 ++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/editor/js/main.js b/editor/js/main.js index 1038facda..4f16af488 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -79,6 +79,23 @@ if (/^#flow\/.+$/.test(currentHash)) { RED.workspaces.show(currentHash.substring(6)); } + + var persistentNotifications = {}; + RED.comms.subscribe("notification/#",function(topic,msg) { + var parts = topic.split("/"); + var notificationId = parts[1]; + if (msg.text) { + var text = RED._(msg.text,{default:msg.text}); + if (!persistentNotifications.hasOwnProperty(notificationId)) { + persistentNotifications[notificationId] = RED.notify(text,msg.type,msg.timeout === undefined,msg.timeout); + } else { + persistentNotifications[notificationId].update(text,msg.timeout); + } + } else if (persistentNotifications.hasOwnProperty(notificationId)) { + persistentNotifications[notificationId].close(); + delete persistentNotifications[notificationId]; + } + }); RED.comms.subscribe("status/#",function(topic,msg) { var parts = topic.split("/"); var node = RED.nodes.node(parts[1]); diff --git a/editor/js/ui/notifications.js b/editor/js/ui/notifications.js index f64bf6f89..6c956be42 100644 --- a/editor/js/ui/notifications.js +++ b/editor/js/ui/notifications.js @@ -51,11 +51,17 @@ RED.notify = (function() { n.update = (function() { var nn = n; - return function(msg) { + return function(msg,timeout) { nn.innerHTML = msg; + if (timeout !== undefined && timeout > 0) { + window.clearTimeout(nn.timeoutid); + nn.timeoutid = window.setTimeout(nn.close,timeout); + } else { + window.clearTimeout(nn.timeoutid); + } } })(); - + if (!fixed) { $(n).click((function() { var nn = n; diff --git a/red/api/comms.js b/red/api/comms.js index 1a51bfa1e..ba64f7a38 100644 --- a/red/api/comms.js +++ b/red/api/comms.js @@ -32,7 +32,9 @@ var lastSentTime; function handleStatus(event) { publish("status/"+event.id,event.status,true); } - +function handleRuntimeEvent(event) { + publish("notification/"+event.id,event,event.hasOwnProperty('text')); +} function init(_server,runtime) { server = _server; settings = runtime.settings; @@ -40,6 +42,9 @@ function init(_server,runtime) { runtime.events.removeListener("node-status",handleStatus); runtime.events.on("node-status",handleStatus); + + runtime.events.removeListener("runtime-event",handleRuntimeEvent); + runtime.events.on("runtime-event",handleRuntimeEvent); } function start() { diff --git a/red/api/locales/en-US/editor.json b/red/api/locales/en-US/editor.json index dca0d7ad7..9f6c7e8fc 100644 --- a/red/api/locales/en-US/editor.json +++ b/red/api/locales/en-US/editor.json @@ -66,7 +66,8 @@ "warning": "Warning: __message__", "warnings": { "undeployedChanges": "node has undeployed changes", - "nodeActionDisabled": "node actions disabled within subflow" + "nodeActionDisabled": "node actions disabled within subflow", + "missing-types": "Flows stopped due to missing node types. Check logs for details." }, "error": "Error: __message__", diff --git a/red/runtime/nodes/flows/index.js b/red/runtime/nodes/flows/index.js index 8aa980e06..234d9e30c 100644 --- a/red/runtime/nodes/flows/index.js +++ b/red/runtime/nodes/flows/index.js @@ -58,6 +58,7 @@ function init(runtime) { log.info(log._("nodes.flows.registered-missing", {type:type})); activeFlowConfig.missingTypes.splice(i,1); if (activeFlowConfig.missingTypes.length === 0 && started) { + events.emit("runtime-event",{id:"runtime-state"}); start(); } } @@ -238,6 +239,7 @@ function start(type,diff,muteLog) { log.info(log._("nodes.flows.missing-type-install-2")); log.info(" "+settings.userDir); } + events.emit("runtime-event",{id:"runtime-state",type:"warning",text:"notification.warnings.missing-types"}); return when.resolve(); } if (!muteLog) { From c720d78c391460e0af7704f535926589cf3a9e65 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 5 Dec 2016 14:39:34 +0000 Subject: [PATCH 20/44] Ensure runtime event notification gets cleared on restart --- red/api/comms.js | 2 +- red/runtime/nodes/flows/index.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/red/api/comms.js b/red/api/comms.js index ba64f7a38..49fb669c2 100644 --- a/red/api/comms.js +++ b/red/api/comms.js @@ -33,7 +33,7 @@ function handleStatus(event) { publish("status/"+event.id,event.status,true); } function handleRuntimeEvent(event) { - publish("notification/"+event.id,event,event.hasOwnProperty('text')); + publish("notification/"+event.id,event,true); } function init(_server,runtime) { server = _server; diff --git a/red/runtime/nodes/flows/index.js b/red/runtime/nodes/flows/index.js index 234d9e30c..b0358235e 100644 --- a/red/runtime/nodes/flows/index.js +++ b/red/runtime/nodes/flows/index.js @@ -289,6 +289,8 @@ function start(type,diff,muteLog) { } } events.emit("nodes-started"); + events.emit("runtime-event",{id:"runtime-state"}); + if (!muteLog) { if (diff) { log.info(log._("nodes.flows.started-modified-"+type)); From 932ea7ba8f4b3b1fdbce575a846abb634483aeee Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 6 Dec 2016 22:37:21 +0000 Subject: [PATCH 21/44] Add flow diff view --- Gruntfile.js | 1 + editor/js/main.js | 2 +- editor/js/nodes.js | 12 ++ editor/js/ui/deploy.js | 310 +------------------------------------ editor/sass/diff.scss | 96 ++++++++++-- editor/templates/index.mst | 3 - 6 files changed, 97 insertions(+), 327 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index f9531d1c2..60b474103 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -119,6 +119,7 @@ module.exports = function(grunt) { "editor/js/ui/utils.js", "editor/js/ui/actions.js", "editor/js/ui/deploy.js", + "editor/js/ui/diff.js", "editor/js/ui/keyboard.js", "editor/js/ui/workspaces.js", "editor/js/ui/view.js", diff --git a/editor/js/main.js b/editor/js/main.js index 4f16af488..17dd1963a 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -249,10 +249,10 @@ RED.view.init(); RED.editor.init(); RED.keyboard.init(); + RED.diff.init(); RED.menu.init({id:"btn-sidemenu",options: menuOptions}); - RED.deploy.init(RED.settings.theme("deployButton",null)); RED.actions.add("core:show-about", showAbout); diff --git a/editor/js/nodes.js b/editor/js/nodes.js index 49bd29860..e471d9081 100644 --- a/editor/js/nodes.js +++ b/editor/js/nodes.js @@ -29,6 +29,8 @@ RED.nodes = (function() { added: {} }; + var initialLoad; + var dirty = false; function setDirty(d) { @@ -697,6 +699,9 @@ RED.nodes = (function() { if (!$.isArray(newNodes)) { newNodes = [newNodes]; } + if (!initialLoad) { + initialLoad = JSON.parse(JSON.stringify(newNodes)); + } var unknownTypes = []; for (i=0;i',{class:"node-diff-tab collapsed"}).appendTo(container); - // - // var titleRow = $('
    ',{class:"node-diff-tab-title"}).appendTo(tabDiv); - // titleRow.click(function(evt) { - // evt.preventDefault(); - // titleRow.parent().toggleClass('collapsed'); - // }) - // var chevron = $('').appendTo(titleRow); - // var title = $('').html(tab.label||tab.id).appendTo(titleRow); - // - // var stats = $('',{class:"node-diff-tab-stats"}).appendTo(titleRow); - // - // var addedCount = 0; - // var deletedCount = 0; - // var changedCount = 0; - // var conflictedCount = 0; - // - // object.tab.nodes.forEach(function(node) { - // var realNode = RED.nodes.node(node.id); - // var hasChanges = false; - // if (currentDiff.added[node.id]) { - // addedCount++; - // hasChanges = true; - // } - // if (currentDiff.deleted[node.id]) { - // deletedCount++; - // hasChanges = true; - // } - // if (currentDiff.changed[node.id]) { - // changedCount++; - // hasChanges = true; - // } - // if (currentDiff.conflicted[node.id]) { - // conflictedCount++; - // hasChanges = true; - // } - // - // if (hasChanges) { - // var def = RED.nodes.getType(node.type)||{}; - // var div = $("
    ",{class:"node-diff-node-entry collapsed"}).appendTo(tabDiv); - // var nodeTitleDiv = $("
    ",{class:"node-diff-node-entry-title"}).appendTo(div); - // nodeTitleDiv.click(function(evt) { - // evt.preventDefault(); - // $(this).parent().toggleClass('collapsed'); - // }) - // var newNode = currentDiff.newConfig.all[node.id]; - // var nodePropertiesDiv = $("
    ",{class:"node-diff-node-entry-properties"}).appendTo(div); - // - // var nodePropertiesTable = $("").appendTo(nodePropertiesDiv); - // - // if (node.hasOwnProperty('x')) { - // if (newNode.x !== node.x || newNode.y !== node.y) { - // var currentPosition = node.x+", "+node.y - // var newPosition = newNode.x+", "+newNode.y; - // $("").appendTo(nodePropertiesTable); - // } - // } - // var properties = Object.keys(node).filter(function(p) { return p!='z'&&p!='wires'&&p!=='x'&&p!=='y'&&p!=='id'&&p!=='type'&&(!def.defaults||!def.defaults.hasOwnProperty(p))}); - // if (def.defaults) { - // properties = properties.concat(Object.keys(def.defaults)); - // } - // properties.forEach(function(d) { - // var localValue = JSON.stringify(node[d]); - // var remoteValue = JSON.stringify(newNode[d]); - // var originalValue = realNode._config[d]; - // - // if (remoteValue !== originalValue) { - // var formattedProperty = formatNodeProperty(node[d]); - // var newFormattedProperty = formatNodeProperty(newNode[d]); - // if (localValue === originalValue) { - // // no conflict change - // } else { - // // conflicting change - // } - // $("").appendTo(nodePropertiesTable); - // } - // - // }) - // var nodeChevron = $('').appendTo(nodeTitleDiv); - // - // - // // var leftColumn = $('
    ',{class:"node-diff-column"}).appendTo(div); - // // var rightColumn = $('
    ',{class:"node-diff-column"}).appendTo(div); - // // rightColumn.html(" "); - // - // - // - // var nodeDiv = $("
    ",{class:"node-diff-node-entry-node"}).appendTo(nodeTitleDiv); - // var colour = def.color; - // var icon_url = "arrow-in.png"; - // if (node.type === 'tab') { - // colour = "#C0DEED"; - // icon_url = "subflow.png"; - // } else if (def.category === 'config') { - // icon_url = "cog.png"; - // } else if (node.type === 'unknown') { - // icon_url = "alert.png"; - // } else { - // icon_url = def.icon; - // } - // nodeDiv.css('backgroundColor',colour); - // - // var iconContainer = $('
    ',{class:"palette_icon_container"}).appendTo(nodeDiv); - // $('
    ',{class:"palette_icon",style:"background-image: url(icons/"+icon_url+")"}).appendTo(iconContainer); - // - // - // - // var contentDiv = $('
    ',{class:"node-diff-node-description"}).appendTo(nodeTitleDiv); - // - // $('',{class:"node-diff-node-label"}).html(node.label || node.name || node.id).appendTo(contentDiv); - // //$('
    ',{class:"red-ui-search-result-node-type"}).html(node.type).appendTo(contentDiv); - // //$('
    ',{class:"red-ui-search-result-node-id"}).html(node.id).appendTo(contentDiv); - // } - // - // }); - // - // var statsInfo = ''+object.tab.nodes.length+" nodes"+ - // (addedCount+deletedCount+changedCount+conflictedCount > 0 ? " : ":"")+ - // " "+ - // ((addedCount > 0)?''+addedCount+' added ':'')+ - // ((deletedCount > 0)?''+deletedCount+' deleted ':'')+ - // ((changedCount > 0)?''+changedCount+' changed ':'')+ - // ((conflictedCount > 0)?''+conflictedCount+' conflicts':''); - // stats.html(statsInfo); - // - // - // - // // - // // - // // - // // var node = object.node; - // // var realNode = RED.nodes.node(node.id); - // // var def = RED.nodes.getType(object.node.type)||{}; - // // var l = ""; - // // if (def && def.label && realNode) { - // // l = def.label; - // // try { - // // l = (typeof l === "function" ? l.call(realNode) : l); - // // } catch(err) { - // // console.log("Definition error: "+node.type+".label",err); - // // } - // // } - // // l = l||node.label||node.name||node.id||""; - // // console.log(node); - // // var div = $('
    ').appendTo(container); - // // div.html(l); - // } - // }); - } - - function formatNodeProperty(prop) { - var formattedProperty = prop; - if (formattedProperty === null) { - formattedProperty = 'null'; - } else if (formattedProperty === undefined) { - formattedProperty = 'undefined'; - } else if (typeof formattedProperty === 'object') { - formattedProperty = JSON.stringify(formattedProperty); - } - if (/\n/.test(formattedProperty)) { - formattedProperty = "
    "+formattedProperty+"
    " - } - return formattedProperty; } function getNodeInfo(node) { @@ -414,124 +223,6 @@ RED.deploy = (function() { // }); } - // function parseNodes(nodeList) { - // var tabOrder = []; - // var tabs = {}; - // var subflows = {}; - // var globals = []; - // var all = {}; - // - // nodeList.forEach(function(node) { - // all[node.id] = node; - // if (node.type === 'tab') { - // tabOrder.push(node.id); - // tabs[node.id] = {n:node,nodes:[]}; - // } else if (node.type === 'subflow') { - // subflows[node.id] = {n:node,nodes:[]}; - // } - // }); - // - // nodeList.forEach(function(node) { - // if (node.type !== 'tab' && node.type !== 'subflow') { - // if (tabs[node.z]) { - // tabs[node.z].nodes.push(node); - // } else if (subflows[node.z]) { - // subflows[node.z].nodes.push(node); - // } else { - // globals.push(node); - // } - // } - // }); - // - // return { - // all: all, - // tabOrder: tabOrder, - // tabs: tabs, - // subflows: subflows, - // globals: globals - // } - // } - - // function generateDiff(currentNodes,newNodes) { - // var currentConfig = parseNodes(currentNodes); - // var newConfig = parseNodes(newNodes); - // var pending = RED.nodes.pending(); - // var added = {}; - // var deleted = {}; - // var changed = {}; - // var conflicted = {}; - // - // - // Object.keys(currentConfig.all).forEach(function(id) { - // var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); - // if (!newConfig.all.hasOwnProperty(id)) { - // if (!pending.added.hasOwnProperty(id)) { - // deleted[id] = true; - // conflicted[id] = node.changed; - // } - // } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) { - // changed[id] = true; - // conflicted[id] = node.changed; - // } - // }); - // Object.keys(newConfig.all).forEach(function(id) { - // if (!currentConfig.all.hasOwnProperty(id) && !pending.deleted.hasOwnProperty(id)) { - // added[id] = true; - // } - // }); - // - // // console.log("Added",added); - // // console.log("Deleted",deleted); - // // console.log("Changed",changed); - // // console.log("Conflicted",conflicted); - // - // var formatString = function(id) { - // return conflicted[id]?"!":(added[id]?"+":(deleted[id]?"-":(changed[id]?"~":" "))); - // } - // newConfig.tabOrder.forEach(function(tabId) { - // var tab = newConfig.tabs[tabId]; - // console.log(formatString(tabId),"Flow:",tab.n.label, "("+tab.n.id+")"); - // tab.nodes.forEach(function(node) { - // console.log(" ",formatString(node.id),node.type,node.name || node.id); - // }) - // if (currentConfig.tabs[tabId]) { - // currentConfig.tabs[tabId].nodes.forEach(function(node) { - // if (deleted[node.id]) { - // console.log(" ",formatString(node.id),node.type,node.name || node.id); - // } - // }) - // } - // }); - // currentConfig.tabOrder.forEach(function(tabId) { - // if (deleted[tabId]) { - // console.log(formatString(tabId),"Flow:",tab.n.label, "("+tab.n.id+")"); - // } - // }); - // - // currentDiff = { - // currentConfig: currentConfig, - // newConfig: newConfig, - // added: added, - // deleted: deleted, - // changed: changed, - // conflicted: conflicted - // } - // } - - // function showDiff() { - // if (currentDiff) { - // var list = $("#node-dialog-view-diff-diff"); - // list.editableList('empty'); - // var currentConfig = currentDiff.currentConfig; - // currentConfig.tabOrder.forEach(function(tabId) { - // var tab = currentConfig.tabs[tabId]; - // list.editableList('addItem',{tab:tab}) - // }); - // } - // $("#node-dialog-view-diff").dialog("open"); - // } - - function save(skipValidation,force) { if (!$("#btn-deploy").hasClass("disabled")) { if (!skipValidation) { @@ -624,6 +315,7 @@ RED.deploy = (function() { }).done(function(data,textStatus,xhr) { RED.nodes.dirty(false); RED.nodes.version(data.rev); + RED.nodes.originalFlow(nns); if (hasUnusedConfig) { RED.notify( '

    '+RED._("deploy.successfulDeploy")+'

    '+ diff --git a/editor/sass/diff.scss b/editor/sass/diff.scss index 8c27f4ad4..8f6febf00 100644 --- a/editor/sass/diff.scss +++ b/editor/sass/diff.scss @@ -22,26 +22,40 @@ border-radius:1px; padding:0; } - ol { + #node-dialog-view-diff-diff { position: absolute; - top:10px; + top:50px; bottom:10px; left:10px; right:10px; li { padding: 0px; border: none; + min-height: 0; } } .red-ui-editableList-item-content { padding: 5px; + padding-bottom: 0; } - +} +.node-diff-toolbar { + position:absolute; + top:0; + left:0; + right:0; + color: #666; + text-align: right; + padding: 8px 10px; + background: #f3f3f3; + border-bottom: 1px solid $secondary-border-color; + white-space: nowrap; } .node-diff-tab { border: 1px solid $secondary-border-color; border-radius: 3px; + overflow: hidden; &.collapsed { .node-diff-tab-title > .node-diff-chevron { @@ -55,9 +69,11 @@ .node-diff-tab-stats { position: absolute; left: 50%; + top: 13px; } .node-diff-chevron { + display: inline-block; width: 15px; text-align: center; margin: 3px 5px 3px 5px; @@ -65,10 +81,7 @@ } .node-diff-node-entry { - padding: 0 0 0 5px; - &:not(:last-child) { - border-bottom: 1px solid $secondary-border-color; - } + border-top: 1px solid $secondary-border-color; &.collapsed { .node-diff-chevron { @@ -88,13 +101,16 @@ border: 1px solid $secondary-border-color; padding: 3px 5px; text-align: left; + overflow-x: auto; + } + tr { + vertical-align: top; } - td:nth-child(1) { - width: 150px; + width: 100px; } td:not(:first-child) { - width: calc(50% - 150px); + width: calc(50% - 100px); } } .node-diff-column { @@ -108,6 +124,7 @@ border-right: 1px solid $secondary-border-color } } + .node-diff-tab-title { padding: 3px 3px 3px 0; background: #f6f6f6; @@ -135,15 +152,60 @@ width: 24px; } } +.node-diff-tab-empty { + .node-diff-chevron i { + display: none; + } + .node-diff-tab-title { + cursor: default; + } +} +.node-diff-node-deleted { + //background: #fadddd; + cursor: default !important; + .node-diff-status { + color: #f80000; + } + .node-diff-node-entry-node { + opacity: 0.5; + } + .node-diff-node-description { + opacity: 0.5; + text-decoration: line-through; + } +} +.node-diff-node-added { + //background: #eefaee; + cursor: default !important; + .node-diff-status { + color: #009900; + } +} +.node-diff-node-changed { + //background: #fff2ca; + .node-diff-status { + color: #f89406; + } +} .node-diff-node-entry-title { cursor: pointer; + .node-diff-status { + margin-left: 15px; + } } .node-diff-node-entry-properties { - margin-left: 30px; - margin-right: 8px; - margin-bottom:8px; + margin: 6px 8px 6px 30px; color: #666; } +.node-diff-status { + display: inline-block; + width: 15px; + height: 20px; + margin-left: 5px; + vertical-align: middle; + text-align: center; +} + .node-diff-node-description { color: $form-text-color; margin-left: 5px; @@ -156,7 +218,13 @@ clear: both; } } - +.node-diff-node-meta { + float: right; + font-size: 0.9em; + color: #999; + margin-top: 7px; + margin-right: 10px; +} .node-diff-count { color: #999} .node-diff-added { color: #009900} diff --git a/editor/templates/index.mst b/editor/templates/index.mst index d8cbb2537..0847b1a84 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -97,9 +97,6 @@
    -
    -
      -
      From a9b17e930ca4007f69ef386ac2cd478ba47637ba Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 7 Dec 2016 13:48:30 +0000 Subject: [PATCH 22/44] Add diff markers to tabs in diff-view --- editor/js/ui/deploy.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/editor/js/ui/deploy.js b/editor/js/ui/deploy.js index c76e5f19c..d19a6e28a 100644 --- a/editor/js/ui/deploy.js +++ b/editor/js/ui/deploy.js @@ -35,8 +35,6 @@ RED.deploy = (function() { $("#btn-deploy-icon").attr("src",deploymentTypes[type].img); } - var currentDiff = null; - /** * options: * type: "default" - Button with drop-down options - no further customisation available From 226ad3fe22b9800aef93cd0e5841fe5e4b2c0a27 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 7 Dec 2016 13:51:20 +0000 Subject: [PATCH 23/44] Add missing diff file --- editor/js/ui/diff.js | 536 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 536 insertions(+) create mode 100644 editor/js/ui/diff.js diff --git a/editor/js/ui/diff.js b/editor/js/ui/diff.js new file mode 100644 index 000000000..14a153c16 --- /dev/null +++ b/editor/js/ui/diff.js @@ -0,0 +1,536 @@ +RED.diff = (function() { + + function init() { + + RED.actions.add("core:show-current-diff",showlocalDiff); + RED.keyboard.add("*","ctrl-shift-l","core:show-current-diff"); + + + var dialog = $('
        ').appendTo(document.body); + + var toolbar = $('
        '+ + ''+ + 'all nodes'+ + 'changed nodes'+ + ''+ + '
        ').prependTo(dialog); + + toolbar.find(".node-diff-filter").click(function(evt) { + evt.preventDefault(); + if (!$(this).hasClass('selected')) { + $(this).siblings().removeClass('selected'); + $(this).addClass('selected'); + } + if ($(this).attr('id') === 'node-diff-filter-all') { + diffList.find('.node-diff-node-unchanged').parent().removeClass('hide'); + diffList.find('.node-diff-tab-unchanged').parent().removeClass('hide'); + } else { + diffList.find('.node-diff-node-unchanged').parent().addClass('hide'); + diffList.find('.node-diff-tab-unchanged').parent().addClass('hide'); + $(".node-diff-tab.node-diff-tab-unchanged").addClass("collapsed"); + } + }) + + $("#node-dialog-view-diff").dialog({ + title: RED._('deploy.confirm.button.review'), + modal: true, + autoOpen: false, + buttons: [ + // { + // text: RED._("deploy.confirm.button.cancel"), + // click: function() { + // $( this ).dialog( "close" ); + // } + // }, + { + text: RED._("common.label.close"), + class: "primary", + click: function() { + $( this ).dialog( "close" ); + } + } + ], + open: function() { + $(this).dialog({width:Math.min($(window).width(),900),height:Math.min($(window).height(),600)}); + } + }); + + var diffList = $("#node-dialog-view-diff-diff").editableList({ + addButton: false, + scrollOnAdd: false, + addItem: function(container,i,object) { + var localDiff = object.diff; + var tab = object.tab.n; + var def = object.def; + var tabDiv = $('
        ',{class:"node-diff-tab"}).appendTo(container); + var titleRow = $('
        ',{class:"node-diff-tab-title"}).appendTo(tabDiv); + if (localDiff.added[tab.id]) { + titleRow.addClass("node-diff-node-added"); + } else if (localDiff.deleted[tab.id]) { + titleRow.addClass("node-diff-node-deleted"); + } + var status = $('').appendTo(titleRow); + + $('').appendTo(titleRow); + createNodeIcon(tab,def).appendTo(titleRow); + var tabForLabel = (object.newTab || object.tab).n; + if (tabForLabel.type === 'tab') { + $('').html(tabForLabel.label||tabForLabel.id).appendTo(titleRow); + } else if (tab.type === 'subflow') { + $('').html((tabForLabel.name||tabForLabel.id)).appendTo(titleRow); + } else { + $('').html("Global configuration nodes").appendTo(titleRow); + } + + if (object.newTab) { + if (localDiff.changed[tab.id]) { + titleRow.addClass("node-diff-node-changed"); + var propTab = $('
        ',{class:"node-diff-node-entry node-diff-node-props"}).appendTo(tabDiv); + var props = createNodePropertiesTable(tab,object.newTab.n,def).appendTo(propTab); + } + } + + var stats = $('',{class:"node-diff-tab-stats"}).appendTo(titleRow); + + var flowStats = { + addedCount:0, + deletedCount:0, + changedCount:0, + conflictedCount:0 + } + var seen = {}; + object.tab.nodes.forEach(function(node) { + seen[node.id] = true; + createNodeDiffRow(node,flowStats,localDiff).appendTo(tabDiv) + }); + if (object.newTab) { + object.newTab.nodes.forEach(function(node) { + if (!seen[node.id]) { + createNodeDiffRow(node,flowStats,localDiff).appendTo(tabDiv) + } + }); + } + titleRow.click(function(evt) { + evt.preventDefault(); + + if (titleRow.parent().find(".node-diff-node-entry:not(.hide)").length > 0) { + titleRow.parent().toggleClass('collapsed'); + } + }) + + var changesCount = flowStats.addedCount+flowStats.deletedCount+flowStats.changedCount+flowStats.conflictedCount; + var tabModified = localDiff.added[tab.id] || localDiff.deleted[tab.id] || localDiff.changed[tab.id]; + if (changesCount === 0) { + tabDiv.addClass("collapsed"); + if (!tabModified) { + tabDiv.parent().addClass("hide"); + tabDiv.addClass("node-diff-tab-unchanged"); + } + } + if (localDiff.deleted[tab.id]) { + $('').appendTo(status); + } else if (localDiff.added[tab.id]) { + $('').appendTo(status); + } else if (localDiff.changed[tab.id]) { + $('').appendTo(status); + } + if (tabDiv.find(".node-diff-node-entry").length === 0) { + tabDiv.addClass("node-diff-tab-empty"); + } + + var statsInfo = ((flowStats.addedCount > 0)?''+flowStats.addedCount+' added ':'')+ + ((flowStats.deletedCount > 0)?''+flowStats.deletedCount+' deleted ':'')+ + ((flowStats.changedCount > 0)?''+flowStats.changedCount+' changed ':'')+ + ((flowStats.conflictedCount > 0)?''+flowStats.conflictedCount+' conflicts':''); + stats.html(statsInfo); + + + + // + // + // + // var node = object.node; + // var realNode = RED.nodes.node(node.id); + // var def = RED.nodes.getType(object.node.type)||{}; + // var l = ""; + // if (def && def.label && realNode) { + // l = def.label; + // try { + // l = (typeof l === "function" ? l.call(realNode) : l); + // } catch(err) { + // console.log("Definition error: "+node.type+".label",err); + // } + // } + // l = l||node.label||node.name||node.id||""; + // console.log(node); + // var div = $('
        ').appendTo(container); + // div.html(l); + } + }); + } + + function formatWireProperty(wires) { + var result = $("
          "); + wires.forEach(function(p,i) { + var port = $("
        1. ").appendTo(result); + if (p && p.length > 0) { + var links = $("
            ").appendTo(port); + p.forEach(function(d) { + var entry = $("
          • ").text(d).appendTo(links); + }) + } else { + port.html('none'); + } + }) + return result; + } + function createNodeIcon(node,def) { + var nodeDiv = $("
            ",{class:"node-diff-node-entry-node"}); + var colour = def.color; + var icon_url = "arrow-in.png"; + if (node.type === 'tab') { + colour = "#C0DEED"; + icon_url = "subflow.png"; + } else if (def.category === 'config') { + icon_url = "cog.png"; + } else if (node.type === 'unknown') { + icon_url = "alert.png"; + } else { + icon_url = def.icon; + } + nodeDiv.css('backgroundColor',colour); + + var iconContainer = $('
            ',{class:"palette_icon_container"}).appendTo(nodeDiv); + $('
            ',{class:"palette_icon",style:"background-image: url(icons/"+icon_url+")"}).appendTo(iconContainer); + + return nodeDiv; + } + function createNodeDiffRow(node,stats,localDiff) { + var realNode = RED.nodes.node(node.id); + var hasChanges = false; + if (localDiff.added[node.id]) { + stats.addedCount++; + } + if (localDiff.deleted[node.id]) { + stats.deletedCount++; + } + if (localDiff.changed[node.id]) { + stats.changedCount++; + hasChanges = true; + } + + var def = RED.nodes.getType(node.type)||{}; + var div = $("
            ",{class:"node-diff-node-entry collapsed"}); + var nodeTitleDiv = $("
            ",{class:"node-diff-node-entry-title"}).appendTo(div); + var status = $('').appendTo(nodeTitleDiv); + var nodeLabel = node.label || node.name || node.id; + + if (hasChanges) { + nodeTitleDiv.addClass("node-diff-node-changed"); + $('').appendTo(status); + var newNode = localDiff.newConfig.all[node.id]; + if (newNode) { + nodeLabel = newNode.label || newNode.name || newNode.id; + nodeTitleDiv.click(function(evt) { + evt.preventDefault(); + $(this).parent().toggleClass('collapsed'); + }) + createNodePropertiesTable(node,newNode,def).appendTo(div); + $('').appendTo(nodeTitleDiv); + } + } else if (localDiff.deleted[node.id]){ + $('').appendTo(nodeTitleDiv); + nodeTitleDiv.addClass("node-diff-node-deleted"); + $('').appendTo(status); + } else if (localDiff.added[node.id]) { + $('').appendTo(nodeTitleDiv); + nodeTitleDiv.addClass("node-diff-node-added") + $('').appendTo(status); + } else { + $('').appendTo(nodeTitleDiv); + nodeTitleDiv.addClass("node-diff-node-unchanged"); + div.addClass("hide"); + } + + createNodeIcon(node,def).appendTo(nodeTitleDiv); + + var contentDiv = $('
            ',{class:"node-diff-node-description"}).appendTo(nodeTitleDiv); + + $('',{class:"node-diff-node-label"}).html(nodeLabel).appendTo(contentDiv); + $('',{class:"node-diff-node-meta"}).html(node.id).appendTo(nodeTitleDiv); + + //$('
            ',{class:"red-ui-search-result-node-type"}).html(node.type).appendTo(contentDiv); + //$('
            ',{class:"red-ui-search-result-node-id"}).html(node.id).appendTo(contentDiv); + + return div; + } + function createNodePropertiesTable(node,newNode,def) { + var nodePropertiesDiv = $("
            ",{class:"node-diff-node-entry-properties"}); + + var nodePropertiesTable = $("
        2. position"+currentPosition+""+newPosition+"
          "+d+''+formattedProperty+''+newFormattedProperty+"
          ").appendTo(nodePropertiesDiv); + var row; + if (node.hasOwnProperty('x')) { + if (newNode.x !== node.x || newNode.y !== node.y) { + var currentPosition = RED.utils.createObjectElement({x:node.x,y:node.y}); + var newPosition = RED.utils.createObjectElement({x:newNode.x,y:newNode.y}); + row = $("").appendTo(nodePropertiesTable); + currentPosition.appendTo(row.children()[1]); + newPosition.appendTo(row.children()[2]); + + } + } + if (node.hasOwnProperty('wires')) { + var localValue = JSON.stringify(node.wires); + var remoteValue = JSON.stringify(newNode.wires); + if (localValue !== remoteValue) { + row = $("").appendTo(nodePropertiesTable); + formatWireProperty(node.wires).appendTo(row.children()[1]); + formatWireProperty(newNode.wires).appendTo(row.children()[2]); + } + + } + var properties = Object.keys(node).filter(function(p) { return p!='z'&&p!='wires'&&p!=='x'&&p!=='y'&&p!=='id'&&p!=='type'&&(!def.defaults||!def.defaults.hasOwnProperty(p))}); + if (def.defaults) { + properties = properties.concat(Object.keys(def.defaults)); + } + properties.forEach(function(d) { + var localValue = JSON.stringify(node[d]); + var remoteValue = JSON.stringify(newNode[d]); + + if (remoteValue !== localValue) { + var formattedProperty = RED.utils.createObjectElement(node[d]); + var newFormattedProperty = RED.utils.createObjectElement(newNode[d]); + var row = $("').appendTo(nodePropertiesTable); + formattedProperty.appendTo(row.children()[1]); + newFormattedProperty.appendTo(row.children()[2]); + } + }) + return nodePropertiesDiv; + } + + + + function showlocalDiff() { + var nns = RED.nodes.createCompleteNodeSet(); + var originalFlow = RED.nodes.originalFlow(); + var diff = generateDiff(originalFlow,nns); + showDiff(diff); + } + + + function parseNodes(nodeList) { + var tabOrder = []; + var tabs = {}; + var subflows = {}; + var globals = []; + var all = {}; + + nodeList.forEach(function(node) { + all[node.id] = node; + if (node.type === 'tab') { + tabOrder.push(node.id); + tabs[node.id] = {n:node,nodes:[]}; + } else if (node.type === 'subflow') { + subflows[node.id] = {n:node,nodes:[]}; + } + }); + + nodeList.forEach(function(node) { + if (node.type !== 'tab' && node.type !== 'subflow') { + if (tabs[node.z]) { + tabs[node.z].nodes.push(node); + } else if (subflows[node.z]) { + subflows[node.z].nodes.push(node); + } else { + globals.push(node); + } + } + }); + + return { + all: all, + tabOrder: tabOrder, + tabs: tabs, + subflows: subflows, + globals: globals + } + } + + function generateDiff(currentNodes,newNodes) { + var currentConfig = parseNodes(currentNodes); + var newConfig = parseNodes(newNodes); + var pending = RED.nodes.pending(); + var added = {}; + var deleted = {}; + var changed = {}; + var conflicted = {}; + + Object.keys(currentConfig.all).forEach(function(id) { + var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); + if (!newConfig.all.hasOwnProperty(id)) { + if (!pending.added.hasOwnProperty(id)) { + deleted[id] = true; + conflicted[id] = node&&node.changed; + } + } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) { + changed[id] = true; + conflicted[id] = node.changed; + } + }); + Object.keys(newConfig.all).forEach(function(id) { + if (!currentConfig.all.hasOwnProperty(id) && !pending.deleted.hasOwnProperty(id)) { + added[id] = true; + } + }); + + // console.log("Added",added); + // console.log("Deleted",deleted); + // console.log("Changed",changed); + // console.log("Conflicted",conflicted); + // + // var formatString = function(id) { + // return conflicted[id]?"!":(added[id]?"+":(deleted[id]?"-":(changed[id]?"~":" "))); + // } + // newConfig.tabOrder.forEach(function(tabId) { + // var tab = newConfig.tabs[tabId]; + // console.log(formatString(tabId),"Flow:",tab.n.label, "("+tab.n.id+")"); + // tab.nodes.forEach(function(node) { + // console.log(" ",formatString(node.id),node.type,node.name || node.id); + // }) + // if (currentConfig.tabs[tabId]) { + // currentConfig.tabs[tabId].nodes.forEach(function(node) { + // if (deleted[node.id]) { + // console.log(" ",formatString(node.id),node.type,node.name || node.id); + // } + // }) + // } + // }); + // currentConfig.tabOrder.forEach(function(tabId) { + // if (deleted[tabId]) { + // var tab = currentConfig.tabs[tabId]; + // console.log(formatString(tabId),"Flow:",tab.n.label, "("+tab.n.id+")"); + // } + // }); + + return { + currentConfig: currentConfig, + newConfig: newConfig, + added: added, + deleted: deleted, + changed: changed, + conflicted: conflicted + } + } + + function formatNodeProperty(prop) { + var formattedProperty = prop; + if (formattedProperty === null) { + formattedProperty = 'null'; + } else if (formattedProperty === undefined) { + formattedProperty = 'undefined'; + } else if (typeof formattedProperty === 'object') { + formattedProperty = JSON.stringify(formattedProperty); + } + if (/\n/.test(formattedProperty)) { + formattedProperty = "
          "+formattedProperty+"
          " + } + return formattedProperty; + } + + function showDiff(localDiff) { + var el; + var list = $("#node-dialog-view-diff-diff"); + list.editableList('empty'); + + var currentConfig = localDiff.currentConfig; + var newConfig = localDiff.newConfig; + + list.editableList('addItem',{ + diff: localDiff, + def: { + category: 'config', + color: '#f0f0f0' + }, + tab: { + n: {}, + nodes: currentConfig.globals + }, + newTab: { + n: {}, + nodes: newConfig.globals + } + }); + + var seenTabs = {}; + + currentConfig.tabOrder.forEach(function(tabId) { + var tab = currentConfig.tabs[tabId]; + var el = { + diff: localDiff, + def: {}, + tab:tab + }; + if (newConfig.tabs.hasOwnProperty(tabId)) { + el.newTab = newConfig.tabs[tabId]; + } + seenTabs[tabId] = true; + list.editableList('addItem',el) + }); + newConfig.tabOrder.forEach(function(tabId) { + if (!seenTabs[tabId]) { + var tab = newConfig.tabs[tabId]; + var el = { + diff: localDiff, + def: {}, + tab:tab + }; + list.editableList('addItem',el) + } + }) + var subflowId; + for (subflowId in currentConfig.subflows) { + if (currentConfig.subflows.hasOwnProperty(subflowId)) { + seenTabs[subflowId] = true; + el = { + diff: localDiff, + def: { + defaults:{}, + icon:"subflow.png", + category: "subflows", + color: "#da9" + }, + tab:currentConfig.subflows[subflowId] + } + if (newConfig.subflows.hasOwnProperty(subflowId)) { + el.newTab = newConfig.subflows[subflowId]; + } + list.editableList('addItem',el) + } + } + for (subflowId in newConfig.subflows) { + if (newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { + el = { + diff: localDiff, + def: { + defaults:{}, + icon:"subflow.png", + category: "subflows", + color: "#da9" + }, + tab:newConfig.subflows[subflowId] + } + list.editableList('addItem',el) + } + } + + $("#node-diff-filter-changed").addClass("selected"); + $("#node-diff-filter-all").removeClass("selected"); + + $("#node-dialog-view-diff").dialog("open"); + } + + + return { + init: init + } +})(); From f6820ec6158569f10d08c068a5d3c44a66738263 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Wed, 7 Dec 2016 23:43:41 +0000 Subject: [PATCH 24/44] Bump a load of packages (for 0.16 branch only) Ready to drop node 0.10 - update ws, bcrypt, drop serial port node --- package.json | 49 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 5c3711df5..412634c0e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "mqtt": "1.14.1", "mustache": "2.2.1", "nopt": "3.0.6", - "oauth2orize":"1.5.0", + "oauth2orize":"1.5.1", "on-headers":"1.0.1", "passport":"0.3.2", "passport-http-bearer":"1.0.1", @@ -54,9 +54,9 @@ "raw-body":"2.1.7", "semver": "5.3.0", "sentiment":"1.0.6", - "uglify-js":"2.7.3", + "uglify-js":"2.7.5", "when": "3.7.7", - "ws": "0.8.1", + "ws": "1.1.1", "xml2js":"0.4.17", "node-red-node-feedparser":"0.1.*", "node-red-node-email":"0.1.*", @@ -64,31 +64,30 @@ "node-red-node-rbe":"0.1.*" }, "optionalDependencies": { - "node-red-node-serialport":"0.4.*", - "bcrypt":"0.8.7" + "bcrypt":"~1.0.0" }, "devDependencies": { - "grunt": "1.0.1", - "grunt-chmod": "1.1.1", - "grunt-cli": "1.2.0", - "grunt-concurrent":"2.3.1", - "grunt-contrib-clean":"1.0.0", - "grunt-contrib-compress": "1.3.0", - "grunt-contrib-concat":"1.0.1", - "grunt-contrib-copy": "1.0.0", - "grunt-contrib-jshint": "1.0.0", - "grunt-contrib-uglify": "2.0.0", - "grunt-contrib-watch":"1.0.0", - "grunt-jsonlint":"1.1.0", - "grunt-nodemon":"0.4.2", - "grunt-sass":"1.2.1", - "grunt-simple-mocha": "0.4.1", - "mocha": "3.1.1", - "should": "8.4.0", - "sinon": "1.17.6", - "supertest": "2.0.0" + "grunt": "~1.0.1", + "grunt-chmod": "~1.1.1", + "grunt-cli": "~1.2.0", + "grunt-concurrent":"~2.3.1", + "grunt-contrib-clean":"~1.0.0", + "grunt-contrib-compress": "~1.3.0", + "grunt-contrib-concat":"~1.0.1", + "grunt-contrib-copy": "~1.0.0", + "grunt-contrib-jshint": "~1.0.0", + "grunt-contrib-uglify": "~2.0.0", + "grunt-contrib-watch":"~1.0.0", + "grunt-jsonlint":"~1.1.0", + "grunt-nodemon":"~0.4.2", + "grunt-sass":"~1.2.1", + "grunt-simple-mocha": "~0.4.1", + "mocha": "~3.1.2", + "should": "^8.4.0", + "sinon": "^1.17.6", + "supertest": "^2.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=4" } } From fc263718a188522f4d3e28ddae2e942233dfa98a Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Fri, 16 Dec 2016 21:54:24 +0000 Subject: [PATCH 25/44] Add res.responseUrl to httprequest node response --- nodes/core/io/21-httprequest.js | 1 + 1 file changed, 1 insertion(+) diff --git a/nodes/core/io/21-httprequest.js b/nodes/core/io/21-httprequest.js index 3fa8097d0..603342497 100644 --- a/nodes/core/io/21-httprequest.js +++ b/nodes/core/io/21-httprequest.js @@ -159,6 +159,7 @@ module.exports = function(RED) { (node.ret === "bin") ? res.setEncoding('binary') : res.setEncoding('utf8'); msg.statusCode = res.statusCode; msg.headers = res.headers; + msg.responseUrl = res.responseUrl; msg.payload = ""; // msg.url = url; // revert when warning above finally removed res.on('data',function(chunk) { From 8423e2d245037143f7aba73fddddda503dcfee27 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Fri, 16 Dec 2016 22:03:00 +0000 Subject: [PATCH 26/44] add info for httprequest responseUrl property --- nodes/core/io/21-httprequest.html | 1 + 1 file changed, 1 insertion(+) diff --git a/nodes/core/io/21-httprequest.html b/nodes/core/io/21-httprequest.html index 19d5f5a2a..891f4d0d6 100644 --- a/nodes/core/io/21-httprequest.html +++ b/nodes/core/io/21-httprequest.html @@ -90,6 +90,7 @@
        3. payload is the body of the response
        4. statusCode is the status code of the response, or the error code if the request could not be completed
        5. headers is an object containing the response headers
        6. +
        7. responseUrl is the url of the server that responds
        8. Note: If you need to configure a proxy please add http_proxy=... to your environment variables and restart Node-RED.

          From bba3ca8cc0d51446322a98a581821fa0d81c3e75 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 20 Dec 2016 22:46:56 +0000 Subject: [PATCH 27/44] Avoid misinterpreting valid objects as encoded arrays in debug --- editor/js/ui/utils.js | 2 +- nodes/core/core/58-debug.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/editor/js/ui/utils.js b/editor/js/ui/utils.js index 8a232f644..d81924b81 100644 --- a/editor/js/ui/utils.js +++ b/editor/js/ui/utils.js @@ -89,7 +89,7 @@ RED.utils = (function() { var isArray = Array.isArray(obj); var isArrayObject = false; - if (obj && typeof obj === 'object' && obj.hasOwnProperty('type') && obj.hasOwnProperty('data')) { + if (obj && typeof obj === 'object' && obj.hasOwnProperty('type') && obj.hasOwnProperty('data') && ((obj.__encoded__ && obj.type === 'array') || obj.type === 'Buffer')) { isArray = true; isArrayObject = true; } diff --git a/nodes/core/core/58-debug.js b/nodes/core/core/58-debug.js index e16f710b5..8b1c5a2bb 100644 --- a/nodes/core/core/58-debug.js +++ b/nodes/core/core/58-debug.js @@ -118,6 +118,7 @@ module.exports = function(RED) { } if (util.isArray(value) && value.length > debuglength) { value = { + __encoded__: true, type: "array", data: value.slice(0,debuglength), length: value.length From e7cc42a927bc1b77ef6f58c322b54cfdc9f4aa0c Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 20 Dec 2016 23:16:11 +0000 Subject: [PATCH 28/44] Use json-stringify-safe to detect circular references in debug msgs --- nodes/core/core/58-debug.js | 8 +++----- package.json | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/nodes/core/core/58-debug.js b/nodes/core/core/58-debug.js index 8b1c5a2bb..d0b93e95a 100644 --- a/nodes/core/core/58-debug.js +++ b/nodes/core/core/58-debug.js @@ -19,6 +19,7 @@ module.exports = function(RED) { var util = require("util"); var events = require("events"); var path = require("path"); + var safeJSONStringify = require("json-stringify-safe"); var debuglength = RED.settings.debugMaxLength||1000; var useColors = false; // util.inspect.styles.boolean = "red"; @@ -87,6 +88,7 @@ module.exports = function(RED) { } } else if (msg.msg && typeof msg.msg === 'object') { var seen = []; + var seenAts = []; try { msg.format = msg.msg.constructor.name || "Object"; } catch(err) { @@ -106,7 +108,7 @@ module.exports = function(RED) { } } if (isArray || (msg.format === "Object")) { - msg.msg = JSON.stringify(msg.msg, function(key, value) { + msg.msg = safeJSONStringify(msg.msg, function(key, value) { if (key[0] === '_' && key !== "_msgid") { return undefined; } @@ -124,10 +126,6 @@ module.exports = function(RED) { length: value.length } } - if (typeof value === 'object' && value !== null) { - if (seen.indexOf(value) !== -1) { return "[circular]"; } - seen.push(value); - } if (typeof value === 'string') { if (value.length > debuglength) { return value.substring(0,debuglength)+"..."; diff --git a/package.json b/package.json index 412634c0e..dc4488f25 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "i18next":"1.10.6", "is-utf8":"0.2.1", "js-yaml": "3.6.1", + "json-stringify-safe":"5.0.1", "jsonata":"1.0.10", "media-typer": "0.3.0", "mqtt": "1.14.1", From b6b65b6bf767fb82a10b72d55b452fea43d743bb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 20 Dec 2016 23:21:25 +0000 Subject: [PATCH 29/44] Update debug node test for circular references --- test/nodes/core/core/58-debug_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nodes/core/core/58-debug_spec.js b/test/nodes/core/core/58-debug_spec.js index 92fcef056..5ab0f0b57 100644 --- a/test/nodes/core/core/58-debug_spec.js +++ b/test/nodes/core/core/58-debug_spec.js @@ -214,7 +214,7 @@ describe('debug node', function() { topic:"debug", data:{ id:"n1", - msg:'{\n "name": "bar",\n "o": "[circular]"\n}', + msg:'{\n "name": "bar",\n "o": "[Circular ~]"\n}', property:"payload",format:"Object" } }); From 4cbe2648690d6cdc8d1116150ebbfe184669e63d Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Thu, 22 Dec 2016 13:17:08 +0000 Subject: [PATCH 30/44] Change file node to use node 4 syntax (drops support for 0.8) --- nodes/core/storage/50-file.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nodes/core/storage/50-file.js b/nodes/core/storage/50-file.js index 3eed20736..e65b12b93 100644 --- a/nodes/core/storage/50-file.js +++ b/nodes/core/storage/50-file.js @@ -45,8 +45,8 @@ module.exports = function(RED) { data = new Buffer(data); if (this.overwriteFile === "true") { // using "binary" not {encoding:"binary"} to be 0.8 compatible for a while - fs.writeFile(filename, data, "binary", function (err) { - //fs.writeFile(filename, data, {encoding:"binary"}, function (err) { + //fs.writeFile(filename, data, "binary", function (err) { + fs.writeFile(filename, data, {encoding:"binary"}, function (err) { if (err) { if ((err.code === "ENOENT") && node.createDir) { fs.ensureFile(filename, function (err) { @@ -71,8 +71,8 @@ module.exports = function(RED) { } else { // using "binary" not {encoding:"binary"} to be 0.8 compatible for a while longer - fs.appendFile(filename, data, "binary", function (err) { - //fs.appendFile(filename, data, {encoding:"binary"}, function (err) { + //fs.appendFile(filename, data, "binary", function (err) { + fs.appendFile(filename, data, {encoding:"binary"}, function (err) { if (err) { if ((err.code === "ENOENT") && node.createDir) { fs.ensureFile(filename, function (err) { From 4ca3df77b33cae4c662810e1f4274efb6acef7dc Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Thu, 22 Dec 2016 13:27:27 +0000 Subject: [PATCH 31/44] =?UTF-8?q?Add=20=E2=87=B6=20to=20debug=20node=20to?= =?UTF-8?q?=20indicate=20debugging=20also=20to=20console.log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/core/core/58-debug.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nodes/core/core/58-debug.html b/nodes/core/core/58-debug.html index ada8a1c12..6d00fe837 100644 --- a/nodes/core/core/58-debug.html +++ b/nodes/core/core/58-debug.html @@ -41,7 +41,7 @@

          The button to the right of the node will toggle its output on and off so you can de-clutter the debug window.

          If the payload is an object or buffer it will be stringified first for display and indicate that by saying "(Object)" or "(Buffer)".

          Selecting any particular message will highlight (in red) the debug node that reported it. This is useful if you wire up multiple debug nodes.

          -

          Optionally can show the complete msg object, and send messages to the console log.

          +

          Optionally can show the complete msg object, and send messages to the console log (⇶).

          In addition any calls to node.warn or node.error will appear here.

          @@ -58,10 +58,12 @@ complete: {value:"false", required:true} }, label: function() { + var suffix = ""; + if (this.console === true || this.console === "true") { suffix = " ⇶"; } if (this.complete === true || this.complete === "true") { - return this.name||"msg"; + return (this.name||"msg") + suffix; } else { - return this.name || "msg." + ((!this.complete || this.complete === "false") ? "payload" : this.complete); + return (this.name || "msg." + ((!this.complete || this.complete === "false") ? "payload" : this.complete)) + suffix; } }, labelStyle: function() { From 28a65923b66b80df896c572a6d8ac865e03c3b23 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Thu, 22 Dec 2016 13:36:06 +0000 Subject: [PATCH 32/44] bump various package versions (not touching mqtt and other major version) --- package.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index dc4488f25..5ff524ec9 100644 --- a/package.json +++ b/package.json @@ -26,35 +26,35 @@ "editor", "messaging", "iot", "ibm", "flow" ], "dependencies": { - "basic-auth": "1.0.4", - "bcryptjs": "2.3.0", + "basic-auth": "1.1.0", + "bcryptjs": "2.4.0", "body-parser": "1.15.2", "cheerio":"0.22.0", - "clone": "2.0.0", + "clone": "2.1.0", "cookie-parser": "1.4.3", "cors":"2.8.1", - "cron":"1.1.1", + "cron":"1.2.1", "express": "4.14.0", - "follow-redirects":"0.2.0", - "fs-extra": "0.30.0", + "follow-redirects":"1.2.1", + "fs-extra": "1.0.0", "fs.notify":"0.0.4", "i18next":"1.10.6", "is-utf8":"0.2.1", - "js-yaml": "3.6.1", + "js-yaml": "3.7.0", "json-stringify-safe":"5.0.1", "jsonata":"1.0.10", "media-typer": "0.3.0", - "mqtt": "1.14.1", - "mustache": "2.2.1", + "mqtt": "1.*", + "mustache": "2.3.0", "nopt": "3.0.6", - "oauth2orize":"1.5.1", + "oauth2orize":"1.6.0", "on-headers":"1.0.1", "passport":"0.3.2", "passport-http-bearer":"1.0.1", "passport-oauth2-client-password":"0.1.2", "raw-body":"2.1.7", "semver": "5.3.0", - "sentiment":"1.0.6", + "sentiment":"2.1.0", "uglify-js":"2.7.5", "when": "3.7.7", "ws": "1.1.1", @@ -65,7 +65,7 @@ "node-red-node-rbe":"0.1.*" }, "optionalDependencies": { - "bcrypt":"~1.0.0" + "bcrypt":"~1.0.1" }, "devDependencies": { "grunt": "~1.0.1", @@ -76,14 +76,14 @@ "grunt-contrib-compress": "~1.3.0", "grunt-contrib-concat":"~1.0.1", "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-jshint": "~1.0.0", + "grunt-contrib-jshint": "~1.1.0", "grunt-contrib-uglify": "~2.0.0", "grunt-contrib-watch":"~1.0.0", "grunt-jsonlint":"~1.1.0", "grunt-nodemon":"~0.4.2", "grunt-sass":"~1.2.1", "grunt-simple-mocha": "~0.4.1", - "mocha": "~3.1.2", + "mocha": "~3.2.0", "should": "^8.4.0", "sinon": "^1.17.6", "supertest": "^2.0.0" From f143a6ba08b68345220ff3c65f831209850a0087 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 24 Dec 2016 00:31:23 +1300 Subject: [PATCH 33/44] update welcome message to use logger so it can be turned off/on if required (#1083) --- red/runtime/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/red/runtime/index.js b/red/runtime/index.js index f0ea0fab3..838cafaff 100644 --- a/red/runtime/index.js +++ b/red/runtime/index.js @@ -92,7 +92,7 @@ function start() { reportMetrics(); }, settings.runtimeMetricInterval||15000); } - console.log("\n\n"+log._("runtime.welcome")+"\n===================\n"); + log.info("\n\n"+log._("runtime.welcome")+"\n===================\n"); if (settings.version) { log.info(log._("runtime.version",{component:"Node-RED",version:"v"+settings.version})); } From d3dfbc30349d00f021ef961ed9911e7c255374c4 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 15 Dec 2016 20:41:53 +0000 Subject: [PATCH 34/44] Add proper three-way diff view --- editor/js/ui/diff.js | 872 ++++++++++++++++++++++++++++++++---------- editor/sass/diff.scss | 242 ++++++++++-- 2 files changed, 894 insertions(+), 220 deletions(-) diff --git a/editor/js/ui/diff.js b/editor/js/ui/diff.js index 14a153c16..6ccd05790 100644 --- a/editor/js/ui/diff.js +++ b/editor/js/ui/diff.js @@ -2,11 +2,14 @@ RED.diff = (function() { function init() { - RED.actions.add("core:show-current-diff",showlocalDiff); + RED.actions.add("core:show-current-diff",showLocalDiff); + RED.actions.add("core:show-remote-diff",showRemoteDiff); + RED.keyboard.add("*","ctrl-shift-l","core:show-current-diff"); + RED.keyboard.add("*","ctrl-shift-r","core:show-remote-diff"); - var dialog = $('
            ').appendTo(document.body); + var dialog = $('
              ').appendTo(document.body); var toolbar = $('
              '+ ''+ @@ -60,128 +63,268 @@ RED.diff = (function() { scrollOnAdd: false, addItem: function(container,i,object) { var localDiff = object.diff; + var remoteDiff = object.remoteDiff; var tab = object.tab.n; var def = object.def; var tabDiv = $('
              ',{class:"node-diff-tab"}).appendTo(container); var titleRow = $('
              ',{class:"node-diff-tab-title"}).appendTo(tabDiv); - if (localDiff.added[tab.id]) { - titleRow.addClass("node-diff-node-added"); - } else if (localDiff.deleted[tab.id]) { - titleRow.addClass("node-diff-node-deleted"); + var nodesDiv = $('
              ').appendTo(tabDiv); + var originalCell = $('
              ',{class:"node-diff-node-entry-cell"}).appendTo(titleRow); + var localCell = $('
              ',{class:"node-diff-node-entry-cell"}).appendTo(titleRow); + var remoteCell; + if (remoteDiff) { + remoteCell = $('
              ',{class:"node-diff-node-entry-cell"}).appendTo(titleRow); } - var status = $('').appendTo(titleRow); + // if (localDiff.added[tab.id]) { + // titleRow.addClass("node-diff-node-added"); + // } else if (localDiff.deleted[tab.id]) { + // titleRow.addClass("node-diff-node-deleted"); + // } + // var status = $('').appendTo(originalCell); - $('').appendTo(titleRow); - createNodeIcon(tab,def).appendTo(titleRow); + // if (!object.newTab && object.remoteTab) { + // $('').appendTo(remoteCell); + // //} else if (object.newTab && (remoteDiff && !object.remoteTab)) { + // } else if (localDiff.added[tab.id]) { + // $('').appendTo(localCell); + // } + $('').appendTo(originalCell); + createNodeIcon(tab,def).appendTo(originalCell); var tabForLabel = (object.newTab || object.tab).n; + var titleSpan = $('',{class:"node-diff-tab-title-meta"}).appendTo(originalCell); if (tabForLabel.type === 'tab') { - $('').html(tabForLabel.label||tabForLabel.id).appendTo(titleRow); + titleSpan.html(tabForLabel.label||tabForLabel.id); } else if (tab.type === 'subflow') { - $('').html((tabForLabel.name||tabForLabel.id)).appendTo(titleRow); + titleSpan.html((tabForLabel.name||tabForLabel.id)); } else { - $('').html("Global configuration nodes").appendTo(titleRow); + titleSpan.html("Global configuration nodes"); } + var flowStats = { + local: { + addedCount:0, + deletedCount:0, + changedCount:0, + unchangedCount: 0 + }, + remote: { + addedCount:0, + deletedCount:0, + changedCount:0, + unchangedCount: 0 + }, + conflicts: 0 + } + if (object.newTab || object.remoteTab) { + var localTabNode = { + node: localDiff.newConfig.all[tab.id], + all: localDiff.newConfig.all, + diff: localDiff + } + var remoteTabNode; + if (remoteDiff) { + remoteTabNode = { + node:remoteDiff.newConfig.all[tab.id]||null, + all: remoteDiff.newConfig.all, + diff: remoteDiff + } + } + if (tab.type !== undefined) { + var div = $("
              ",{class:"node-diff-node-entry node-diff-node-props collapsed"}).appendTo(nodesDiv); + var row = $("
              ",{class:"node-diff-node-entry-header"}).appendTo(div); + var originalNodeDiv = $("
              ",{class:"node-diff-node-entry-cell"}).appendTo(row); + var localNodeDiv = $("
              ",{class:"node-diff-node-entry-cell"}).appendTo(row); + var localChanged = false; + var remoteChanged = false; - if (object.newTab) { - if (localDiff.changed[tab.id]) { - titleRow.addClass("node-diff-node-changed"); - var propTab = $('
              ',{class:"node-diff-node-entry node-diff-node-props"}).appendTo(tabDiv); - var props = createNodePropertiesTable(tab,object.newTab.n,def).appendTo(propTab); + if (!localDiff.newConfig.all[tab.id]) { + localNodeDiv.addClass("node-diff-empty"); + } else if (localDiff.added[tab.id]) { + localNodeDiv.addClass("node-diff-node-added"); + $(' added').appendTo(localNodeDiv); + } else if (localDiff.changed[tab.id]) { + localNodeDiv.addClass("node-diff-node-changed"); + $(' changed').appendTo(localNodeDiv); + } else { + localNodeDiv.addClass("node-diff-node-unchanged"); + $(' unchanged').appendTo(localNodeDiv); + } + + + var remoteNodeDiv; + if (remoteDiff) { + remoteNodeDiv = $("
              ",{class:"node-diff-node-entry-cell"}).appendTo(row); + if (!remoteDiff.newConfig.all[tab.id]) { + remoteNodeDiv.addClass("node-diff-empty"); + } else if (remoteDiff.added[tab.id]) { + remoteNodeDiv.addClass("node-diff-node-added"); + $(' added').appendTo(remoteNodeDiv); + } else if (remoteDiff.changed[tab.id]) { + remoteNodeDiv.addClass("node-diff-node-changed"); + $(' changed').appendTo(remoteNodeDiv); + } else { + remoteNodeDiv.addClass("node-diff-node-unchanged"); + $(' unchanged').appendTo(remoteNodeDiv); + } + } + $('').appendTo(originalNodeDiv); + $('').html("Flow Properties").appendTo(originalNodeDiv); + row.click(function(evt) { + evt.preventDefault(); + $(this).parent().toggleClass('collapsed'); + }); + createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,flowStats).appendTo(div); } } - var stats = $('',{class:"node-diff-tab-stats"}).appendTo(titleRow); + // var stats = $('',{class:"node-diff-tab-stats"}).appendTo(titleRow); + - var flowStats = { - addedCount:0, - deletedCount:0, - changedCount:0, - conflictedCount:0 - } var seen = {}; object.tab.nodes.forEach(function(node) { seen[node.id] = true; - createNodeDiffRow(node,flowStats,localDiff).appendTo(tabDiv) + createNodeDiffRow(node,flowStats,localDiff,remoteDiff).appendTo(nodesDiv) }); if (object.newTab) { object.newTab.nodes.forEach(function(node) { if (!seen[node.id]) { - createNodeDiffRow(node,flowStats,localDiff).appendTo(tabDiv) + seen[node.id] = true; + createNodeDiffRow(node,flowStats,localDiff,remoteDiff).appendTo(nodesDiv) + } + }); + } + if (object.remoteTab) { + object.remoteTab.nodes.forEach(function(node) { + if (!seen[node.id]) { + createNodeDiffRow(node,flowStats,localDiff,remoteDiff).appendTo(nodesDiv) } }); } titleRow.click(function(evt) { evt.preventDefault(); - if (titleRow.parent().find(".node-diff-node-entry:not(.hide)").length > 0) { - titleRow.parent().toggleClass('collapsed'); + // if (titleRow.parent().find(".node-diff-node-entry:not(.hide)").length > 0) { + titleRow.parent().toggleClass('collapsed'); + if ($(this).parent().hasClass('collapsed')) { + $(this).parent().find('.node-diff-node-entry').addClass('collapsed'); + $(this).parent().find('.debug-message-element').addClass('collapsed'); } + // } }) - var changesCount = flowStats.addedCount+flowStats.deletedCount+flowStats.changedCount+flowStats.conflictedCount; - var tabModified = localDiff.added[tab.id] || localDiff.deleted[tab.id] || localDiff.changed[tab.id]; - if (changesCount === 0) { - tabDiv.addClass("collapsed"); - if (!tabModified) { - tabDiv.parent().addClass("hide"); - tabDiv.addClass("node-diff-tab-unchanged"); - } - } + var localChangesCount = flowStats.local.addedCount+flowStats.local.deletedCount+flowStats.local.changedCount; + var remoteChangesCount = flowStats.remote.addedCount+flowStats.remote.deletedCount+flowStats.remote.changedCount; + if (localDiff.deleted[tab.id]) { - $('').appendTo(status); - } else if (localDiff.added[tab.id]) { - $('').appendTo(status); - } else if (localDiff.changed[tab.id]) { - $('').appendTo(status); + $(' flow deleted').appendTo(localCell); + } else if (object.newTab) { + if (localDiff.added[tab.id]) { + $(' flow added').appendTo(localCell); + } else { + if (tab.id) { + if (localDiff.changed[tab.id]) { + localChangesCount++; + flowStats.local.changedCount++; + } else { + flowStats.local.unchangedCount++; + } + } + var localStats = $('',{class:"node-diff-tab-stats"}).appendTo(localCell); + if (flowStats.conflicts > 0) { + $(' '+flowStats.conflicts+'').appendTo(localStats); + } + if (flowStats.local.unchangedCount > 0) { + $(' '+flowStats.local.unchangedCount+'').appendTo(localStats); + } + if (flowStats.local.addedCount > 0) { + $(' '+flowStats.local.addedCount+'').appendTo(localStats); + } + if (flowStats.local.changedCount > 0) { + $(' '+flowStats.local.changedCount+'').appendTo(localStats); + } + if (flowStats.local.deletedCount > 0) { + $(' '+flowStats.local.deletedCount+'').appendTo(localStats); + } + } + } else { + localCell.addClass("node-diff-empty"); + } + + if (remoteDiff) { + if (remoteDiff.deleted[tab.id]) { + $(' flow deleted').appendTo(remoteCell); + } else if (object.remoteTab) { + if (remoteDiff.added[tab.id]) { + $(' flow added').appendTo(remoteCell); + } else { + if (tab.id) { + if (remoteDiff.changed[tab.id]) { + remoteChangesCount++; + flowStats.remote.changedCount++; + } else { + flowStats.remote.unchangedCount++; + } + } + var remoteStats = $('',{class:"node-diff-tab-stats"}).appendTo(remoteCell); + if (flowStats.conflicts > 0) { + $(' '+flowStats.conflicts+'').appendTo(remoteStats); + } + if (flowStats.remote.unchangedCount > 0) { + $(' '+flowStats.remote.unchangedCount+'').appendTo(remoteStats); + } + if (flowStats.remote.addedCount > 0) { + $(' '+flowStats.remote.addedCount+'').appendTo(remoteStats); + } + if (flowStats.remote.changedCount > 0) { + $(' '+flowStats.remote.changedCount+'').appendTo(remoteStats); + } + if (flowStats.remote.deletedCount > 0) { + $(' '+flowStats.remote.deletedCount+'').appendTo(remoteStats); + } + } + } else { + remoteCell.addClass("node-diff-empty"); + } } if (tabDiv.find(".node-diff-node-entry").length === 0) { tabDiv.addClass("node-diff-tab-empty"); } - - var statsInfo = ((flowStats.addedCount > 0)?''+flowStats.addedCount+' added ':'')+ - ((flowStats.deletedCount > 0)?''+flowStats.deletedCount+' deleted ':'')+ - ((flowStats.changedCount > 0)?''+flowStats.changedCount+' changed ':'')+ - ((flowStats.conflictedCount > 0)?''+flowStats.conflictedCount+' conflicts':''); - stats.html(statsInfo); - - - - // - // - // - // var node = object.node; - // var realNode = RED.nodes.node(node.id); - // var def = RED.nodes.getType(object.node.type)||{}; - // var l = ""; - // if (def && def.label && realNode) { - // l = def.label; - // try { - // l = (typeof l === "function" ? l.call(realNode) : l); - // } catch(err) { - // console.log("Definition error: "+node.type+".label",err); - // } - // } - // l = l||node.label||node.name||node.id||""; - // console.log(node); - // var div = $('
              ').appendTo(container); - // div.html(l); + // var statsInfo = ((flowStats.addedCount > 0)?''+flowStats.addedCount+' added ':'')+ + // ((flowStats.deletedCount > 0)?''+flowStats.deletedCount+' deleted ':'')+ + // ((flowStats.changedCount > 0)?''+flowStats.changedCount+' changed ':''); + // stats.html(statsInfo); } }); } - function formatWireProperty(wires) { - var result = $("
                "); + function formatWireProperty(wires,allNodes) { + var result = $("
                ",{class:"node-diff-property-wires"}) + var list = $("
                  "); + var c = 0; wires.forEach(function(p,i) { - var port = $("
                1. ").appendTo(result); + var port = $("
                2. ").appendTo(list); if (p && p.length > 0) { + $("").html(i+1).appendTo(port); var links = $("
                    ").appendTo(port); p.forEach(function(d) { - var entry = $("
                  • ").text(d).appendTo(links); + c++; + var entry = $("
                  • ").appendTo(links); + var node = allNodes[d]; + if (node) { + var def = RED.nodes.getType(node.type)||{}; + createNode(node,def).appendTo(entry); + } else { + entry.html(d); + } }) } else { port.html('none'); } }) + if (c === 0) { + result.html("none"); + } else { + list.appendTo(result); + } return result; } function createNodeIcon(node,def) { @@ -205,119 +348,434 @@ RED.diff = (function() { return nodeDiv; } - function createNodeDiffRow(node,stats,localDiff) { - var realNode = RED.nodes.node(node.id); - var hasChanges = false; + function createNode(node,def) { + var nodeTitleDiv = $("
                    ",{class:"node-diff-node-entry-title"}) + createNodeIcon(node,def).appendTo(nodeTitleDiv); + var contentDiv = $('
                    ',{class:"node-diff-node-description"}).appendTo(nodeTitleDiv); + var nodeLabel = node.label || node.name || node.id; + $('',{class:"node-diff-node-label"}).html(nodeLabel).appendTo(contentDiv); + return nodeTitleDiv; + } + function createNodeDiffRow(node,stats,localDiff,remoteDiff) { + var hasChanges = false; // exists in original and local/remote but with changes + var unChanged = true; // existing in original,local,remote unchanged + var conflicted = false; + if (localDiff.added[node.id]) { - stats.addedCount++; + stats.local.addedCount++; + unChanged = false; + } + if (remoteDiff && remoteDiff.added[node.id]) { + stats.remote.addedCount++; + unChanged = false; } if (localDiff.deleted[node.id]) { - stats.deletedCount++; + stats.local.deletedCount++; + unChanged = false; + } + if (remoteDiff && remoteDiff.deleted[node.id]) { + stats.remote.deletedCount++; + unChanged = false; } if (localDiff.changed[node.id]) { - stats.changedCount++; + stats.local.changedCount++; hasChanges = true; + unChanged = false; + } + if (remoteDiff && remoteDiff.changed[node.id]) { + stats.remote.changedCount++; + hasChanges = true; + unChanged = false; } - var def = RED.nodes.getType(node.type)||{}; - var div = $("
                    ",{class:"node-diff-node-entry collapsed"}); - var nodeTitleDiv = $("
                    ",{class:"node-diff-node-entry-title"}).appendTo(div); - var status = $('').appendTo(nodeTitleDiv); - var nodeLabel = node.label || node.name || node.id; - - if (hasChanges) { - nodeTitleDiv.addClass("node-diff-node-changed"); - $('').appendTo(status); - var newNode = localDiff.newConfig.all[node.id]; - if (newNode) { - nodeLabel = newNode.label || newNode.name || newNode.id; - nodeTitleDiv.click(function(evt) { - evt.preventDefault(); - $(this).parent().toggleClass('collapsed'); - }) - createNodePropertiesTable(node,newNode,def).appendTo(div); - $('').appendTo(nodeTitleDiv); + var def = RED.nodes.getType(node.type); + if (def === undefined) { + if (/^subflow:/.test(node.type)) { + def = { + icon:"subflow.png", + category: "subflows", + color: "#da9", + defaults:{name:{value:""}} + } + } else { + def = {}; } - } else if (localDiff.deleted[node.id]){ - $('').appendTo(nodeTitleDiv); - nodeTitleDiv.addClass("node-diff-node-deleted"); - $('').appendTo(status); - } else if (localDiff.added[node.id]) { - $('').appendTo(nodeTitleDiv); - nodeTitleDiv.addClass("node-diff-node-added") - $('').appendTo(status); - } else { - $('').appendTo(nodeTitleDiv); - nodeTitleDiv.addClass("node-diff-node-unchanged"); - div.addClass("hide"); } + var div = $("
                    ",{class:"node-diff-node-entry collapsed"}); + var row = $("
                    ").appendTo(div); - createNodeIcon(node,def).appendTo(nodeTitleDiv); + var originalNodeDiv = $("
                    ",{class:"node-diff-node-entry-cell"}).appendTo(row); + var localNodeDiv = $("
                    ",{class:"node-diff-node-entry-cell"}).appendTo(row); + var remoteNodeDiv; + var chevron; + if (remoteDiff) { + remoteNodeDiv = $("
                    ",{class:"node-diff-node-entry-cell"}).appendTo(row); + } + $('').appendTo(originalNodeDiv); - var contentDiv = $('
                    ',{class:"node-diff-node-description"}).appendTo(nodeTitleDiv); + if (unChanged) { + stats.local.unchangedCount++; + // $('').appendTo(originalNodeDiv); + //$('').appendTo(originalNodeDiv); + createNode(node,def).appendTo(originalNodeDiv); + localNodeDiv.addClass("node-diff-node-unchanged"); + $(' unchanged').appendTo(localNodeDiv); + if (remoteDiff) { + stats.remote.unchangedCount++; + remoteNodeDiv.addClass("node-diff-node-unchanged"); + $(' unchanged').appendTo(remoteNodeDiv); + } - $('',{class:"node-diff-node-label"}).html(nodeLabel).appendTo(contentDiv); - $('',{class:"node-diff-node-meta"}).html(node.id).appendTo(nodeTitleDiv); + //$('').appendTo(localNodeDiv); + // createNode(node,def).appendTo(localNodeDiv); + // if (remoteDiff) { + // //$('').appendTo(remoteNodeDiv); + // createNode(node,def).appendTo(remoteNodeDiv); + // } + } else if (localDiff.added[node.id]) { + // $('').appendTo(originalNodeDiv); + localNodeDiv.addClass("node-diff-node-added"); + if (remoteNodeDiv) { + remoteNodeDiv.addClass("node-diff-empty"); + } + $(' added').appendTo(localNodeDiv); + createNode(node,def).appendTo(originalNodeDiv); + } else if (remoteDiff && remoteDiff.added[node.id]) { + // $('').appendTo(originalNodeDiv); + localNodeDiv.addClass("node-diff-empty"); + remoteNodeDiv.addClass("node-diff-node-added"); + $(' added').appendTo(remoteNodeDiv); + createNode(node,def).appendTo(originalNodeDiv); + } else { + // chevron = $('').appendTo(originalNodeDiv); + // if (localDiff.changed[node.id] || (remoteDiff && remoteDiff.changed[node.id])) { + // $('').appendTo(chevron); + // } + //$('').appendTo(originalNodeDiv); + createNode(node,def).appendTo(originalNodeDiv); + if (localDiff.deleted[node.z]) { + localNodeDiv.addClass("node-diff-empty"); + } else if (localDiff.deleted[node.id]) { + localNodeDiv.addClass("node-diff-node-deleted"); + $(' deleted').appendTo(localNodeDiv); + } else if (localDiff.changed[node.id]) { + localNodeDiv.addClass("node-diff-node-changed"); + $(' changed').appendTo(localNodeDiv); + } else { + stats.local.unchangedCount++; + localNodeDiv.addClass("node-diff-node-unchanged"); + $(' unchanged').appendTo(localNodeDiv); + } + // createNode(node,def).appendTo(localNodeDiv); - //$('
                    ',{class:"red-ui-search-result-node-type"}).html(node.type).appendTo(contentDiv); - //$('
                    ',{class:"red-ui-search-result-node-id"}).html(node.id).appendTo(contentDiv); + if (remoteDiff) { + if (remoteDiff.deleted[node.z]) { + remoteNodeDiv.addClass("node-diff-empty"); + } else if (remoteDiff.deleted[node.id]) { + remoteNodeDiv.addClass("node-diff-node-deleted"); + $(' deleted').appendTo(remoteNodeDiv); + } else if (remoteDiff.changed[node.id]) { + remoteNodeDiv.addClass("node-diff-node-changed"); + $(' changed').appendTo(remoteNodeDiv); + } else { + stats.remote.unchangedCount++; + remoteNodeDiv.addClass("node-diff-node-unchanged"); + $(' unchanged').appendTo(remoteNodeDiv); + } + //createNode(node,def).appendTo(remoteNodeDiv); + } + } + var localNode = { + node: localDiff.newConfig.all[node.id], + all: localDiff.newConfig.all, + diff: localDiff + }; + var remoteNode; + if (remoteDiff) { + remoteNode = { + node:remoteDiff.newConfig.all[node.id]||null, + all: remoteDiff.newConfig.all, + diff: remoteDiff + } + } + var currentConflictCount = stats.conflicts; + createNodePropertiesTable(def,node,localNode,remoteNode,stats).appendTo(div); + if (currentConflictCount !== stats.conflicts) { + $('').prependTo(localNodeDiv); + $('').prependTo(remoteNodeDiv); + } + row.click(function(evt) { + evt.preventDefault(); + $(this).parent().toggleClass('collapsed'); + }); return div; } - function createNodePropertiesTable(node,newNode,def) { + function createNodePropertiesTable(def,node,localNodeObj,remoteNodeObj,stats) { + var localNode = localNodeObj.node; + var remoteNode; + if (remoteNodeObj) { + remoteNode = remoteNodeObj.node; + } + var nodePropertiesDiv = $("
                    ",{class:"node-diff-node-entry-properties"}); var nodePropertiesTable = $("
                3. position
                  wires
                  "+d+'
                  ").appendTo(nodePropertiesDiv); + //var nodePropertiesTable = $("
                  ").appendTo(nodePropertiesDiv); var row; - if (node.hasOwnProperty('x')) { - if (newNode.x !== node.x || newNode.y !== node.y) { - var currentPosition = RED.utils.createObjectElement({x:node.x,y:node.y}); - var newPosition = RED.utils.createObjectElement({x:newNode.x,y:newNode.y}); - row = $("
                  ").appendTo(nodePropertiesTable); - currentPosition.appendTo(row.children()[1]); - newPosition.appendTo(row.children()[2]); + var localCell, remoteCell; + var currentValue, localValue, remoteValue; + var localChanged = false; + var remoteChanged = false; + var localChanges = 0; + var remoteChanges = 0; + var conflict = false; + var conflicted = false; + var status; + if (remoteNodeObj) { + if ( (remoteNodeObj.diff.changed[node.id] && localNodeObj.diff.deleted[node.id]) || + (remoteNodeObj.diff.deleted[node.id] && localNodeObj.diff.changed[node.id]) + ) { + conflicted = true; + } + } + if (node.hasOwnProperty('x')) { + if (localNode) { + if (localNode.x !== node.x || localNode.y !== node.y) { + localChanged = true; + localChanges++; + } + } + if (remoteNode) { + if (remoteNode.x !== node.x || remoteNode.y !== node.y) { + remoteChanged = true; + remoteChanges++; + } + } + if ( (remoteChanged && localChanged && (localNode.x !== remoteNode.y || localNode.y !== remoteNode.x)) || + (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) || + (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id]) + ) { + conflicted = true; + conflict = true; + } + row = $("").appendTo(nodePropertiesTable); + $("").appendTo(nodePropertiesTable); - formatWireProperty(node.wires).appendTo(row.children()[1]); - formatWireProperty(newNode.wires).appendTo(row.children()[2]); + currentValue = JSON.stringify(node.wires); + if (localNode) { + localValue = JSON.stringify(localNode.wires); + if (currentValue !== localValue) { + localChanged = true; + localChanges++; + } + } + if (remoteNode) { + remoteValue = JSON.stringify(remoteNode.wires); + if (currentValue !== remoteValue) { + remoteChanged = true; + remoteChanges++; + } + } + if ( (remoteChanged && localChanged && (localValue !== remoteValue)) || + (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) || + (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id]) + ){ + conflicted = true; + conflict = true; + } + row = $("").appendTo(nodePropertiesTable); + $("').appendTo(nodePropertiesTable); - formattedProperty.appendTo(row.children()[1]); - newFormattedProperty.appendTo(row.children()[2]); + localChanged = false; + remoteChanged = false; + conflict = false; + currentValue = JSON.stringify(node[d]); + if (localNode) { + localValue = JSON.stringify(localNode[d]); + if (currentValue !== localValue) { + localChanged = true; + localChanges++; + } } - }) + if (remoteNode) { + remoteValue = JSON.stringify(remoteNode[d]); + if (currentValue !== remoteValue) { + remoteChanged = true; + remoteChanges++; + } + } + + if ( (remoteChanged && localChanged && (localValue !== remoteValue)) || + (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) || + (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id]) + ){ + conflicted = true; + conflict = true; + } + + row = $("").appendTo(nodePropertiesTable); + $("",{class:"node-diff-property-header"}).prependTo(nodePropertiesTable); + // $("').appendTo(row); + // } else if (localChanges > 0) { + // cell = $('').appendTo(row); + // if (conflicted) { + // $(' conflict ').appendTo(cell); + // } + // $(' changed').appendTo(cell); + // } else { + // $('').appendTo(row); + // } + // } else if (localNodeObj.diff.deleted[node.id]) { + // cell = $('').appendTo(row); + // if (conflicted) { + // $(' conflict ').appendTo(cell); + // } + // $(' deleted').appendTo(cell); + // } else { + // $('').appendTo(row); + // } + // if (remoteNode) { + // if (remoteNodeObj.diff.added[node.id]) { + // $('').appendTo(row); + // } else if (remoteChanges > 0) { + // cell = $('').appendTo(row); + // if (conflicted) { + // $(' conflict ').appendTo(cell); + // } + // $(' changed').appendTo(cell); + // } else { + // $('').appendTo(row); + // } + // } else if (remoteNodeObj.diff.deleted[node.id]) { + // cell = $('').appendTo(row); + // if (conflicted) { + // $(' conflict ').appendTo(cell); + // } + // $(' deleted').appendTo(cell); + // } else { + // $('').appendTo(row); + // } + if (conflicted) { + stats.conflicts++; + } return nodePropertiesDiv; } - - - - function showlocalDiff() { + function showLocalDiff() { var nns = RED.nodes.createCompleteNodeSet(); var originalFlow = RED.nodes.originalFlow(); var diff = generateDiff(originalFlow,nns); showDiff(diff); } - - + function showRemoteDiff() { + $.ajax({ + headers: { + "Accept":"application/json", + }, + cache: false, + url: 'flows', + success: function(nodes) { + var localFlow = RED.nodes.createCompleteNodeSet(); + var originalFlow = RED.nodes.originalFlow(); + var remoteFlow = nodes.flows; + var localDiff = generateDiff(originalFlow,localFlow); + var remoteDiff = generateDiff(originalFlow,remoteFlow); + showDiff(localDiff,remoteDiff); + } + }); + } function parseNodes(nodeList) { var tabOrder = []; var tabs = {}; @@ -355,7 +813,6 @@ RED.diff = (function() { globals: globals } } - function generateDiff(currentNodes,newNodes) { var currentConfig = parseNodes(currentNodes); var newConfig = parseNodes(newNodes); @@ -363,18 +820,15 @@ RED.diff = (function() { var added = {}; var deleted = {}; var changed = {}; - var conflicted = {}; Object.keys(currentConfig.all).forEach(function(id) { var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); if (!newConfig.all.hasOwnProperty(id)) { if (!pending.added.hasOwnProperty(id)) { deleted[id] = true; - conflicted[id] = node&&node.changed; } } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) { changed[id] = true; - conflicted[id] = node.changed; } }); Object.keys(newConfig.all).forEach(function(id) { @@ -383,45 +837,14 @@ RED.diff = (function() { } }); - // console.log("Added",added); - // console.log("Deleted",deleted); - // console.log("Changed",changed); - // console.log("Conflicted",conflicted); - // - // var formatString = function(id) { - // return conflicted[id]?"!":(added[id]?"+":(deleted[id]?"-":(changed[id]?"~":" "))); - // } - // newConfig.tabOrder.forEach(function(tabId) { - // var tab = newConfig.tabs[tabId]; - // console.log(formatString(tabId),"Flow:",tab.n.label, "("+tab.n.id+")"); - // tab.nodes.forEach(function(node) { - // console.log(" ",formatString(node.id),node.type,node.name || node.id); - // }) - // if (currentConfig.tabs[tabId]) { - // currentConfig.tabs[tabId].nodes.forEach(function(node) { - // if (deleted[node.id]) { - // console.log(" ",formatString(node.id),node.type,node.name || node.id); - // } - // }) - // } - // }); - // currentConfig.tabOrder.forEach(function(tabId) { - // if (deleted[tabId]) { - // var tab = currentConfig.tabs[tabId]; - // console.log(formatString(tabId),"Flow:",tab.n.label, "("+tab.n.id+")"); - // } - // }); - return { currentConfig: currentConfig, newConfig: newConfig, added: added, deleted: deleted, changed: changed, - conflicted: conflicted } } - function formatNodeProperty(prop) { var formattedProperty = prop; if (formattedProperty === null) { @@ -436,16 +859,14 @@ RED.diff = (function() { } return formattedProperty; } - - function showDiff(localDiff) { - var el; + function showDiff(localDiff,remoteDiff) { var list = $("#node-dialog-view-diff-diff"); list.editableList('empty'); + $("#node-dialog-view-diff-headers").empty(); var currentConfig = localDiff.currentConfig; var newConfig = localDiff.newConfig; - - list.editableList('addItem',{ + var el = { diff: localDiff, def: { category: 'config', @@ -459,7 +880,22 @@ RED.diff = (function() { n: {}, nodes: newConfig.globals } - }); + }; + + if (remoteDiff !== undefined) { + $('#node-dialog-view-diff').addClass('node-diff-three-way'); + + $('
                  Local
                  Remote
                  ').appendTo("#node-dialog-view-diff-headers"); + el.remoteTab = { + n:{}, + nodes:remoteDiff.newConfig.globals + }; + el.remoteDiff = remoteDiff; + } else { + $('#node-dialog-view-diff').removeClass('node-diff-three-way'); + } + + list.editableList('addItem',el); var seenTabs = {}; @@ -473,20 +909,45 @@ RED.diff = (function() { if (newConfig.tabs.hasOwnProperty(tabId)) { el.newTab = newConfig.tabs[tabId]; } + if (remoteDiff !== undefined) { + el.remoteTab = remoteDiff.newConfig.tabs[tabId]; + el.remoteDiff = remoteDiff; + } seenTabs[tabId] = true; list.editableList('addItem',el) }); newConfig.tabOrder.forEach(function(tabId) { if (!seenTabs[tabId]) { + seenTabs[tabId] = true; var tab = newConfig.tabs[tabId]; var el = { diff: localDiff, def: {}, - tab:tab + tab:tab, + newTab: tab }; + if (remoteDiff !== undefined) { + el.remoteDiff = remoteDiff; + } list.editableList('addItem',el) } - }) + }); + if (remoteDiff !== undefined) { + remoteDiff.newConfig.tabOrder.forEach(function(tabId) { + if (!seenTabs[tabId]) { + var tab = remoteDiff.newConfig.tabs[tabId]; + // TODO how to recognise this is a remotely added flow + var el = { + diff: localDiff, + remoteDiff: remoteDiff, + def: {}, + tab:tab, + remoteTab:tab + }; + list.editableList('addItem',el) + } + }); + } var subflowId; for (subflowId in currentConfig.subflows) { if (currentConfig.subflows.hasOwnProperty(subflowId)) { @@ -504,6 +965,10 @@ RED.diff = (function() { if (newConfig.subflows.hasOwnProperty(subflowId)) { el.newTab = newConfig.subflows[subflowId]; } + if (remoteDiff !== undefined) { + el.remoteTab = remoteDiff.newConfig.subflows[subflowId]; + el.remoteDiff = remoteDiff; + } list.editableList('addItem',el) } } @@ -517,19 +982,42 @@ RED.diff = (function() { category: "subflows", color: "#da9" }, - tab:newConfig.subflows[subflowId] + tab:newConfig.subflows[subflowId], + newTab:newConfig.subflows[subflowId] + } + if (remoteDiff !== undefined) { + el.remoteDiff = remoteDiff; } list.editableList('addItem',el) } } + if (remoteDiff !== undefined) { + for (subflowId in remoteDiff.newConfig.subflows) { + if (remoteDiff.newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { + // TODO how to recognise this is a remotely added flow + el = { + diff: localDiff, + remoteDiff: remoteDiff, + def: { + defaults:{}, + icon:"subflow.png", + category: "subflows", + color: "#da9" + }, + tab:remoteDiff.newConfig.subflows[subflowId], + remoteTab: remoteDiff.newConfig.subflows[subflowId] + } + list.editableList('addItem',el) + } + } + } + $("#node-diff-filter-changed").addClass("selected"); $("#node-diff-filter-all").removeClass("selected"); $("#node-dialog-view-diff").dialog("open"); } - - return { init: init } diff --git a/editor/sass/diff.scss b/editor/sass/diff.scss index 8f6febf00..e3de4c77d 100644 --- a/editor/sass/diff.scss +++ b/editor/sass/diff.scss @@ -21,14 +21,16 @@ .red-ui-editableList-container { border-radius:1px; padding:0; + background: #f9f9f9; } #node-dialog-view-diff-diff { position: absolute; - top:50px; + top:80px; bottom:10px; left:10px; right:10px; li { + background: #f9f9f9; padding: 0px; border: none; min-height: 0; @@ -37,14 +39,34 @@ } .red-ui-editableList-item-content { padding: 5px; - padding-bottom: 0; + // padding-bottom: 5px; } } +#node-dialog-view-diff-headers { + position: absolute; + left:17px; + right:32px; + top: 55px; + height: 25px; + .node-diff-node-entry-cell:not(:first-child) { + background: #f9f9f9; + text-align: center; + border-top: 1px solid $secondary-border-color; + border-color:$secondary-border-color; + } + .node-diff-node-entry-cell:last-child { + border-right: 1px solid $secondary-border-color; + + } +} + .node-diff-toolbar { position:absolute; top:0; left:0; right:0; + height: 43px; + box-sizing: border-box; color: #666; text-align: right; padding: 8px 10px; @@ -53,12 +75,13 @@ white-space: nowrap; } .node-diff-tab { - border: 1px solid $secondary-border-color; - border-radius: 3px; + background: #fff; + border: 1px solid #ddd; + border-radius: 1px; overflow: hidden; &.collapsed { - .node-diff-tab-title > .node-diff-chevron { + .node-diff-tab-title .node-diff-chevron { transform: rotate(-90deg); } .node-diff-node-entry { @@ -67,21 +90,27 @@ } } .node-diff-tab-stats { - position: absolute; - left: 50%; - top: 13px; + font-size: 0.9em; } .node-diff-chevron { display: inline-block; width: 15px; text-align: center; - margin: 3px 5px 3px 5px; + margin-left: 3px; transition: transform 0.1s ease-in-out; } .node-diff-node-entry { - border-top: 1px solid $secondary-border-color; + margin-left: 20px; + font-size: 0.9em; + + &:first-child { + border-top: 1px solid #eee; + } + &:not(:last-child) { + border-bottom: 1px solid #eee; + } &.collapsed { .node-diff-chevron { @@ -91,6 +120,14 @@ display: none; } } + &:not(.collapsed) { + .node-diff-node-entry-cell:not(:first-child) { + //display: none; + } + .node-diff-node-entry-cell:first-child { + width: 100% + } + } table { border-collapse: collapse; @@ -99,20 +136,57 @@ } td, th { border: 1px solid $secondary-border-color; - padding: 3px 5px; + padding: 0 0 0 3px; text-align: left; overflow-x: auto; } tr { vertical-align: top; + &:first-child td { + white-space:nowrap; + overflow:hidden; + } } - td:nth-child(1) { - width: 100px; + td:first-child { + width: 140px; } td:not(:first-child) { - width: calc(50% - 100px); + width: calc( 100% - 140px); + } + td { + .node-diff-status { + margin-left: 0; + } + } + tr:not(.node-diff-property-header) { + .node-diff-status { + width: 12px; + margin-left: 0; + margin-top: 0; + margin-bottom: 0; + margin-right: 5px; + } } } +.node-diff-three-way { + .node-diff-node-entry-cell { + width: 33.3333333% + } + td:not(:first-child) { + width: calc( (100% - 140px) / 2); + } + + .node-diff-node-entry { + .node-diff-node-entry-cell { + width: calc( ( 100% + 20px ) / 3 ); + + &:first-child { + width: calc( ( 100% + 20px ) / 3 - 20px ); + } + } + } +} + .node-diff-column { display:inline-block; height:100%; @@ -126,17 +200,24 @@ } .node-diff-tab-title { - padding: 3px 3px 3px 0; - background: #f6f6f6; + cursor: pointer; + padding: 0; + // background: #f6f6f6; +} +.node-diff-tab-title-meta { + vertical-align: middle; + display: inline-block; + padding-top: 2px; +} +.node-diff-node-entry-header { cursor: pointer; } - .node-diff-node-entry-node { vertical-align: middle; display: inline-block; margin: 5px; - width: 24px; - height: 20px; + width: 18px; + height: 15px; background: #ddd; border-radius: 2px; border: 1px solid #999; @@ -145,11 +226,12 @@ background-size: contain; position: relative; - .palette-icon { - width: 16px; + .palette_icon { + background-position: 49% 50%; + width: 15px; } .palette_icon_container { - width: 24px; + width: 18px; } } .node-diff-tab-empty { @@ -187,28 +269,39 @@ color: #f89406; } } +.node-diff-node-unchanged { + //background: #fff2ca; + .node-diff-status { + color: #bbb; + } +} +.node-diff-node-conflict { + .node-diff-status { + color: #9b45ce; + } +} .node-diff-node-entry-title { - cursor: pointer; + display: inline-block; .node-diff-status { margin-left: 15px; } } .node-diff-node-entry-properties { - margin: 6px 8px 6px 30px; + margin: 5px ; color: #666; } .node-diff-status { display: inline-block; - width: 15px; height: 20px; margin-left: 5px; - vertical-align: middle; + vertical-align: top; + margin-top: 6px; + margin-bottom: 6px; text-align: center; } .node-diff-node-description { color: $form-text-color; - margin-left: 5px; margin-right: 5px; padding-top: 5px; display: inline-block; @@ -220,7 +313,7 @@ } .node-diff-node-meta { float: right; - font-size: 0.9em; + //font-size: 0.9em; color: #999; margin-top: 7px; margin-right: 10px; @@ -231,3 +324,96 @@ .node-diff-deleted { color: #f80000} .node-diff-changed { color: #f89406} .node-diff-conflicted { color: purple} + + +.node-diff-node-entry-cell { + display: inline-block; + vertical-align: top; + box-sizing: border-box; + width: calc( (100% - 20px) / 2); + height: 32px; + border-left: 1px solid #eee; + padding-top: 2px; + white-space: nowrap; + overflow: hidden; +} +.node-diff-empty { + background: #f3f3f3; + background: repeating-linear-gradient( + 20deg, + #fff, #fff 5px, + #f9f9f9 5px, + #f9f9f9 10px + ); +} +.node-diff-node-entry-cell:first-child { + border-left: none; +} +.node-diff-property-cell-label { + margin-left: 20px; + vertical-align: top; + box-sizing: border-box; + padding-left: 8px; + width: 120px; +} +.node-diff-property-wires { + display: inline-block; + .node-diff-node-entry-node { + width: 18px; + height: 15px; + } + .palette_icon_container { + width: 18px; + } + .palette_icon { + width: 15px; + } + ul,li,ol { + background: none !important; + } + ul { + vertical-align: middle; + display: inline-block; + margin-left: 5px; + } + li { + list-style-type: none !important; + } + ol { + font-size: 0.9em; + margin: 0; + & > span { + vertical-align: middle; + display: inline-block; + width: 30px; + text-align: center; + } + & > li:not(:last-child) { + border-bottom: 1px solid #999; + } + } + +} +.node-diff-node-props .node-diff-node-entry-cell:first-child { + padding: 6px 0px; + span:not(.node-diff-chevron) { + margin-left: 5px; + } + +} +.node-diff-property-cell { + // vertical-align: top; + // display:inline-block; + // + // box-sizing: border-box; + // padding: 1px 5px; + //min-height: 30px; + + &.node-diff-node-changed { + background: #fff2e1; + } + &.node-diff-node-conflict { + background: #ffdad4; + } + +} From 31a72b65624fd51ca2287303b011a85839a94093 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 20 Dec 2016 19:42:38 +0000 Subject: [PATCH 35/44] Three-way-diff --- editor/js/nodes.js | 33 +- editor/js/ui/deploy.js | 63 ++- editor/js/ui/diff.js | 659 +++++++++++++++++++----------- editor/js/ui/subflow.js | 87 ++-- editor/sass/diff.scss | 104 ++++- editor/templates/index.mst | 22 +- red/api/locales/en-US/editor.json | 9 +- 7 files changed, 645 insertions(+), 332 deletions(-) diff --git a/editor/js/nodes.js b/editor/js/nodes.js index e471d9081..e50d02eca 100644 --- a/editor/js/nodes.js +++ b/editor/js/nodes.js @@ -24,10 +24,6 @@ RED.nodes = (function() { var workspacesOrder =[]; var subflows = {}; var loadedFlowVersion = null; - var pending = { - deleted: {}, - added: {} - }; var initialLoad; @@ -35,12 +31,6 @@ RED.nodes = (function() { function setDirty(d) { dirty = d; - if (!d) { - pending = { - deleted: {}, - added: {} - }; - } RED.events.emit("nodes:change",{dirty:dirty}); } @@ -191,8 +181,6 @@ RED.nodes = (function() { } nodes.push(n); } - delete pending.deleted[n.id]; - pending.added[n.id] = true; RED.events.emit('nodes:add',n); } function addLink(l) { @@ -258,12 +246,6 @@ RED.nodes = (function() { if (node && node._def.onremove) { node._def.onremove.call(n); } - delete pending.added[id]; - pending.deleted[id] = true; - removedNodes.forEach(function(node) { - delete pending.added[node.id]; - pending.deleted[node.id] = true; - }); return {links:removedLinks,nodes:removedNodes}; } @@ -276,8 +258,6 @@ RED.nodes = (function() { function addWorkspace(ws) { workspaces[ws.id] = ws; - pending.added[ws.id] = true; - delete pending.deleted[ws.id]; ws._def = { defaults: { label: {value:""} @@ -315,8 +295,6 @@ RED.nodes = (function() { var result = removeNode(removedNodes[n].id); removedLinks = removedLinks.concat(result.links); } - pending.deleted[id] = true; - delete pending.added[id] return {nodes:removedNodes,links:removedLinks}; } @@ -346,8 +324,6 @@ RED.nodes = (function() { outputs: sf.out.length } subflows[sf.id] = sf; - delete pending.deleted[sf.id]; - pending.added[sf.id] = true; RED.nodes.registerType("subflow:"+sf.id, { defaults:{name:{value:""}}, info: sf.info, @@ -371,8 +347,6 @@ RED.nodes = (function() { } function removeSubflow(sf) { delete subflows[sf.id]; - delete pending.added[sf.id]; - pending.deleted[sf.id] = true; registry.removeNodeType("subflow:"+sf.id); } @@ -1027,8 +1001,9 @@ RED.nodes = (function() { for (var w1=0;w1
                    ').appendTo(document.body); var toolbar = $('
                    '+ - ''+ - 'all nodes'+ - 'changed nodes'+ - ''+ + ' '+ + // ''+ + // 'previous'+ + // 'next'+ + // ''+ '
                    ').prependTo(dialog); - - toolbar.find(".node-diff-filter").click(function(evt) { - evt.preventDefault(); - if (!$(this).hasClass('selected')) { - $(this).siblings().removeClass('selected'); - $(this).addClass('selected'); - } - if ($(this).attr('id') === 'node-diff-filter-all') { - diffList.find('.node-diff-node-unchanged').parent().removeClass('hide'); - diffList.find('.node-diff-tab-unchanged').parent().removeClass('hide'); - } else { - diffList.find('.node-diff-node-unchanged').parent().addClass('hide'); - diffList.find('.node-diff-tab-unchanged').parent().addClass('hide'); - $(".node-diff-tab.node-diff-tab-unchanged").addClass("collapsed"); - } - }) + // + // toolbar.find(".node-diff-filter").click(function(evt) { + // evt.preventDefault(); + // if (!$(this).hasClass('selected')) { + // $(this).siblings().removeClass('selected'); + // $(this).addClass('selected'); + // } + // if ($(this).attr('id') === 'node-diff-filter-all') { + // diffList.find('.node-diff-node-unchanged').parent().removeClass('hide'); + // diffList.find('.node-diff-tab-unchanged').parent().removeClass('hide'); + // } else { + // diffList.find('.node-diff-node-unchanged').parent().addClass('hide'); + // diffList.find('.node-diff-tab-unchanged').parent().addClass('hide'); + // $(".node-diff-tab.node-diff-tab-unchanged").addClass("collapsed"); + // } + // }) $("#node-dialog-view-diff").dialog({ title: RED._('deploy.confirm.button.review'), modal: true, autoOpen: false, buttons: [ - // { - // text: RED._("deploy.confirm.button.cancel"), - // click: function() { - // $( this ).dialog( "close" ); - // } - // }, { - text: RED._("common.label.close"), - class: "primary", + text: RED._("common.label.cancel"), click: function() { $( this ).dialog( "close" ); } + }, + { + id: "node-diff-view-diff-merge", + text: RED._("deploy.confirm.button.merge"), + class: "primary disabled", + click: function() { + if (!$("#node-diff-view-diff-merge").hasClass('disabled')) { + refreshConflictHeader(); + mergeDiff(currentDiff); + $( this ).dialog( "close" ); + } + } } ], open: function() { @@ -67,27 +75,17 @@ RED.diff = (function() { var tab = object.tab.n; var def = object.def; var tabDiv = $('
                    ',{class:"node-diff-tab"}).appendTo(container); + tabDiv.addClass('collapsed'); var titleRow = $('
                    ',{class:"node-diff-tab-title"}).appendTo(tabDiv); var nodesDiv = $('
                    ').appendTo(tabDiv); var originalCell = $('
                    ',{class:"node-diff-node-entry-cell"}).appendTo(titleRow); - var localCell = $('
                    ',{class:"node-diff-node-entry-cell"}).appendTo(titleRow); + var localCell = $('
                    ',{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(titleRow); var remoteCell; - if (remoteDiff) { - remoteCell = $('
                    ',{class:"node-diff-node-entry-cell"}).appendTo(titleRow); - } - // if (localDiff.added[tab.id]) { - // titleRow.addClass("node-diff-node-added"); - // } else if (localDiff.deleted[tab.id]) { - // titleRow.addClass("node-diff-node-deleted"); - // } - // var status = $('').appendTo(originalCell); + var selectState; - // if (!object.newTab && object.remoteTab) { - // $('').appendTo(remoteCell); - // //} else if (object.newTab && (remoteDiff && !object.remoteTab)) { - // } else if (localDiff.added[tab.id]) { - // $('').appendTo(localCell); - // } + if (remoteDiff) { + remoteCell = $('
                    ',{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(titleRow); + } $('').appendTo(originalCell); createNodeIcon(tab,def).appendTo(originalCell); var tabForLabel = (object.newTab || object.tab).n; @@ -97,7 +95,7 @@ RED.diff = (function() { } else if (tab.type === 'subflow') { titleSpan.html((tabForLabel.name||tabForLabel.id)); } else { - titleSpan.html("Global configuration nodes"); + titleSpan.html("Global nodes"); } var flowStats = { local: { @@ -132,27 +130,27 @@ RED.diff = (function() { var div = $("
                    ",{class:"node-diff-node-entry node-diff-node-props collapsed"}).appendTo(nodesDiv); var row = $("
                    ",{class:"node-diff-node-entry-header"}).appendTo(div); var originalNodeDiv = $("
                    ",{class:"node-diff-node-entry-cell"}).appendTo(row); - var localNodeDiv = $("
                    ",{class:"node-diff-node-entry-cell"}).appendTo(row); + var localNodeDiv = $("
                    ",{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(row); var localChanged = false; - var remoteChanged = false; if (!localDiff.newConfig.all[tab.id]) { localNodeDiv.addClass("node-diff-empty"); } else if (localDiff.added[tab.id]) { localNodeDiv.addClass("node-diff-node-added"); + localChanged = true; $(' added').appendTo(localNodeDiv); } else if (localDiff.changed[tab.id]) { localNodeDiv.addClass("node-diff-node-changed"); - $(' changed').appendTo(localNodeDiv); + localChanged = true; + $(' changed').appendTo(localNodeDiv); } else { localNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(localNodeDiv); + $(' unchanged').appendTo(localNodeDiv); } - var remoteNodeDiv; if (remoteDiff) { - remoteNodeDiv = $("
                    ",{class:"node-diff-node-entry-cell"}).appendTo(row); + remoteNodeDiv = $("
                    ",{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(row); if (!remoteDiff.newConfig.all[tab.id]) { remoteNodeDiv.addClass("node-diff-empty"); } else if (remoteDiff.added[tab.id]) { @@ -160,48 +158,69 @@ RED.diff = (function() { $(' added').appendTo(remoteNodeDiv); } else if (remoteDiff.changed[tab.id]) { remoteNodeDiv.addClass("node-diff-node-changed"); - $(' changed').appendTo(remoteNodeDiv); + $(' changed').appendTo(remoteNodeDiv); } else { remoteNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(remoteNodeDiv); + $(' unchanged').appendTo(remoteNodeDiv); } } $('').appendTo(originalNodeDiv); $('').html("Flow Properties").appendTo(originalNodeDiv); + row.click(function(evt) { evt.preventDefault(); $(this).parent().toggleClass('collapsed'); }); - createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,flowStats).appendTo(div); + + createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,object.conflicts).appendTo(div); + selectState = ""; + if (object.conflicts[tab.id]) { + flowStats.conflicts++; + + if (!localNodeDiv.hasClass("node-diff-empty")) { + $('').prependTo(localNodeDiv); + } + if (!remoteNodeDiv.hasClass("node-diff-empty")) { + $('').prependTo(remoteNodeDiv); + } + div.addClass("node-diff-node-entry-conflict"); + } else { + if (!localChanged) { + selectState = "remote"; + } else { + selectState = "local"; + } + } + + createNodeConflictRadioBoxes(tab,div,localNodeDiv,remoteNodeDiv,true,!object.conflicts[tab.id],selectState); } } - // var stats = $('',{class:"node-diff-tab-stats"}).appendTo(titleRow); - - + var localNodeCount = 0; + var remoteNodeCount = 0; var seen = {}; object.tab.nodes.forEach(function(node) { seen[node.id] = true; - createNodeDiffRow(node,flowStats,localDiff,remoteDiff).appendTo(nodesDiv) + createNodeDiffRow(node,flowStats,localDiff,remoteDiff,object.conflicts[node.id]).appendTo(nodesDiv) }); if (object.newTab) { + localNodeCount = object.newTab.nodes.length; object.newTab.nodes.forEach(function(node) { if (!seen[node.id]) { seen[node.id] = true; - createNodeDiffRow(node,flowStats,localDiff,remoteDiff).appendTo(nodesDiv) + createNodeDiffRow(node,flowStats,localDiff,remoteDiff,object.conflicts[node.id]).appendTo(nodesDiv) } }); } if (object.remoteTab) { + remoteNodeCount = object.remoteTab.nodes.length; object.remoteTab.nodes.forEach(function(node) { if (!seen[node.id]) { - createNodeDiffRow(node,flowStats,localDiff,remoteDiff).appendTo(nodesDiv) + createNodeDiffRow(node,flowStats,localDiff,remoteDiff,object.conflicts[node.id]).appendTo(nodesDiv) } }); } titleRow.click(function(evt) { - evt.preventDefault(); - // if (titleRow.parent().find(".node-diff-node-entry:not(.hide)").length > 0) { titleRow.parent().toggleClass('collapsed'); if ($(this).parent().hasClass('collapsed')) { @@ -211,9 +230,6 @@ RED.diff = (function() { // } }) - var localChangesCount = flowStats.local.addedCount+flowStats.local.deletedCount+flowStats.local.changedCount; - var remoteChangesCount = flowStats.remote.addedCount+flowStats.remote.deletedCount+flowStats.remote.changedCount; - if (localDiff.deleted[tab.id]) { $(' flow deleted').appendTo(localCell); } else if (object.newTab) { @@ -222,28 +238,31 @@ RED.diff = (function() { } else { if (tab.id) { if (localDiff.changed[tab.id]) { - localChangesCount++; flowStats.local.changedCount++; } else { flowStats.local.unchangedCount++; } } var localStats = $('',{class:"node-diff-tab-stats"}).appendTo(localCell); - if (flowStats.conflicts > 0) { - $(' '+flowStats.conflicts+'').appendTo(localStats); - } - if (flowStats.local.unchangedCount > 0) { - $(' '+flowStats.local.unchangedCount+'').appendTo(localStats); - } - if (flowStats.local.addedCount > 0) { - $(' '+flowStats.local.addedCount+'').appendTo(localStats); - } - if (flowStats.local.changedCount > 0) { - $(' '+flowStats.local.changedCount+'').appendTo(localStats); - } - if (flowStats.local.deletedCount > 0) { - $(' '+flowStats.local.deletedCount+'').appendTo(localStats); + $(''+localNodeCount+' nodes').appendTo(localStats); + + if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.deletedCount > 0) { + $(' [ ').appendTo(localStats); + if (flowStats.conflicts > 0) { + $(' '+flowStats.conflicts+'').appendTo(localStats); + } + if (flowStats.local.addedCount > 0) { + $(' '+flowStats.local.addedCount+'').appendTo(localStats); + } + if (flowStats.local.changedCount > 0) { + $(' '+flowStats.local.changedCount+'').appendTo(localStats); + } + if (flowStats.local.deletedCount > 0) { + $(' '+flowStats.local.deletedCount+'').appendTo(localStats); + } + $(' ] ').appendTo(localStats); } + } } else { localCell.addClass("node-diff-empty"); @@ -258,33 +277,42 @@ RED.diff = (function() { } else { if (tab.id) { if (remoteDiff.changed[tab.id]) { - remoteChangesCount++; flowStats.remote.changedCount++; } else { flowStats.remote.unchangedCount++; } } var remoteStats = $('',{class:"node-diff-tab-stats"}).appendTo(remoteCell); - if (flowStats.conflicts > 0) { - $(' '+flowStats.conflicts+'').appendTo(remoteStats); - } - if (flowStats.remote.unchangedCount > 0) { - $(' '+flowStats.remote.unchangedCount+'').appendTo(remoteStats); - } - if (flowStats.remote.addedCount > 0) { - $(' '+flowStats.remote.addedCount+'').appendTo(remoteStats); - } - if (flowStats.remote.changedCount > 0) { - $(' '+flowStats.remote.changedCount+'').appendTo(remoteStats); - } - if (flowStats.remote.deletedCount > 0) { - $(' '+flowStats.remote.deletedCount+'').appendTo(remoteStats); + $(''+remoteNodeCount+' nodes').appendTo(remoteStats); + if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.deletedCount > 0) { + $(' [ ').appendTo(remoteStats); + if (flowStats.conflicts > 0) { + $(' '+flowStats.conflicts+'').appendTo(remoteStats); + } + if (flowStats.remote.addedCount > 0) { + $(' '+flowStats.remote.addedCount+'').appendTo(remoteStats); + } + if (flowStats.remote.changedCount > 0) { + $(' '+flowStats.remote.changedCount+'').appendTo(remoteStats); + } + if (flowStats.remote.deletedCount > 0) { + $(' '+flowStats.remote.deletedCount+'').appendTo(remoteStats); + } + $(' ] ').appendTo(remoteStats); } } } else { remoteCell.addClass("node-diff-empty"); } + if (flowStats.conflicts > 0) { + titleRow.addClass("node-diff-node-entry-conflict"); + } + if (tab.id) { + selectState = ""; + createNodeConflictRadioBoxes(tab,titleRow,localCell,remoteCell, false, !(flowStats.conflicts > 0 &&(localDiff.deleted[tab.id] || remoteDiff.deleted[tab.id])),selectState); + } } + if (tabDiv.find(".node-diff-node-entry").length === 0) { tabDiv.addClass("node-diff-tab-empty"); } @@ -295,7 +323,6 @@ RED.diff = (function() { } }); } - function formatWireProperty(wires,allNodes) { var result = $("
                    ",{class:"node-diff-property-wires"}) var list = $("
                      "); @@ -356,10 +383,10 @@ RED.diff = (function() { $('',{class:"node-diff-node-label"}).html(nodeLabel).appendTo(contentDiv); return nodeTitleDiv; } - function createNodeDiffRow(node,stats,localDiff,remoteDiff) { + function createNodeDiffRow(node,stats,localDiff,remoteDiff,conflicted) { var hasChanges = false; // exists in original and local/remote but with changes var unChanged = true; // existing in original,local,remote unchanged - var conflicted = false; + var localChanged = false; if (localDiff.added[node.id]) { stats.local.addedCount++; @@ -387,7 +414,7 @@ RED.diff = (function() { hasChanges = true; unChanged = false; } - + // console.log(node.id,localDiff.added[node.id],remoteDiff.added[node.id],localDiff.deleted[node.id],remoteDiff.deleted[node.id],localDiff.changed[node.id],remoteDiff.changed[node.id]) var def = RED.nodes.getType(node.type); if (def === undefined) { if (/^subflow:/.test(node.type)) { @@ -402,38 +429,28 @@ RED.diff = (function() { } } var div = $("
                      ",{class:"node-diff-node-entry collapsed"}); - var row = $("
                      ").appendTo(div); + var row = $("
                      ",{class:"node-diff-node-entry-header"}).appendTo(div); var originalNodeDiv = $("
                      ",{class:"node-diff-node-entry-cell"}).appendTo(row); - var localNodeDiv = $("
                      ",{class:"node-diff-node-entry-cell"}).appendTo(row); + var localNodeDiv = $("
                      ",{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(row); var remoteNodeDiv; var chevron; if (remoteDiff) { - remoteNodeDiv = $("
                      ",{class:"node-diff-node-entry-cell"}).appendTo(row); + remoteNodeDiv = $("
                      ",{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(row); } $('').appendTo(originalNodeDiv); if (unChanged) { stats.local.unchangedCount++; - // $('').appendTo(originalNodeDiv); - //$('').appendTo(originalNodeDiv); createNode(node,def).appendTo(originalNodeDiv); localNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(localNodeDiv); + $(' unchanged').appendTo(localNodeDiv); if (remoteDiff) { stats.remote.unchangedCount++; remoteNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(remoteNodeDiv); + $(' unchanged').appendTo(remoteNodeDiv); } - - //$('').appendTo(localNodeDiv); - // createNode(node,def).appendTo(localNodeDiv); - // if (remoteDiff) { - // //$('').appendTo(remoteNodeDiv); - // createNode(node,def).appendTo(remoteNodeDiv); - // } } else if (localDiff.added[node.id]) { - // $('').appendTo(originalNodeDiv); localNodeDiv.addClass("node-diff-node-added"); if (remoteNodeDiv) { remoteNodeDiv.addClass("node-diff-empty"); @@ -441,32 +458,28 @@ RED.diff = (function() { $(' added').appendTo(localNodeDiv); createNode(node,def).appendTo(originalNodeDiv); } else if (remoteDiff && remoteDiff.added[node.id]) { - // $('').appendTo(originalNodeDiv); localNodeDiv.addClass("node-diff-empty"); remoteNodeDiv.addClass("node-diff-node-added"); $(' added').appendTo(remoteNodeDiv); createNode(node,def).appendTo(originalNodeDiv); } else { - // chevron = $('').appendTo(originalNodeDiv); - // if (localDiff.changed[node.id] || (remoteDiff && remoteDiff.changed[node.id])) { - // $('').appendTo(chevron); - // } - //$('').appendTo(originalNodeDiv); createNode(node,def).appendTo(originalNodeDiv); if (localDiff.deleted[node.z]) { localNodeDiv.addClass("node-diff-empty"); + localChanged = true; } else if (localDiff.deleted[node.id]) { localNodeDiv.addClass("node-diff-node-deleted"); $(' deleted').appendTo(localNodeDiv); + localChanged = true; } else if (localDiff.changed[node.id]) { localNodeDiv.addClass("node-diff-node-changed"); - $(' changed').appendTo(localNodeDiv); + $(' changed').appendTo(localNodeDiv); + localChanged = true; } else { stats.local.unchangedCount++; localNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(localNodeDiv); + $(' unchanged').appendTo(localNodeDiv); } - // createNode(node,def).appendTo(localNodeDiv); if (remoteDiff) { if (remoteDiff.deleted[node.z]) { @@ -476,13 +489,12 @@ RED.diff = (function() { $(' deleted').appendTo(remoteNodeDiv); } else if (remoteDiff.changed[node.id]) { remoteNodeDiv.addClass("node-diff-node-changed"); - $(' changed').appendTo(remoteNodeDiv); + $(' changed').appendTo(remoteNodeDiv); } else { stats.remote.unchangedCount++; remoteNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(remoteNodeDiv); + $(' unchanged').appendTo(remoteNodeDiv); } - //createNode(node,def).appendTo(remoteNodeDiv); } } var localNode = { @@ -498,20 +510,34 @@ RED.diff = (function() { diff: remoteDiff } } - var currentConflictCount = stats.conflicts; - createNodePropertiesTable(def,node,localNode,remoteNode,stats).appendTo(div); - if (currentConflictCount !== stats.conflicts) { - $('').prependTo(localNodeDiv); - $('').prependTo(remoteNodeDiv); + createNodePropertiesTable(def,node,localNode,remoteNode).appendTo(div); + + var selectState = ""; + + if (conflicted) { + stats.conflicts++; + if (!localNodeDiv.hasClass("node-diff-empty")) { + $('').prependTo(localNodeDiv); + } + if (!remoteNodeDiv.hasClass("node-diff-empty")) { + $('').prependTo(remoteNodeDiv); + } + div.addClass("node-diff-node-entry-conflict"); + } else { + if (!localChanged) { + selectState = "remote"; + } else { + selectState = "local"; + } } + createNodeConflictRadioBoxes(node,div,localNodeDiv,remoteNodeDiv,false,!conflicted,selectState); row.click(function(evt) { - evt.preventDefault(); $(this).parent().toggleClass('collapsed'); }); return div; } - function createNodePropertiesTable(def,node,localNodeObj,remoteNodeObj,stats) { + function createNodePropertiesTable(def,node,localNodeObj,remoteNodeObj) { var localNode = localNodeObj.node; var remoteNode; if (remoteNodeObj) { @@ -519,9 +545,7 @@ RED.diff = (function() { } var nodePropertiesDiv = $("
                      ",{class:"node-diff-node-entry-properties"}); - var nodePropertiesTable = $("
                      position
                      ",{class:"node-diff-property-cell-label"}).html("position").appendTo(row); + localCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + if (localNode) { + localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged")); + $(''+(localChanged?'':'')+'').appendTo(localCell); + RED.utils.createObjectElement({x:localNode.x,y:localNode.y}).appendTo(localCell); + } else { + localCell.addClass("node-diff-empty"); + } + + if (remoteNode !== undefined) { + remoteCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged")); + if (remoteNode) { + $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); + RED.utils.createObjectElement({x:remoteNode.x,y:remoteNode.y}).appendTo(remoteCell); + } else { + remoteCell.addClass("node-diff-empty"); + } } } + // + localChanged = remoteChanged = conflict = false; if (node.hasOwnProperty('wires')) { - var localValue = JSON.stringify(node.wires); - var remoteValue = JSON.stringify(newNode.wires); - if (localValue !== remoteValue) { - row = $("
                      wires
                      ",{class:"node-diff-property-cell-label"}).html("wires").appendTo(row); + localCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + if (localNode) { + if (!conflict) { + localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged")); + $(''+(localChanged?'':'')+'').appendTo(localCell); + } else { + localCell.addClass("node-diff-node-conflict"); + $('').appendTo(localCell); + } + formatWireProperty(localNode.wires,localNodeObj.all).appendTo(localCell); + } else { + localCell.addClass("node-diff-empty"); } + if (remoteNode !== undefined) { + remoteCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + if (remoteNode) { + if (!conflict) { + remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged")); + $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); + } else { + remoteCell.addClass("node-diff-node-conflict"); + $('').appendTo(remoteCell); + } + formatWireProperty(remoteNode.wires,remoteNodeObj.all).appendTo(remoteCell); + } else { + remoteCell.addClass("node-diff-empty"); + } + } } var properties = Object.keys(node).filter(function(p) { return p!='z'&&p!='wires'&&p!=='x'&&p!=='y'&&p!=='id'&&p!=='type'&&(!def.defaults||!def.defaults.hasOwnProperty(p))}); if (def.defaults) { properties = properties.concat(Object.keys(def.defaults)); } properties.forEach(function(d) { - var localValue = JSON.stringify(node[d]); - var remoteValue = JSON.stringify(newNode[d]); - - if (remoteValue !== localValue) { - var formattedProperty = RED.utils.createObjectElement(node[d]); - var newFormattedProperty = RED.utils.createObjectElement(newNode[d]); - var row = $("
                      "+d+'
                      ",{class:"node-diff-property-cell-label"}).html(d).appendTo(row); + localCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + if (localNode) { + if (!conflict) { + localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged")); + $(''+(localChanged?'':'')+'').appendTo(localCell); + } else { + localCell.addClass("node-diff-node-conflict"); + $('').appendTo(localCell); + } + RED.utils.createObjectElement(localNode[d]).appendTo(localCell); + } else { + localCell.addClass("node-diff-empty"); + } + if (remoteNode !== undefined) { + remoteCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + if (remoteNode) { + if (!conflict) { + remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged")); + $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); + } else { + remoteCell.addClass("node-diff-node-conflict"); + $('').appendTo(remoteCell); + } + RED.utils.createObjectElement(remoteNode[d]).appendTo(remoteCell); + } else { + remoteCell.addClass("node-diff-empty"); + } + } + }); + + // row = $("
                      ").appendTo(row); + // var cell; + // if (localNode) { + // if (localNodeObj.diff.added[node.id]) { + // $(' added unchanged added unchanged
                      ").appendTo(nodePropertiesDiv); - //var nodePropertiesTable = $("
                      ").appendTo(nodePropertiesDiv); var row; var localCell, remoteCell; var currentValue, localValue, remoteValue; @@ -530,16 +554,8 @@ RED.diff = (function() { var localChanges = 0; var remoteChanges = 0; var conflict = false; - var conflicted = false; var status; - if (remoteNodeObj) { - if ( (remoteNodeObj.diff.changed[node.id] && localNodeObj.diff.deleted[node.id]) || - (remoteNodeObj.diff.deleted[node.id] && localNodeObj.diff.changed[node.id]) - ) { - conflicted = true; - } - } if (node.hasOwnProperty('x')) { if (localNode) { if (localNode.x !== node.x || localNode.y !== node.y) { @@ -553,29 +569,28 @@ RED.diff = (function() { remoteChanges++; } } - if ( (remoteChanged && localChanged && (localNode.x !== remoteNode.y || localNode.y !== remoteNode.x)) || + if ( (remoteChanged && localChanged && (localNode.x !== remoteNode.x || localNode.y !== remoteNode.y)) || (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) || (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id]) ) { - conflicted = true; conflict = true; } row = $("
                      ").appendTo(nodePropertiesTable); $("").appendTo(nodePropertiesTable); $("").appendTo(nodePropertiesTable); $("",{class:"node-diff-property-header"}).prependTo(nodePropertiesTable); - // $("').appendTo(row); - // } else if (localChanges > 0) { - // cell = $('').appendTo(row); - // if (conflicted) { - // $(' conflict ').appendTo(cell); - // } - // $(' changed').appendTo(cell); - // } else { - // $('').appendTo(row); - // } - // } else if (localNodeObj.diff.deleted[node.id]) { - // cell = $('').appendTo(row); - // if (conflicted) { - // $(' conflict ').appendTo(cell); - // } - // $(' deleted').appendTo(cell); - // } else { - // $('').appendTo(row); - // } - // if (remoteNode) { - // if (remoteNodeObj.diff.added[node.id]) { - // $('').appendTo(row); - // } else if (remoteChanges > 0) { - // cell = $('').appendTo(row); - // if (conflicted) { - // $(' conflict ').appendTo(cell); - // } - // $(' changed').appendTo(cell); - // } else { - // $('').appendTo(row); - // } - // } else if (remoteNodeObj.diff.deleted[node.id]) { - // cell = $('').appendTo(row); - // if (conflicted) { - // $(' conflict ').appendTo(cell); - // } - // $(' deleted').appendTo(cell); - // } else { - // $('').appendTo(row); - // } - if (conflicted) { - stats.conflicts++; - } return nodePropertiesDiv; } - function showLocalDiff() { - var nns = RED.nodes.createCompleteNodeSet(); - var originalFlow = RED.nodes.originalFlow(); - var diff = generateDiff(originalFlow,nns); - showDiff(diff); + function createNodeConflictRadioBoxes(node,row,localDiv,remoteDiv,propertiesTable,hide,state) { + var safeNodeId = "node-diff-selectbox-"+node.id.replace(/\./g,'-')+(propertiesTable?"-props":""); + var className = ""; + if (node.z||propertiesTable) { + className = "node-diff-selectbox-tab-"+(propertiesTable?node.id:node.z).replace(/\./g,'-'); + } + var titleRow = !propertiesTable && (node.type === 'tab' || node.type === 'subflow'); + var changeHandler = function(evt) { + var className; + if (node.type === undefined) { + // TODO: handle globals + } else if (titleRow) { + className = "node-diff-selectbox-tab-"+node.id.replace(/\./g,'-'); + $("."+className+"-"+this.value).prop('checked',true); + if (this.value === 'local') { + $("."+className+"-"+this.value).closest(".node-diff-node-entry").addClass("node-diff-select-local"); + $("."+className+"-"+this.value).closest(".node-diff-node-entry").removeClass("node-diff-select-remote"); + } else { + $("."+className+"-"+this.value).closest(".node-diff-node-entry").removeClass("node-diff-select-local"); + $("."+className+"-"+this.value).closest(".node-diff-node-entry").addClass("node-diff-select-remote"); + } + } else { + // Individual node or properties table + var parentId = "node-diff-selectbox-"+(propertiesTable?node.id:node.z).replace(/\./g,'-'); + $('#'+parentId+"-local").prop('checked',false); + $('#'+parentId+"-remote").prop('checked',false); + var titleRowDiv = $('#'+parentId+"-local").closest(".node-diff-tab").find(".node-diff-tab-title"); + titleRowDiv.removeClass("node-diff-select-local"); + titleRowDiv.removeClass("node-diff-select-remote"); + } + if (this.value === 'local') { + row.removeClass("node-diff-select-remote"); + row.addClass("node-diff-select-local"); + } else if (this.value === 'remote') { + row.addClass("node-diff-select-remote"); + row.removeClass("node-diff-select-local"); + } + refreshConflictHeader(); + } + + var localSelectDiv = $('").appendTo(nodePropertiesTable); + $("
                      ",{class:"node-diff-property-cell-label"}).html("position").appendTo(row); - localCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + localCell = $("",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row); if (localNode) { localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged")); - $(''+(localChanged?'':'')+'').appendTo(localCell); + $(''+(localChanged?'':'')+'').appendTo(localCell); RED.utils.createObjectElement({x:localNode.x,y:localNode.y}).appendTo(localCell); } else { localCell.addClass("node-diff-empty"); } if (remoteNode !== undefined) { - remoteCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + remoteCell = $("",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row); remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged")); if (remoteNode) { - $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); + $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); RED.utils.createObjectElement({x:remoteNode.x,y:remoteNode.y}).appendTo(remoteCell); } else { remoteCell.addClass("node-diff-empty"); @@ -604,16 +619,15 @@ RED.diff = (function() { (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) || (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id]) ){ - conflicted = true; conflict = true; } row = $("
                      ",{class:"node-diff-property-cell-label"}).html("wires").appendTo(row); - localCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + localCell = $("",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row); if (localNode) { if (!conflict) { localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged")); - $(''+(localChanged?'':'')+'').appendTo(localCell); + $(''+(localChanged?'':'')+'').appendTo(localCell); } else { localCell.addClass("node-diff-node-conflict"); $('').appendTo(localCell); @@ -624,11 +638,11 @@ RED.diff = (function() { } if (remoteNode !== undefined) { - remoteCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + remoteCell = $("",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row); if (remoteNode) { if (!conflict) { remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged")); - $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); + $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); } else { remoteCell.addClass("node-diff-node-conflict"); $('').appendTo(remoteCell); @@ -667,17 +681,16 @@ RED.diff = (function() { (!localChanged && remoteChanged && localNodeObj.diff.deleted[node.id]) || (localChanged && !remoteChanged && remoteNodeObj.diff.deleted[node.id]) ){ - conflicted = true; conflict = true; } row = $("
                      ",{class:"node-diff-property-cell-label"}).html(d).appendTo(row); - localCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + localCell = $("",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row); if (localNode) { if (!conflict) { localCell.addClass("node-diff-node-"+(localChanged?"changed":"unchanged")); - $(''+(localChanged?'':'')+'').appendTo(localCell); + $(''+(localChanged?'':'')+'').appendTo(localCell); } else { localCell.addClass("node-diff-node-conflict"); $('').appendTo(localCell); @@ -687,11 +700,11 @@ RED.diff = (function() { localCell.addClass("node-diff-empty"); } if (remoteNode !== undefined) { - remoteCell = $("",{class:"node-diff-property-cell"}).appendTo(row); + remoteCell = $("",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row); if (remoteNode) { if (!conflict) { remoteCell.addClass("node-diff-node-"+(remoteChanged?"changed":"unchanged")); - $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); + $(''+(remoteChanged?'':'')+'').appendTo(remoteCell); } else { remoteCell.addClass("node-diff-node-conflict"); $('').appendTo(remoteCell); @@ -702,64 +715,85 @@ RED.diff = (function() { } } }); - - // row = $("
                      ").appendTo(row); - // var cell; - // if (localNode) { - // if (localNodeObj.diff.added[node.id]) { - // $(' added unchanged added unchanged
                      ",{class:"node-diff-property-cell-label"}).html("id").appendTo(row); + localCell = $("",{class:"node-diff-property-cell node-diff-node-local"}).appendTo(row); + if (localNode) { + localCell.addClass("node-diff-node-unchanged"); + $('').appendTo(localCell); + RED.utils.createObjectElement(localNode.id).appendTo(localCell); + } else { + localCell.addClass("node-diff-empty"); + } + if (remoteNode !== undefined) { + remoteCell = $("",{class:"node-diff-property-cell node-diff-node-remote"}).appendTo(row); + remoteCell.addClass("node-diff-node-unchanged"); + if (remoteNode) { + $('').appendTo(remoteCell); + RED.utils.createObjectElement(remoteNode.id).appendTo(remoteCell); + } else { + remoteCell.addClass("node-diff-empty"); + } + } + + if (node.hasOwnProperty('x')) { if (localNode) { if (localNode.x !== node.x || localNode.y !== node.y) { @@ -807,7 +874,9 @@ RED.diff = (function() { var localDiff = generateDiff(originalFlow,localFlow); var remoteDiff = generateDiff(originalFlow,remoteFlow); var conflicts = identifyConflicts(localDiff,remoteDiff); - console.log(conflicts); + console.log(localDiff.moved); + console.log(remoteDiff.moved); + callback({ localDiff:localDiff, remoteDiff:remoteDiff, @@ -873,6 +942,7 @@ RED.diff = (function() { var added = {}; var deleted = {}; var changed = {}; + var moved = {}; Object.keys(currentConfig.all).forEach(function(id) { var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id); @@ -880,6 +950,10 @@ RED.diff = (function() { deleted[id] = true; } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) { changed[id] = true; + + if (currentConfig.all[id].z !== newConfig.all[id].z) { + moved[id] = true; + } } }); Object.keys(newConfig.all).forEach(function(id) { @@ -894,6 +968,7 @@ RED.diff = (function() { added: added, deleted: deleted, changed: changed, + moved: moved } } function identifyConflicts(localDiff,remoteDiff) { @@ -1081,6 +1156,7 @@ RED.diff = (function() { } for (subflowId in newConfig.subflows) { if (newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { + seenTabs[subflowId] = true; el = { conflicts: conflicts, diff: localDiff, @@ -1138,6 +1214,7 @@ RED.diff = (function() { var toAdd = []; var toRemove = []; var toMerge = []; + var toMove = []; var id; for (id in remoteDiff.added) { @@ -1153,11 +1230,19 @@ RED.diff = (function() { } else { if (remoteDiff.deleted[id]) { toRemove.push(id); + } else if (remoteDiff.moved[id]) { + toRemove.push(id); + toAdd.push(remoteDiff.newConfig.all[id]); } else if (remoteDiff.changed[id]) { if (localDiff.deleted[id]) { toAdd.push(remoteDiff.newConfig.all[id]); } else { - toMerge.push(remoteDiff.newConfig.all[id]); + if (node.type !== 'tab' && node.type !== 'subflow') { + toRemove.push(id); + toAdd.push(remoteDiff.newConfig.all[id]); + } else { + toMerge.push(remoteDiff.newConfig.all[id]); + } } } } @@ -1166,8 +1251,8 @@ RED.diff = (function() { console.log("adding",toAdd); console.log("deleting",toRemove); console.log("replacing",toMerge); + console.log("moving",toMove); - var imported = RED.nodes.import(toAdd); var removed = []; toRemove.forEach(function(id) { var node = currentConfig.all[id]; @@ -1186,6 +1271,18 @@ RED.diff = (function() { } } }); + // Need to refresh the view so when we add back nodes with the same id, + // they get properly initialised in the view. + RED.view.redraw(true); + + var imported = RED.nodes.import(toAdd); + + + // toMove.forEach(function(newNode) { + // var currentNode; + // currentNode = RED.nodes.node(newNode.id); + // currentNode.z = newNode.z; + // }); toMerge.forEach(function(newNode) { var currentNode; console.log("merging node",newNode.id); @@ -1209,7 +1306,7 @@ RED.diff = (function() { currentNode = RED.nodes.workspace(newNode.id); currentNode.label = newNode.label; } - }) + }); RED.view.redraw(true); diff --git a/editor/sass/diff.scss b/editor/sass/diff.scss index 9da7a0d26..0c21a15a8 100644 --- a/editor/sass/diff.scss +++ b/editor/sass/diff.scss @@ -266,6 +266,13 @@ color: #009900; } } +.node-diff-node-moved { + //background: #eefaee; + .node-diff-status { + color: #3f81b3; + } +} + .node-diff-node-changed { //background: #fff2ca; .node-diff-status { From 7970c9dbe5ad2c6b0714c62f970133a1f07c4711 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 1 Jan 2017 00:18:39 +0000 Subject: [PATCH 38/44] Merge changes by reimporting changed node config --- editor/js/history.js | 547 +++++++++++++++++++----------------- editor/js/nodes.js | 49 +++- editor/js/ui/common/tabs.js | 3 + editor/js/ui/deploy.js | 23 +- editor/js/ui/diff.js | 270 ++++++++---------- 5 files changed, 456 insertions(+), 436 deletions(-) diff --git a/editor/js/history.js b/editor/js/history.js index c22c71912..169d05b3f 100644 --- a/editor/js/history.js +++ b/editor/js/history.js @@ -16,6 +16,289 @@ RED.history = (function() { var undo_history = []; + function undoEvent(ev) { + var i; + var len; + var node; + var subflow; + var modifiedTabs = {}; + if (ev) { + if (ev.t == 'multi') { + len = ev.events.length; + for (i=len-1;i>=0;i--) { + undoEvent(ev.events[i]); + } + } else if (ev.t == 'replace') { + RED.nodes.clear(); + var imported = RED.nodes.import(ev.config); + imported[0].forEach(function(n) { + if (ev.changed[n.id]) { + n.changed = true; + } + }) + + RED.nodes.version(ev.rev); + } else if (ev.t == 'add') { + if (ev.nodes) { + for (i=0;i 0) { + subflow = RED.nodes.subflow(ev.subflowInputs[0].z); + subflow.in.push(ev.subflowInputs[0]); + subflow.in[0].dirty = true; + } + if (ev.subflowOutputs && ev.subflowOutputs.length > 0) { + subflow = RED.nodes.subflow(ev.subflowOutputs[0].z); + ev.subflowOutputs.sort(function(a,b) { return a.i-b.i}); + for (i=0;i= output.i) { + l.sourcePort++; + } + } + }); + } + } + if (ev.subflow && ev.subflow.hasOwnProperty('instances')) { + ev.subflow.instances.forEach(function(n) { + var node = RED.nodes.node(n.id); + if (node) { + node.changed = n.changed; + node.dirty = true; + } + }); + } + if (subflow) { + RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) { + n.inputs = subflow.in.length; + n.outputs = subflow.out.length; + while (n.outputs > n.ports.length) { + n.ports.push(n.ports.length); + } + n.resize = true; + n.dirty = true; + }); + } + if (ev.nodes) { + for (i=0;i ev.subflow.inputCount) { + ev.node.in.splice(ev.subflow.inputCount); + } else if (ev.subflow.inputs.length > 0) { + ev.node.in = ev.node.in.concat(ev.subflow.inputs); + } + } + if (ev.subflow.hasOwnProperty('outputCount')) { + if (ev.node.out.length > ev.subflow.outputCount) { + ev.node.out.splice(ev.subflow.outputCount); + } else if (ev.subflow.outputs.length > 0) { + ev.node.out = ev.node.out.concat(ev.subflow.outputs); + } + } + if (ev.subflow.hasOwnProperty('instances')) { + ev.subflow.instances.forEach(function(n) { + var node = RED.nodes.node(n.id); + if (node) { + node.changed = n.changed; + node.dirty = true; + } + }); + } + RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) { + n.inputs = ev.node.in.length; + n.outputs = ev.node.out.length; + RED.editor.updateNodeProperties(n); + }); + } else { + var outputMap; + if (ev.outputMap) { + outputMap = {}; + for (var port in ev.outputMap) { + if (ev.outputMap.hasOwnProperty(port) && ev.outputMap[port] !== -1) { + outputMap[ev.outputMap[port]] = port; + } + } + } + RED.editor.updateNodeProperties(ev.node,outputMap); + RED.editor.validateNode(ev.node); + } + if (ev.links) { + for (i=0;i 0) { - subflow = RED.nodes.subflow(ev.subflowInputs[0].z); - subflow.in.push(ev.subflowInputs[0]); - subflow.in[0].dirty = true; - } - if (ev.subflowOutputs && ev.subflowOutputs.length > 0) { - subflow = RED.nodes.subflow(ev.subflowOutputs[0].z); - ev.subflowOutputs.sort(function(a,b) { return a.i-b.i}); - for (i=0;i= output.i) { - l.sourcePort++; - } - } - }); - } - } - if (ev.subflow && ev.subflow.hasOwnProperty('instances')) { - ev.subflow.instances.forEach(function(n) { - var node = RED.nodes.node(n.id); - if (node) { - node.changed = n.changed; - node.dirty = true; - } - }); - } - if (subflow) { - RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) { - n.inputs = subflow.in.length; - n.outputs = subflow.out.length; - while (n.outputs > n.ports.length) { - n.ports.push(n.ports.length); - } - n.resize = true; - n.dirty = true; - }); - } - if (ev.nodes) { - for (i=0;i ev.subflow.inputCount) { - ev.node.in.splice(ev.subflow.inputCount); - } else if (ev.subflow.inputs.length > 0) { - ev.node.in = ev.node.in.concat(ev.subflow.inputs); - } - } - if (ev.subflow.hasOwnProperty('outputCount')) { - if (ev.node.out.length > ev.subflow.outputCount) { - ev.node.out.splice(ev.subflow.outputCount); - } else if (ev.subflow.outputs.length > 0) { - ev.node.out = ev.node.out.concat(ev.subflow.outputs); - } - } - if (ev.subflow.hasOwnProperty('instances')) { - ev.subflow.instances.forEach(function(n) { - var node = RED.nodes.node(n.id); - if (node) { - node.changed = n.changed; - node.dirty = true; - } - }); - } - RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) { - n.inputs = ev.node.in.length; - n.outputs = ev.node.out.length; - RED.editor.updateNodeProperties(n); - }); - } else { - var outputMap; - if (ev.outputMap) { - outputMap = {}; - for (var port in ev.outputMap) { - if (ev.outputMap.hasOwnProperty(port) && ev.outputMap[port] !== -1) { - outputMap[ev.outputMap[port]] = port; - } - } - } - RED.editor.updateNodeProperties(ev.node,outputMap); - RED.editor.validateNode(ev.node); - } - if (ev.links) { - for (i=0;i')); - // $("#node-dialog-confirm-deploy-review .ui-button-text").css("opacity",0.4); - // $("#node-dialog-confirm-deploy-review").attr("disabled",true).addClass("disabled"); - // $.ajax({ - // headers: { - // "Accept":"application/json", - // }, - // cache: false, - // url: 'flows', - // success: function(nodes) { - // var newNodes = nodes.flows; - // var newRevision = nodes.rev; - // generateDiff(currentNodes,newNodes); - // $("#node-dialog-confirm-deploy-review").attr("disabled",false).removeClass("disabled"); - // $("#node-dialog-confirm-deploy-review img").remove(); - // $("#node-dialog-confirm-deploy-review .ui-button-text").css("opacity",1); - // } - // }); } function save(skipValidation,force) { diff --git a/editor/js/ui/diff.js b/editor/js/ui/diff.js index 28a26f78a..8bfd8364c 100644 --- a/editor/js/ui/diff.js +++ b/editor/js/ui/diff.js @@ -4,10 +4,10 @@ RED.diff = (function() { function init() { - RED.actions.add("core:show-current-diff",showLocalDiff); + // RED.actions.add("core:show-current-diff",showLocalDiff); RED.actions.add("core:show-remote-diff",showRemoteDiff); - RED.keyboard.add("*","ctrl-shift-l","core:show-current-diff"); + // RED.keyboard.add("*","ctrl-shift-l","core:show-current-diff"); RED.keyboard.add("*","ctrl-shift-r","core:show-remote-diff"); @@ -74,6 +74,8 @@ RED.diff = (function() { var remoteDiff = object.remoteDiff; var tab = object.tab.n; var def = object.def; + var conflicts = currentDiff.conflicts; + var tabDiv = $('
                      ',{class:"node-diff-tab"}).appendTo(container); tabDiv.addClass('collapsed'); var titleRow = $('
                      ',{class:"node-diff-tab-title"}).appendTo(tabDiv); @@ -132,6 +134,7 @@ RED.diff = (function() { var originalNodeDiv = $("
                      ",{class:"node-diff-node-entry-cell"}).appendTo(row); var localNodeDiv = $("
                      ",{class:"node-diff-node-entry-cell node-diff-node-local"}).appendTo(row); var localChanged = false; + var remoteChanged = false; if (!localDiff.newConfig.all[tab.id]) { localNodeDiv.addClass("node-diff-empty"); @@ -153,11 +156,16 @@ RED.diff = (function() { remoteNodeDiv = $("
                      ",{class:"node-diff-node-entry-cell node-diff-node-remote"}).appendTo(row); if (!remoteDiff.newConfig.all[tab.id]) { remoteNodeDiv.addClass("node-diff-empty"); + if (remoteDiff.deleted[tab.id]) { + remoteChanged = true; + } } else if (remoteDiff.added[tab.id]) { remoteNodeDiv.addClass("node-diff-node-added"); + remoteChanged = true; $(' added').appendTo(remoteNodeDiv); } else if (remoteDiff.changed[tab.id]) { remoteNodeDiv.addClass("node-diff-node-changed"); + remoteChanged = true; $(' changed').appendTo(remoteNodeDiv); } else { remoteNodeDiv.addClass("node-diff-node-unchanged"); @@ -172,9 +180,9 @@ RED.diff = (function() { $(this).parent().toggleClass('collapsed'); }); - createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,object.conflicts).appendTo(div); + createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,conflicts).appendTo(div); selectState = ""; - if (object.conflicts[tab.id]) { + if (conflicts[tab.id]) { flowStats.conflicts++; if (!localNodeDiv.hasClass("node-diff-empty")) { @@ -185,14 +193,10 @@ RED.diff = (function() { } div.addClass("node-diff-node-entry-conflict"); } else { - if (!localChanged) { - selectState = "remote"; - } else { - selectState = "local"; - } + selectState = currentDiff.resolutions[tab.id]; } - - createNodeConflictRadioBoxes(tab,div,localNodeDiv,remoteNodeDiv,true,!object.conflicts[tab.id],selectState); + // Tab properties row + createNodeConflictRadioBoxes(tab,div,localNodeDiv,remoteNodeDiv,true,!conflicts[tab.id],selectState); } } // var stats = $('',{class:"node-diff-tab-stats"}).appendTo(titleRow); @@ -201,14 +205,14 @@ RED.diff = (function() { var seen = {}; object.tab.nodes.forEach(function(node) { seen[node.id] = true; - createNodeDiffRow(node,flowStats,localDiff,remoteDiff,object.conflicts[node.id]).appendTo(nodesDiv) + createNodeDiffRow(node,flowStats).appendTo(nodesDiv) }); if (object.newTab) { localNodeCount = object.newTab.nodes.length; object.newTab.nodes.forEach(function(node) { if (!seen[node.id]) { seen[node.id] = true; - createNodeDiffRow(node,flowStats,localDiff,remoteDiff,object.conflicts[node.id]).appendTo(nodesDiv) + createNodeDiffRow(node,flowStats).appendTo(nodesDiv) } }); } @@ -216,7 +220,7 @@ RED.diff = (function() { remoteNodeCount = object.remoteTab.nodes.length; object.remoteTab.nodes.forEach(function(node) { if (!seen[node.id]) { - createNodeDiffRow(node,flowStats,localDiff,remoteDiff,object.conflicts[node.id]).appendTo(nodesDiv) + createNodeDiffRow(node,flowStats).appendTo(nodesDiv) } }); } @@ -304,12 +308,16 @@ RED.diff = (function() { } else { remoteCell.addClass("node-diff-empty"); } + selectState = ""; if (flowStats.conflicts > 0) { titleRow.addClass("node-diff-node-entry-conflict"); + } else { + selectState = currentDiff.resolutions[tab.id]; } if (tab.id) { - selectState = ""; - createNodeConflictRadioBoxes(tab,titleRow,localCell,remoteCell, false, !(flowStats.conflicts > 0 &&(localDiff.deleted[tab.id] || remoteDiff.deleted[tab.id])),selectState); + var hide = !(flowStats.conflicts > 0 &&(localDiff.deleted[tab.id] || remoteDiff.deleted[tab.id])); + // Tab parent row + createNodeConflictRadioBoxes(tab,titleRow,localCell,remoteCell, false, hide, selectState); } } @@ -383,7 +391,11 @@ RED.diff = (function() { $('',{class:"node-diff-node-label"}).html(nodeLabel).appendTo(contentDiv); return nodeTitleDiv; } - function createNodeDiffRow(node,stats,localDiff,remoteDiff,conflicted) { + function createNodeDiffRow(node,stats) { + var localDiff = currentDiff.localDiff; + var remoteDiff = currentDiff.remoteDiff; + var conflicted = currentDiff.conflicts[node.id]; + var hasChanges = false; // exists in original and local/remote but with changes var unChanged = true; // existing in original,local,remote unchanged var localChanged = false; @@ -450,6 +462,7 @@ RED.diff = (function() { remoteNodeDiv.addClass("node-diff-node-unchanged"); $(' unchanged').appendTo(remoteNodeDiv); } + div.addClass("node-diff-node-unchanged"); } else if (localDiff.added[node.id]) { localNodeDiv.addClass("node-diff-node-added"); if (remoteNodeDiv) { @@ -569,12 +582,9 @@ RED.diff = (function() { } div.addClass("node-diff-node-entry-conflict"); } else { - if (!localChanged) { - selectState = "remote"; - } else { - selectState = "local"; - } + selectState = currentDiff.resolutions[node.id]; } + // Node row createNodeConflictRadioBoxes(node,div,localNodeDiv,remoteNodeDiv,false,!conflicted,selectState); row.click(function(evt) { $(this).parent().toggleClass('collapsed'); @@ -840,17 +850,14 @@ RED.diff = (function() { } function refreshConflictHeader() { - currentDiff.resolutions = {}; var resolutionCount = 0; $(".node-diff-selectbox>input:checked").each(function() { if (currentDiff.conflicts[$(this).data('node-id')]) { resolutionCount++; } currentDiff.resolutions[$(this).data('node-id')] = $(this).val(); - // console.log($(this).data('node-id'),$(this).val()) }) var conflictCount = Object.keys(currentDiff.conflicts).length; - // console.log(resolutionCount,"of",conflictCount,"conflicts resolve"); if (conflictCount - resolutionCount === 0) { $("#node-diff-toolbar-resolved-conflicts").html(' '+RED._("diff.unresolvedCount",{count:conflictCount - resolutionCount})); } else { @@ -873,30 +880,23 @@ RED.diff = (function() { var remoteFlow = nodes.flows; var localDiff = generateDiff(originalFlow,localFlow); var remoteDiff = generateDiff(originalFlow,remoteFlow); - var conflicts = identifyConflicts(localDiff,remoteDiff); - console.log(localDiff.moved); - console.log(remoteDiff.moved); - - callback({ - localDiff:localDiff, - remoteDiff:remoteDiff, - conflicts: conflicts - }); + remoteDiff.rev = nodes.rev; + callback(resolveDiffs(localDiff,remoteDiff)) } }); } - function showLocalDiff() { - var nns = RED.nodes.createCompleteNodeSet(); - var originalFlow = RED.nodes.originalFlow(); - var diff = generateDiff(originalFlow,nns); - showDiff(diff); - } + // function showLocalDiff() { + // var nns = RED.nodes.createCompleteNodeSet(); + // var originalFlow = RED.nodes.originalFlow(); + // var diff = generateDiff(originalFlow,nns); + // showDiff(diff); + // } function showRemoteDiff(diff) { if (diff === undefined) { getRemoteDiff(showRemoteDiff); } else { - showDiff(diff.localDiff,diff.remoteDiff,diff.conflicts); + showDiff(diff); } } function parseNodes(nodeList) { @@ -971,11 +971,18 @@ RED.diff = (function() { moved: moved } } - function identifyConflicts(localDiff,remoteDiff) { - var seen = {}; + function resolveDiffs(localDiff,remoteDiff) { var conflicted = {}; - var id,node; + var resolutions = {}; + var diff = { + localDiff: localDiff, + remoteDiff: remoteDiff, + conflicts: conflicted, + resolutions: resolutions + } + var seen = {}; + var id,node; for (id in localDiff.currentConfig.all) { if (localDiff.currentConfig.all.hasOwnProperty(id)) { seen[id] = true; @@ -990,6 +997,13 @@ RED.diff = (function() { conflicted[id] = true; } } + if (!conflicted[id]) { + if (remoteDiff.added[id]||remoteDiff.changed[id]||remoteDiff.deleted[id]) { + resolutions[id] = 'remote'; + } else { + resolutions[id] = 'local'; + } + } } } for (id in localDiff.added) { @@ -998,6 +1012,8 @@ RED.diff = (function() { if (remoteDiff.deleted[node.z]) { conflicted[id] = true; // conflicted[node.z] = true; + } else { + resolutions[id] = 'local'; } } } @@ -1007,13 +1023,20 @@ RED.diff = (function() { if (localDiff.deleted[node.z]) { conflicted[id] = true; // conflicted[node.z] = true; + } else { + resolutions[id] = 'remote'; } } } + // console.log(diff.resolutions); // console.log(conflicted); - return conflicted; + return diff; } - function showDiff(localDiff,remoteDiff,conflicts) { + function showDiff(diff) { + var localDiff = diff.localDiff; + var remoteDiff = diff.remoteDiff; + var conflicts = diff.conflicts; + currentDiff = diff; var list = $("#node-dialog-view-diff-diff"); list.editableList('empty'); @@ -1027,12 +1050,6 @@ RED.diff = (function() { } else { $("#node-diff-view-diff-merge").hide(); } - currentDiff = { - localDiff: localDiff, - remoteDiff: remoteDiff, - conflicts: conflicts, - resolutions: {} - } refreshConflictHeader(); $("#node-dialog-view-diff-headers").empty(); @@ -1044,7 +1061,6 @@ RED.diff = (function() { conflicts = conflicts || {}; var el = { - conflicts: conflicts, diff: localDiff, def: { category: 'config', @@ -1080,7 +1096,6 @@ RED.diff = (function() { currentConfig.tabOrder.forEach(function(tabId) { var tab = currentConfig.tabs[tabId]; var el = { - conflicts: conflicts, diff: localDiff, def: {}, tab:tab @@ -1100,7 +1115,6 @@ RED.diff = (function() { seenTabs[tabId] = true; var tab = newConfig.tabs[tabId]; var el = { - conflicts: conflicts, diff: localDiff, def: {}, tab:tab, @@ -1118,7 +1132,6 @@ RED.diff = (function() { var tab = remoteDiff.newConfig.tabs[tabId]; // TODO how to recognise this is a remotely added flow var el = { - conflicts: conflicts, diff: localDiff, remoteDiff: remoteDiff, def: {}, @@ -1134,7 +1147,6 @@ RED.diff = (function() { if (currentConfig.subflows.hasOwnProperty(subflowId)) { seenTabs[subflowId] = true; el = { - conflicts: conflicts, diff: localDiff, def: { defaults:{}, @@ -1158,7 +1170,6 @@ RED.diff = (function() { if (newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { seenTabs[subflowId] = true; el = { - conflicts: conflicts, diff: localDiff, def: { defaults:{}, @@ -1178,9 +1189,7 @@ RED.diff = (function() { if (remoteDiff !== undefined) { for (subflowId in remoteDiff.newConfig.subflows) { if (remoteDiff.newConfig.subflows.hasOwnProperty(subflowId) && !seenTabs[subflowId]) { - // TODO how to recognise this is a remotely added flow el = { - conflicts: conflicts, diff: localDiff, remoteDiff: remoteDiff, def: { @@ -1205,109 +1214,77 @@ RED.diff = (function() { } function mergeDiff(diff) { var currentConfig = diff.localDiff.currentConfig; - var localDiff = diff.localDiff; var remoteDiff = diff.remoteDiff; var conflicts = diff.conflicts; var resolutions = diff.resolutions; - - var toAdd = []; - var toRemove = []; - var toMerge = []; - var toMove = []; - var id; - for (id in remoteDiff.added) { - if (remoteDiff.added.hasOwnProperty(id) && !localDiff.added.hasOwnProperty(id)) { - toAdd.push(remoteDiff.newConfig.all[id]); + + for (id in conflicts) { + if (conflicts.hasOwnProperty(id)) { + if (!resolutions.hasOwnProperty(id)) { + console.log(diff); + throw new Error("No resolution for conflict on node",id); + } } } - for (id in currentConfig.all) { - if (currentConfig.all.hasOwnProperty(id)) { - var node = currentConfig.all[id]; + + var newConfig = []; + var node; + var nodeChangedStates = {}; + var localChangedStates = {}; + for (id in localDiff.newConfig.all) { + if (localDiff.newConfig.all.hasOwnProperty(id)) { + node = RED.nodes.node(id); if (resolutions[id] === 'local') { - // use local - nothing to change then - } else { - if (remoteDiff.deleted[id]) { - toRemove.push(id); - } else if (remoteDiff.moved[id]) { - toRemove.push(id); - toAdd.push(remoteDiff.newConfig.all[id]); - } else if (remoteDiff.changed[id]) { - if (localDiff.deleted[id]) { - toAdd.push(remoteDiff.newConfig.all[id]); - } else { - if (node.type !== 'tab' && node.type !== 'subflow') { - toRemove.push(id); - toAdd.push(remoteDiff.newConfig.all[id]); - } else { - toMerge.push(remoteDiff.newConfig.all[id]); - } - } + if (node) { + nodeChangedStates[id] = node.changed; } + newConfig.push(localDiff.newConfig.all[id]); + } else if (resolutions[id] === 'remote') { + if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) { + if (node) { + nodeChangedStates[id] = node.changed; + } + localChangedStates[id] = true; + newConfig.push(remoteDiff.newConfig.all[id]); + } + } else { + console.log("Unresolved",id) } } } - console.log("adding",toAdd); - console.log("deleting",toRemove); - console.log("replacing",toMerge); - console.log("moving",toMove); - - var removed = []; - toRemove.forEach(function(id) { - var node = currentConfig.all[id]; - if (node.type === 'tab') { - console.log("removing tab",id); - RED.workspaces.remove(node); - removed.push(RED.nodes.removeWorkspace(id)); - } else if (node.type === 'subflow') { - console.log("removing subflow",id); - removed.push(RED.subflow.removeSubflow(id)); - } else { - console.log("removing node",id); - var r = RED.nodes.remove(id); - if (r.links.length > 0 || r.nodes.length > 0) { - removed.push(r); + for (id in remoteDiff.added) { + if (remoteDiff.added.hasOwnProperty(id)) { + node = RED.nodes.node(id); + if (node) { + nodeChangedStates[id] = node.changed; + } + if (!localDiff.added.hasOwnProperty(id)) { + localChangedStates[id] = true; + newConfig.push(remoteDiff.newConfig.all[id]); } } - }); - // Need to refresh the view so when we add back nodes with the same id, - // they get properly initialised in the view. - RED.view.redraw(true); + } + var historyEvent = { + t:"replace", + config: RED.nodes.createCompleteNodeSet(), + changed: nodeChangedStates, + dirty: RED.nodes.dirty(), + rev: RED.nodes.version() + } - var imported = RED.nodes.import(toAdd); + RED.history.push(historyEvent); - - // toMove.forEach(function(newNode) { - // var currentNode; - // currentNode = RED.nodes.node(newNode.id); - // currentNode.z = newNode.z; - // }); - toMerge.forEach(function(newNode) { - var currentNode; - console.log("merging node",newNode.id); - if (newNode.type !== 'tab' && newNode.type !== 'subflow') { - currentNode = RED.nodes.node(newNode.id); - var def = RED.nodes.getType(currentNode.type); - if (currentNode.hasOwnProperty('x')) { - currentNode.x = newNode.x; - currentNode.y = newNode.y; - } - for (var d in def.defaults) { - if (def.defaults.hasOwnProperty(d)) { - currentNode[d] = newNode[d]; - } - } - var removedLinks = RED.editor.updateNodeProperties(currentNode); - if (removedLinks.length > 0) { - removed.push({links:removedLinks}); - } - } else if (newNode.type === 'tab') { - currentNode = RED.nodes.workspace(newNode.id); - currentNode.label = newNode.label; + RED.nodes.clear(); + var imported = RED.nodes.import(newConfig); + imported[0].forEach(function(n) { + if (nodeChangedStates[n.id] || localChangedStates[n.id]) { + n.changed = true; } - }); + }) + RED.nodes.version(remoteDiff.rev); RED.view.redraw(true); RED.palette.refresh(); @@ -1318,6 +1295,7 @@ RED.diff = (function() { return { init: init, getRemoteDiff: getRemoteDiff, - showRemoteDiff: showRemoteDiff + showRemoteDiff: showRemoteDiff, + mergeDiff: mergeDiff } })(); From 18a519f9ed03465913255122bbd32371a9582768 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 1 Jan 2017 11:41:03 +0000 Subject: [PATCH 39/44] Remove node 0.10 from travis config --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3d064cb65..04d4ab3ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,6 @@ node_js: - "7" - "6" - "4" - - "0.10" script: - istanbul cover ./node_modules/.bin/grunt --report lcovonly && istanbul report text && ( cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js || true ) && rm -rf coverage before_script: From 061cc908a78c308b0580e8ec494055491b559781 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 1 Jan 2017 21:59:09 +0000 Subject: [PATCH 40/44] Hide common entries when filtering typeSearch --- editor/js/ui/typeSearch.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/editor/js/ui/typeSearch.js b/editor/js/ui/typeSearch.js index 0105d1799..60df8f4e0 100644 --- a/editor/js/ui/typeSearch.js +++ b/editor/js/ui/typeSearch.js @@ -95,7 +95,7 @@ RED.typeSearch = (function() { if (activeFilter === "" ) { return true; } - if (data.recent) { + if (data.recent || data.common) { return false; } return (activeFilter==="")||(data.index.indexOf(activeFilter) > -1); @@ -266,6 +266,7 @@ RED.typeSearch = (function() { for(i=0;i Date: Sun, 1 Jan 2017 22:14:33 +0000 Subject: [PATCH 41/44] NLS type search --- editor/js/ui/typeSearch.js | 2 +- red/api/locales/en-US/editor.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/editor/js/ui/typeSearch.js b/editor/js/ui/typeSearch.js index 60df8f4e0..f949890b8 100644 --- a/editor/js/ui/typeSearch.js +++ b/editor/js/ui/typeSearch.js @@ -46,7 +46,7 @@ RED.typeSearch = (function() { //shade = $('
                      ',{class:"red-ui-type-search-shade"}).appendTo("#main-container"); dialog = $("
                      ",{id:"red-ui-type-search",class:"red-ui-search red-ui-type-search"}).appendTo("#main-container"); var searchDiv = $("
                      ",{class:"red-ui-search-container"}).appendTo(dialog); - searchInput = $('').appendTo(searchDiv).searchBox({ + searchInput = $('').attr("placeholder",RED._("search.addNode")).appendTo(searchDiv).searchBox({ delay: 50, change: function() { search($(this).val()); diff --git a/red/api/locales/en-US/editor.json b/red/api/locales/en-US/editor.json index a7c265e98..70abdc70a 100644 --- a/red/api/locales/en-US/editor.json +++ b/red/api/locales/en-US/editor.json @@ -325,7 +325,8 @@ "add": "add" }, "search": { - "empty": "No matches found" + "empty": "No matches found", + "addNode": "add a node..." }, "expressionEditor": { "functions": "Functions", From ffa628be2dfb8112a6cd3420d677267fb75c6be5 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 4 Jan 2017 16:46:36 +0000 Subject: [PATCH 42/44] Index all node properties for node search --- editor/js/ui/search.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/editor/js/ui/search.js b/editor/js/ui/search.js index 9f30dbd4f..519dcfa64 100644 --- a/editor/js/ui/search.js +++ b/editor/js/ui/search.js @@ -45,6 +45,9 @@ RED.search = (function() { var properties = ['id','type','name','label','info']; + if (n._def && n._def.defaults) { + properties = properties.concat(Object.keys(n._def.defaults)); + } for (var i=0;i Date: Wed, 4 Jan 2017 20:57:10 +0000 Subject: [PATCH 43/44] Fix diff node table layout for Safari --- editor/sass/diff.scss | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/editor/sass/diff.scss b/editor/sass/diff.scss index 0c21a15a8..57be425cc 100644 --- a/editor/sass/diff.scss +++ b/editor/sass/diff.scss @@ -130,9 +130,13 @@ } table { - border-collapse: collapse; - width: 100%; - table-layout:fixed; + border-collapse: collapse; + table-layout:fixed; + + // Fix for table-layout: fixed on safari: + max-width: none; + width: auto; + min-width: 100%; } td, th { border: 1px solid $secondary-border-color; From 83acb66f005acb16e6be8f6c6ec310e499d9709a Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 4 Jan 2017 22:02:35 +0000 Subject: [PATCH 44/44] NLS the diff dialog --- editor/js/ui/diff.js | 79 +++++++++++-------------------- red/api/locales/en-US/editor.json | 17 ++++++- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/editor/js/ui/diff.js b/editor/js/ui/diff.js index 8bfd8364c..553e73cd0 100644 --- a/editor/js/ui/diff.js +++ b/editor/js/ui/diff.js @@ -15,27 +15,7 @@ RED.diff = (function() { var toolbar = $('
                      '+ ' '+ - // ''+ - // 'previous'+ - // 'next'+ - // ''+ '
                      ').prependTo(dialog); - // - // toolbar.find(".node-diff-filter").click(function(evt) { - // evt.preventDefault(); - // if (!$(this).hasClass('selected')) { - // $(this).siblings().removeClass('selected'); - // $(this).addClass('selected'); - // } - // if ($(this).attr('id') === 'node-diff-filter-all') { - // diffList.find('.node-diff-node-unchanged').parent().removeClass('hide'); - // diffList.find('.node-diff-tab-unchanged').parent().removeClass('hide'); - // } else { - // diffList.find('.node-diff-node-unchanged').parent().addClass('hide'); - // diffList.find('.node-diff-tab-unchanged').parent().addClass('hide'); - // $(".node-diff-tab.node-diff-tab-unchanged").addClass("collapsed"); - // } - // }) $("#node-dialog-view-diff").dialog({ title: RED._('deploy.confirm.button.review'), @@ -141,14 +121,14 @@ RED.diff = (function() { } else if (localDiff.added[tab.id]) { localNodeDiv.addClass("node-diff-node-added"); localChanged = true; - $(' added').appendTo(localNodeDiv); + $(' ').appendTo(localNodeDiv); } else if (localDiff.changed[tab.id]) { localNodeDiv.addClass("node-diff-node-changed"); localChanged = true; - $(' changed').appendTo(localNodeDiv); + $(' ').appendTo(localNodeDiv); } else { localNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(localNodeDiv); + $(' ').appendTo(localNodeDiv); } var remoteNodeDiv; @@ -162,14 +142,14 @@ RED.diff = (function() { } else if (remoteDiff.added[tab.id]) { remoteNodeDiv.addClass("node-diff-node-added"); remoteChanged = true; - $(' added').appendTo(remoteNodeDiv); + $(' ').appendTo(remoteNodeDiv); } else if (remoteDiff.changed[tab.id]) { remoteNodeDiv.addClass("node-diff-node-changed"); remoteChanged = true; - $(' changed').appendTo(remoteNodeDiv); + $(' ').appendTo(remoteNodeDiv); } else { remoteNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(remoteNodeDiv); + $(' ').appendTo(remoteNodeDiv); } } $('').appendTo(originalNodeDiv); @@ -235,10 +215,10 @@ RED.diff = (function() { }) if (localDiff.deleted[tab.id]) { - $(' flow deleted').appendTo(localCell); + $(' ').appendTo(localCell); } else if (object.newTab) { if (localDiff.added[tab.id]) { - $(' flow added').appendTo(localCell); + $(' ').appendTo(localCell); } else { if (tab.id) { if (localDiff.changed[tab.id]) { @@ -248,7 +228,7 @@ RED.diff = (function() { } } var localStats = $('',{class:"node-diff-tab-stats"}).appendTo(localCell); - $(''+localNodeCount+' nodes').appendTo(localStats); + $('').html(RED._('diff.nodeCount',{count:localNodeCount})).appendTo(localStats); if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.deletedCount > 0) { $(' [ ').appendTo(localStats); @@ -274,10 +254,10 @@ RED.diff = (function() { if (remoteDiff) { if (remoteDiff.deleted[tab.id]) { - $(' flow deleted').appendTo(remoteCell); + $(' ').appendTo(remoteCell); } else if (object.remoteTab) { if (remoteDiff.added[tab.id]) { - $(' flow added').appendTo(remoteCell); + $(' ').appendTo(remoteCell); } else { if (tab.id) { if (remoteDiff.changed[tab.id]) { @@ -287,7 +267,7 @@ RED.diff = (function() { } } var remoteStats = $('',{class:"node-diff-tab-stats"}).appendTo(remoteCell); - $(''+remoteNodeCount+' nodes').appendTo(remoteStats); + $('').html(RED._('diff.nodeCount',{count:remoteNodeCount})).appendTo(remoteStats); if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.deletedCount > 0) { $(' [ ').appendTo(remoteStats); if (flowStats.conflicts > 0) { @@ -324,10 +304,7 @@ RED.diff = (function() { if (tabDiv.find(".node-diff-node-entry").length === 0) { tabDiv.addClass("node-diff-tab-empty"); } - // var statsInfo = ((flowStats.addedCount > 0)?''+flowStats.addedCount+' added ':'')+ - // ((flowStats.deletedCount > 0)?''+flowStats.deletedCount+' deleted ':'')+ - // ((flowStats.changedCount > 0)?''+flowStats.changedCount+' changed ':''); - // stats.html(statsInfo); + container.i18n(); } }); } @@ -456,11 +433,11 @@ RED.diff = (function() { stats.local.unchangedCount++; createNode(node,def).appendTo(originalNodeDiv); localNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(localNodeDiv); + $(' ').appendTo(localNodeDiv); if (remoteDiff) { stats.remote.unchangedCount++; remoteNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(remoteNodeDiv); + $(' ').appendTo(remoteNodeDiv); } div.addClass("node-diff-node-unchanged"); } else if (localDiff.added[node.id]) { @@ -468,12 +445,12 @@ RED.diff = (function() { if (remoteNodeDiv) { remoteNodeDiv.addClass("node-diff-empty"); } - $(' added').appendTo(localNodeDiv); + $(' ').appendTo(localNodeDiv); createNode(node,def).appendTo(originalNodeDiv); } else if (remoteDiff && remoteDiff.added[node.id]) { localNodeDiv.addClass("node-diff-empty"); remoteNodeDiv.addClass("node-diff-node-added"); - $(' added').appendTo(remoteNodeDiv); + $(' ').appendTo(remoteNodeDiv); createNode(node,def).appendTo(originalNodeDiv); } else { createNode(node,def).appendTo(originalNodeDiv); @@ -485,9 +462,9 @@ RED.diff = (function() { localNodeDiv.addClass("node-diff-node-moved"); var localMovedMessage = ""; if (node.z === localN.z) { - localMovedMessage = "moved from "+(localDiff.currentConfig.all[node.id].z||'global'); + localMovedMessage = RED._("diff.type.movedFrom",{id:(localDiff.currentConfig.all[node.id].z||'global')}); } else { - localMovedMessage = "moved to "+(localN.z||'global'); + localMovedMessage = RED._("diff.type.movedTo",{id:(localN.z||'global')}); } $(' '+localMovedMessage+'').appendTo(localNodeDiv); } @@ -497,14 +474,14 @@ RED.diff = (function() { localChanged = true; } else if (localDiff.deleted[node.id]) { localNodeDiv.addClass("node-diff-node-deleted"); - $(' deleted').appendTo(localNodeDiv); + $(' ').appendTo(localNodeDiv); localChanged = true; } else if (localDiff.changed[node.id]) { if (localDiff.newConfig.all[node.id].z !== node.z) { localNodeDiv.addClass("node-diff-empty"); } else { localNodeDiv.addClass("node-diff-node-changed"); - $(' changed').appendTo(localNodeDiv); + $(' ').appendTo(localNodeDiv); localChanged = true; } } else { @@ -513,7 +490,7 @@ RED.diff = (function() { } else { stats.local.unchangedCount++; localNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(localNodeDiv); + $(' ').appendTo(localNodeDiv); } } @@ -526,9 +503,9 @@ RED.diff = (function() { remoteNodeDiv.addClass("node-diff-node-moved"); var remoteMovedMessage = ""; if (node.z === remoteN.z) { - remoteMovedMessage = "moved from "+(remoteDiff.currentConfig.all[node.id].z||'global'); + remoteMovedMessage = RED._("diff.type.movedFrom",{id:(remoteDiff.currentConfig.all[node.id].z||'global')}); } else { - remoteMovedMessage = "moved to "+(remoteN.z||'global'); + remoteMovedMessage = RED._("diff.type.movedTo",{id:(remoteN.z||'global')}); } $(' '+remoteMovedMessage+'').appendTo(remoteNodeDiv); } @@ -536,13 +513,13 @@ RED.diff = (function() { remoteNodeDiv.addClass("node-diff-empty"); } else if (remoteDiff.deleted[node.id]) { remoteNodeDiv.addClass("node-diff-node-deleted"); - $(' deleted').appendTo(remoteNodeDiv); + $(' ').appendTo(remoteNodeDiv); } else if (remoteDiff.changed[node.id]) { if (remoteDiff.newConfig.all[node.id].z !== node.z) { remoteNodeDiv.addClass("node-diff-empty"); } else { remoteNodeDiv.addClass("node-diff-node-changed"); - $(' changed').appendTo(remoteNodeDiv); + $(' ').appendTo(remoteNodeDiv); } } else { if (remoteDiff.newConfig.all[node.id].z !== node.z) { @@ -550,7 +527,7 @@ RED.diff = (function() { } else { stats.remote.unchangedCount++; remoteNodeDiv.addClass("node-diff-node-unchanged"); - $(' unchanged').appendTo(remoteNodeDiv); + $(' ').appendTo(remoteNodeDiv); } } } @@ -1079,7 +1056,7 @@ RED.diff = (function() { if (remoteDiff !== undefined) { $('#node-dialog-view-diff').addClass('node-diff-three-way'); - $('
                      Local
                      Remote
                      ').appendTo("#node-dialog-view-diff-headers"); + $('
                      ').i18n().appendTo("#node-dialog-view-diff-headers"); el.remoteTab = { n:{}, nodes:remoteDiff.newConfig.globals diff --git a/red/api/locales/en-US/editor.json b/red/api/locales/en-US/editor.json index 70abdc70a..cad166866 100644 --- a/red/api/locales/en-US/editor.json +++ b/red/api/locales/en-US/editor.json @@ -138,7 +138,22 @@ }, "diff": { "unresolvedCount": "__count__ unresolved conflict", - "unresolvedCount_plural": "__count__ unresolved conflicts" + "unresolvedCount_plural": "__count__ unresolved conflicts", + "type": { + "added": "added", + "changed": "changed", + "unchanged": "unchanged", + "deleted": "deleted", + "flowDeleted": "flow deleted", + "flowAdded": "flow added", + "movedTo": "moved to __id__", + "movedFrom": "moved from __id__" + }, + "nodeCount": "__count__ node", + "nodeCount_plural": "__count__ nodes", + "local":"Local", + "remote":"Remote" + }, "subflow": { "editSubflow": "Edit flow template: __name__",