journeyapps
Advanced tools
Comparing version 0.2.27 to 0.2.28-alpha1
@@ -0,3 +1,218 @@ | ||
/** | ||
* Abstract base token expression class. | ||
* @param {string} expression | ||
* @param {int} [start] | ||
* @constructor | ||
*/ | ||
function TokenExpression(expression, start) { | ||
if (this.constructor === TokenExpression) { | ||
throw new Error('Cannot instantiate abstract TokenExpression class!'); | ||
} | ||
this.expression = expression; | ||
this.start = start; | ||
this.format = null; | ||
} | ||
/** | ||
* @return {string} | ||
*/ | ||
TokenExpression.prototype.toString = function() { | ||
return '[object ' + this.constructor.name + ' <' + this.expression + ', ' + this.start + '>]'; | ||
}; | ||
/** | ||
* @return {boolean} | ||
*/ | ||
TokenExpression.prototype.isConstant = function() { | ||
// not constant by default | ||
return false; | ||
}; | ||
/** | ||
* @return {boolean} | ||
*/ | ||
TokenExpression.prototype.isShorthand = function() { | ||
// not shorthand by default | ||
return false; | ||
}; | ||
/** | ||
* If the token expression is a function that needs to be called or not. | ||
* @return {boolean} | ||
*/ | ||
TokenExpression.prototype.isFunction = function() { | ||
return false; | ||
}; | ||
/** | ||
* Constant token expression. | ||
* @param {string} expression | ||
* @param {int} [start] | ||
* @constructor | ||
*/ | ||
function ConstantTokenExpression(expression, start) { | ||
TokenExpression.call(this, expression, start); | ||
} | ||
ConstantTokenExpression.prototype = Object.create(TokenExpression.prototype); | ||
ConstantTokenExpression.prototype.constructor = ConstantTokenExpression; | ||
/** | ||
* Concatenate a token to current token and return a new token. | ||
* @param {ConstantTokenExpression} token | ||
* @return {ConstantTokenExpression} result token | ||
*/ | ||
ConstantTokenExpression.prototype.concat = function(token) { | ||
// start value should be start of first token | ||
return new ConstantTokenExpression(this.expression + token.expression, this.start); | ||
}; | ||
/** | ||
* @return {boolean} | ||
*/ | ||
ConstantTokenExpression.prototype.isConstant = function() { | ||
return true; | ||
}; | ||
/** | ||
* Get the value of the constant token expression. | ||
* @return {string} | ||
*/ | ||
ConstantTokenExpression.prototype.valueOf = function() { | ||
return this.expression; | ||
}; | ||
/** | ||
* JavaScript function token expression. | ||
* @param {string} expression | ||
* @param {int} [start] | ||
* @constructor | ||
*/ | ||
function FunctionTokenExpression(expression, start) { | ||
// remove indicator prefix from expression | ||
var prefix = FunctionTokenExpression.PREFIX; | ||
var processedExpression = expression.trim(); | ||
if (processedExpression.indexOf(prefix) === 0) { | ||
processedExpression = processedExpression.substr(prefix.length); | ||
} | ||
TokenExpression.call(this, processedExpression, start); | ||
} | ||
FunctionTokenExpression.prototype = Object.create(TokenExpression.prototype); | ||
FunctionTokenExpression.prototype.constructor = FunctionTokenExpression; | ||
/** | ||
* Prefix for function token expressions. | ||
* @type {string} | ||
*/ | ||
FunctionTokenExpression.PREFIX = '$:'; | ||
FunctionTokenExpression.prototype.isFunction = function() { | ||
return true; | ||
}; | ||
/** | ||
* Name of function represented by function token expression. | ||
* @return {string} | ||
*/ | ||
FunctionTokenExpression.prototype.functionName = function() { | ||
return this.expression.substr(0, this.expression.indexOf('(')); | ||
}; | ||
/** | ||
* Generate a constant token expression from function token expression. | ||
* @param {boolean} [includeEscapeTags] if "{" and "}" format string escape tags should be included or not | ||
* @return {ConstantTokenExpression} | ||
*/ | ||
FunctionTokenExpression.prototype.toConstant = function(includeEscapeTags) { | ||
if (typeof includeEscapeTags === 'undefined' || includeEscapeTags === null) { | ||
includeEscapeTags = false; | ||
} | ||
var constantExpression = FunctionTokenExpression.PREFIX + this.expression; | ||
if (includeEscapeTags) { | ||
constantExpression = '{' + constantExpression + '}'; | ||
} | ||
return new ConstantTokenExpression(constantExpression, this.start); | ||
}; | ||
/** | ||
* Legacy function token expression. | ||
* @param {string} expression | ||
* @param {int} [start] | ||
* @constructor | ||
*/ | ||
function LegacyFunctionTokenExpression(expression, start) { | ||
TokenExpression.call(this, expression, start); | ||
} | ||
LegacyFunctionTokenExpression.prototype = Object.create(TokenExpression.prototype); | ||
LegacyFunctionTokenExpression.prototype.constructor = LegacyFunctionTokenExpression; | ||
LegacyFunctionTokenExpression.prototype.isFunction = function() { | ||
return true; | ||
}; | ||
/** | ||
* Generate a constant token expression from legacy function token expression. | ||
* @param {boolean} [includeEscapeTags] if "{" and "}" format string escape tags should be included or not | ||
* @return {ConstantTokenExpression} | ||
*/ | ||
LegacyFunctionTokenExpression.prototype.toConstant = function(includeEscapeTags) { | ||
if (typeof includeEscapeTags === 'undefined' || includeEscapeTags === null) { | ||
includeEscapeTags = false; | ||
} | ||
var constantExpression = this.expression; | ||
if (includeEscapeTags) { | ||
constantExpression = '{' + constantExpression + '}'; | ||
} | ||
return new ConstantTokenExpression(constantExpression, this.start); | ||
}; | ||
/** | ||
* Shorthand token expression. | ||
* @param {string} expression | ||
* @param {int} [start] | ||
* @constructor | ||
*/ | ||
function ShorthandTokenExpression(expression, start) { | ||
TokenExpression.call(this, expression, start); | ||
} | ||
ShorthandTokenExpression.prototype = Object.create(TokenExpression.prototype); | ||
ShorthandTokenExpression.prototype.constructor = ShorthandTokenExpression; | ||
/** | ||
* @return {boolean} | ||
*/ | ||
ShorthandTokenExpression.prototype.isShorthand = function() { | ||
return true; | ||
}; | ||
/** | ||
* Shorthand token expression with format specifier. | ||
* @param {string} expression | ||
* @param {string} format | ||
* @param {int} [start] | ||
* @constructor | ||
*/ | ||
function FormatShorthandTokenExpression(expression, format, start) { | ||
// wraps ShorthandTokenExpression with format | ||
TokenExpression.call(this, expression, start); | ||
this.inner = new ShorthandTokenExpression(expression, start); | ||
this.format = format; | ||
} | ||
FormatShorthandTokenExpression.prototype = Object.create(TokenExpression.prototype); | ||
FormatShorthandTokenExpression.prototype.constructor = FormatShorthandTokenExpression; | ||
/** | ||
* @return {boolean} | ||
*/ | ||
FormatShorthandTokenExpression.prototype.isShorthand = function() { | ||
return this.inner.isShorthand(); | ||
}; | ||
/** | ||
* @return {string} | ||
*/ | ||
FormatShorthandTokenExpression.prototype.toString = function() { | ||
return '[object ' + this.constructor.name + ' <' + this.expression + ', ' + this.start + ', ' + this.format + '>]'; | ||
}; | ||
// Unescape double closing braces to a single brace | ||
@@ -21,6 +236,58 @@ function unescape(s) { | ||
function parseEnclosingBraces(format) { | ||
var i = format.indexOf('{'); | ||
if (i == -1) { | ||
return null; | ||
} | ||
// We want to skip through these sections | ||
// i.e. do not match { in a string, e.g. "{" | ||
var SPECIAL_SECTIONS = ['\'', '"']; | ||
for (var k = i + 1; k < format.length; k++) { | ||
var character = format[k]; | ||
if (SPECIAL_SECTIONS.indexOf(character) != -1) { | ||
// This is the start of a string, jump to its end | ||
var endChar = format.indexOf(character, k + 1); | ||
if (endChar == -1) { | ||
// Unless the end doesn't exist. Error out. | ||
return null; | ||
} | ||
k = endChar; | ||
continue; | ||
} | ||
if (character == '{') { | ||
// Start of pair of inner braces, | ||
// recursively parse them | ||
var inner = parseEnclosingBraces(format.substring(k)); | ||
if (!inner) { | ||
// Faulty inner, return null | ||
return null; | ||
} | ||
k += inner.length; | ||
continue; | ||
} | ||
if (character == '}') { | ||
// Found closing part for current level of braces | ||
// Return the length to the caller | ||
return { | ||
length: (k - i) | ||
}; | ||
} | ||
} | ||
// Came to end of loop without a match. Faulty, return null | ||
return null; | ||
} | ||
/** | ||
* Compile a format string expression into tokens. | ||
* @param {string} format string expression | ||
* @return {TokenExpression[]} compiled tokens | ||
*/ | ||
function compile(format) { | ||
var start = 0; | ||
var split = []; | ||
/** @type {TokenExpression[]} */ | ||
var tokens = []; | ||
var len = format.length; | ||
@@ -31,10 +298,10 @@ while(true) { | ||
// end of string - everything is normal text | ||
split.push(unescape(format.substring(start))); | ||
tokens.push(new ConstantTokenExpression(unescape(format.substring(start)), start)); | ||
break; | ||
} | ||
// normal text in the gaps between curly braces | ||
split.push(unescape(format.substring(start, i))); | ||
tokens.push(new ConstantTokenExpression(unescape(format.substring(start, i)), start)); | ||
if(format[i+1] == '{') { | ||
// Double left brace - escape and continue | ||
split.push('{'); | ||
tokens.push(new ConstantTokenExpression('{', start)); | ||
start = i + 2; | ||
@@ -44,50 +311,55 @@ continue; | ||
var end = format.indexOf('}', i + 1); | ||
if (end == -1) { | ||
// No closing brace - treat as text | ||
split.push(format.substring(i)); | ||
var parsedBraces = parseEnclosingBraces(format.substring(i)); | ||
if (!parsedBraces) { | ||
// Brace pair faulty (no closing brace), return as a constant | ||
tokens.push(new ConstantTokenExpression(format.substring(i), start)); | ||
break; | ||
} | ||
start = end + 1; | ||
// Next start is at the end of the currently parsed brace pair | ||
start = i + parsedBraces.length + 1; | ||
// `spec` is everything between the curly braces "{" and "}". | ||
var spec = format.substring(i + 1, end); | ||
var spec = format.substring(i + 1, i + parsedBraces.length); | ||
var colon = spec.indexOf(':'); | ||
var variable, formatSpec; | ||
if(colon == -1) { | ||
variable = spec; | ||
formatSpec = null; | ||
// test for function token prefix | ||
if (spec.trim().indexOf(FunctionTokenExpression.PREFIX) === 0) { | ||
// function token because the function name has "$:" as prefix (leading whitespace is ignored) | ||
tokens.push(new FunctionTokenExpression(spec, i)); | ||
} else { | ||
variable = spec.substring(0, colon); | ||
formatSpec = spec.substring(colon+1); | ||
// shorthand token | ||
var colon = spec.indexOf(':'); | ||
if(colon == -1) { | ||
tokens.push(new ShorthandTokenExpression(spec, i)); | ||
} else { | ||
tokens.push(new FormatShorthandTokenExpression(spec.substring(0, colon), spec.substring(colon+1), i)); | ||
} | ||
} | ||
var value = {expression: variable, format: formatSpec, start: i}; | ||
split.push(value); | ||
} | ||
// Concatenate any neighbouring strings | ||
// concatenate any neighbouring constant token expressions | ||
/** @type {TokenExpression[]} */ | ||
var result = []; | ||
/** @type {ConstantTokenExpression} */ | ||
var last = null; | ||
for(var j = 0; j < split.length; j++) { | ||
var token = split[j]; | ||
if(typeof token == 'string') { | ||
if(last == null) { | ||
if(token.length > 0) { | ||
for (var j = 0; j < tokens.length; j++) { | ||
var token = tokens[j]; | ||
if (token instanceof ConstantTokenExpression) { | ||
if (last == null) { | ||
if (token.expression.length > 0) { | ||
last = token; | ||
} | ||
} else { | ||
last += token; | ||
last = last.concat(token); | ||
} | ||
} else { | ||
if(last != null) { | ||
if (last != null) { | ||
result.push(last); | ||
last = null; | ||
} | ||
result.push(token); | ||
last = null; | ||
} | ||
} | ||
if(last != null) { | ||
if (last != null) { | ||
result.push(last); | ||
@@ -99,9 +371,17 @@ } | ||
/** | ||
* Construct a new format string expression. | ||
* @param {string} expression | ||
* @constructor | ||
*/ | ||
function FormatString(expression) { | ||
this.expression = expression || ''; | ||
/** @type {TokenExpression[]} */ | ||
this.tokens = compile(this.expression); | ||
} | ||
/** | ||
* @return {string} | ||
*/ | ||
FormatString.prototype.toString = function() { | ||
@@ -111,2 +391,11 @@ return this.expression; | ||
/** | ||
* If the format string is constant (i.e. no values need to be evaluated). | ||
* @return {boolean} | ||
*/ | ||
FormatString.prototype.isConstant = function() { | ||
// constants format strings will only contain a single constant token | ||
return this.tokens.length == 1 && this.tokens[0].isConstant(); | ||
}; | ||
function getObjectType(parent, name) { | ||
@@ -188,3 +477,3 @@ var variable = parent.getAttribute(name); | ||
var token = tokens[i]; | ||
if(typeof token == 'object') { | ||
if(token.isShorthand()) { | ||
var expression = token.expression; | ||
@@ -197,2 +486,6 @@ extract(type, expression, result, depth); | ||
/** | ||
* @param scopeType | ||
* @return {Array} | ||
*/ | ||
FormatString.prototype.validate = function(scopeType) { | ||
@@ -204,5 +497,6 @@ var tokens = this.tokens; | ||
for(var i = 0; i < tokens.length; i++) { | ||
// validate all shorthand and function token expressions (ignore constant token expressions) | ||
var token = tokens[i]; | ||
if (typeof token == 'object') { | ||
var expression = token.expression; | ||
var expression = token.expression; | ||
if (token.isShorthand()) { | ||
var warnQuestionMark = false; | ||
@@ -214,4 +508,2 @@ if(expression.length > 0 && expression[0] == '?') { | ||
var type = scopeType.getVariable(expression); | ||
@@ -234,2 +526,5 @@ if(type == null) { | ||
} | ||
if (token.isFunction()) { | ||
// TODO: validate that function exists in view | ||
} | ||
} | ||
@@ -245,3 +540,3 @@ return results; | ||
var token = tokens[i]; | ||
if (typeof token == 'object') { | ||
if (!token.isConstant()) { | ||
var expression = token.expression; | ||
@@ -265,2 +560,7 @@ // We are interested in the type and name of the final two variables in the expression | ||
/** | ||
* Create format string. | ||
* @param {string} expression | ||
* @return {FormatString|null} | ||
*/ | ||
function formatString(expression) { | ||
@@ -274,7 +574,57 @@ if(expression == null) { | ||
/** | ||
# Construct a function token expression from a raw expression string. | ||
* @param {string} expression | ||
* @param {boolean} [allowLegacy=true] if legacy function token expressions are allowed (defaults to true) | ||
* @return {FunctionTokenExpression|LegacyFunctionTokenExpression|null} | ||
*/ | ||
function functionTokenExpression(expression, allowLegacy) { | ||
if (typeof allowLegacy === 'undefined' || allowLegacy == null) { | ||
allowLegacy = true; // default value | ||
} | ||
if (expression == null) { | ||
return null; | ||
} | ||
if (expression.trim().indexOf(FunctionTokenExpression.PREFIX) === 0) { | ||
return new FunctionTokenExpression(expression); | ||
} | ||
if (allowLegacy) { | ||
// assume legacy function token expression (if allowed) at this point | ||
return new LegacyFunctionTokenExpression(expression); | ||
} | ||
return null; | ||
} | ||
/** | ||
* Create a token expression that can be evaluated. | ||
* @param {string} expression | ||
* @return {FunctionTokenExpression|ShorthandTokenExpression|FormatShorthandTokenExpression} | ||
*/ | ||
function actionableTokenExpression(expression) { | ||
if (expression == null) { | ||
return null; | ||
} | ||
if (expression.trim().indexOf(FunctionTokenExpression.PREFIX) === 0) { | ||
return new FunctionTokenExpression(expression); | ||
} | ||
var colon = expression.indexOf(':'); | ||
if(colon == -1) { | ||
return new ShorthandTokenExpression(expression); | ||
} | ||
return new FormatShorthandTokenExpression(expression.substring(0, colon), expression.substring(colon+1)); | ||
} | ||
module.exports = { | ||
FormatString: FormatString, | ||
compile: compile, | ||
TokenExpression: TokenExpression, | ||
ConstantTokenExpression: ConstantTokenExpression, | ||
FunctionTokenExpression: FunctionTokenExpression, | ||
LegacyFunctionTokenExpression: LegacyFunctionTokenExpression, | ||
ShorthandTokenExpression: ShorthandTokenExpression, | ||
FormatShorthandTokenExpression: FormatShorthandTokenExpression, | ||
_compile: compile, // exposed for tests | ||
formatString: formatString, | ||
functionTokenExpression: functionTokenExpression, | ||
actionableTokenExpression: actionableTokenExpression, | ||
_deepMerge: deepMerge // Exposed for tests only | ||
}; |
@@ -113,3 +113,3 @@ // # schema module | ||
bind: xml.attribute.notBlank, | ||
required: xml.attribute.optionList(['true', 'false']), | ||
required: xml.attribute.optionListWithFunctions(['true', 'false'], evaluator.FunctionTokenExpression.PREFIX), | ||
'show-if': xml.attribute.notBlank, | ||
@@ -259,3 +259,3 @@ 'hide-if': xml.attribute.notBlank | ||
label: xml.attribute.label, | ||
state: xml.attribute.optionList(['normal', 'active', 'disabled']), | ||
state: xml.attribute.optionListWithFunctions(['normal', 'active', 'disabled'], evaluator.FunctionTokenExpression.PREFIX), | ||
icon: xml.attribute.path, | ||
@@ -665,3 +665,11 @@ 'on-press': xml.attribute.notBlank, | ||
function validateBinding(element, attr) { | ||
/** | ||
* @param element | ||
* @param {string} attr | ||
* @param {boolean} [allowFunctionBinding=false] if function bindings are allowed (defaults to false) | ||
*/ | ||
function validateBinding(element, attr, allowFunctionBinding) { | ||
if (typeof allowFunctionBinding === 'undefined' || allowFunctionBinding == null) { | ||
allowFunctionBinding = false; | ||
} | ||
var binding = getAttribute(element, attr); | ||
@@ -672,2 +680,10 @@ // TODO: somehow handle this automatically with getType. | ||
} | ||
if (allowFunctionBinding) { | ||
var token = evaluator.functionTokenExpression(binding, false); | ||
if (token != null) { | ||
// binding is a function token expression | ||
// TODO: validate that the function exists in the view (not done for `onpress` currently) | ||
return; | ||
} | ||
} | ||
var bindingType = view.type.getType(binding); | ||
@@ -687,4 +703,4 @@ if(bindingType == null) { | ||
} | ||
validateBinding(element, 'show-if'); | ||
validateBinding(element, 'hide-if'); | ||
validateBinding(element, 'show-if', true); | ||
validateBinding(element, 'hide-if', true); | ||
} | ||
@@ -691,0 +707,0 @@ |
@@ -270,2 +270,22 @@ // # xml module | ||
/** | ||
* @param {Array} options | ||
* @param {string} functionPrefix | ||
* @param {string} [customMessage] | ||
* @return {Function} | ||
*/ | ||
attribute.optionListWithFunctions = function optionListWithFunctions(options, functionPrefix, customMessage) { | ||
return function(value, element) { | ||
if (value.indexOf(functionPrefix) === 0) { | ||
// function token expression, therefore allow | ||
return value; | ||
} | ||
if(options.indexOf(value) == -1) { | ||
var message = customMessage == null ? element.name + ' must be one of ' + options : customMessage; | ||
throw new Error(message); | ||
} | ||
return value; | ||
}; | ||
}; | ||
attribute.multiOptionList = function multiOptionList(options, customMessage) { | ||
@@ -272,0 +292,0 @@ |
@@ -212,4 +212,8 @@ | ||
var token = tokens[i]; | ||
if(typeof token == 'string') { | ||
result += token; | ||
if(token.isConstant()) { | ||
result += token.valueOf(); | ||
} else if (token.isFunction()) { | ||
// format strings used in the data model should not allow evaluation of functions | ||
// therefore the literal expression string is used (with format string escape tags included) | ||
result += token.toConstant(true).valueOf(); | ||
} else { | ||
@@ -234,3 +238,3 @@ var value = formatValue(scope, token.expression, token.format); | ||
var token = tokens[i]; | ||
if(typeof token == 'string') { | ||
if(token.isConstant() || token.isFunction()) { | ||
// Ignore for now | ||
@@ -248,4 +252,8 @@ } else { | ||
var token = tokens[i]; | ||
if(typeof token == 'string') { | ||
result += token; | ||
if(token.isConstant()) { | ||
result += token.valueOf(); | ||
} else if (token.isFunction()) { | ||
// format strings used in the data model should not allow evaluation of functions | ||
// therefore the literal expression string is used (with format string escape tags included) | ||
result += token.toConstant(true).valueOf(); | ||
} else { | ||
@@ -267,6 +275,2 @@ result += results[promiseIndex]; | ||
evaluator.FormatString.prototype.isConstant = function() { | ||
return this.tokens.length == 1 && typeof this.tokens[0] == 'string'; | ||
}; | ||
evaluator.getValue = getValue; | ||
@@ -273,0 +277,0 @@ evaluator.setValue = setValue; |
{ | ||
"name": "journeyapps", | ||
"version": "0.2.27", | ||
"version": "0.2.28-alpha1", | ||
"description": "Journey JS library", | ||
@@ -49,2 +49,3 @@ "keywords": [ | ||
"grunt-webpack": "^1.0.11", | ||
"imports-loader": "^0.7.1", | ||
"jasmine": "^2.4.1", | ||
@@ -51,0 +52,0 @@ "jasmine-core": "^2.4.1", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
432645
30
64
11049