Comparing version 2.1.0 to 2.2.0
@@ -13,3 +13,3 @@ /* | ||
var util = require('racer/lib/util'); | ||
var derbyTemplates = require('derby-templates'); | ||
var derbyTemplates = require('./templates'); | ||
var templates = derbyTemplates.templates; | ||
@@ -21,3 +21,4 @@ var components = require('./components'); | ||
globalThis.APPS = globalThis.APPS || new Map(); | ||
// TODO: Change to Map once we officially drop support for ES5. | ||
global.APPS = global.APPS || {}; | ||
@@ -76,10 +77,10 @@ function App(derby, name, filename, options) { | ||
if (!util.isProduction) { | ||
previousAppInfo = globalThis.APPS.get(this.name); | ||
previousAppInfo = global.APPS[this.name]; | ||
if (previousAppInfo) { | ||
previousAppInfo.app._destroyCurrentPage(); | ||
} | ||
globalThis.APPS.set(this.name, { | ||
global.APPS[this.name] = { | ||
app: this, | ||
initialState: data, | ||
}); | ||
}; | ||
} | ||
@@ -126,3 +127,3 @@ | ||
} else { | ||
return globalThis.APPS.get(this.name).initialState; | ||
return global.APPS[this.name].initialState; | ||
} | ||
@@ -261,3 +262,3 @@ } | ||
// TODO: DRY. This is copy-pasted from derby-templates | ||
// TODO: DRY. This is copy-pasted from ./templates | ||
var mapName = viewName.replace(/:index$/, ''); | ||
@@ -264,0 +265,0 @@ var currentView = this.views.nameMap[mapName]; |
@@ -11,3 +11,3 @@ /* | ||
var util = require('racer/lib/util'); | ||
var derbyTemplates = require('derby-templates'); | ||
var derbyTemplates = require('./templates'); | ||
var templates = derbyTemplates.templates; | ||
@@ -14,0 +14,0 @@ var expressions = derbyTemplates.expressions; |
@@ -1,2 +0,2 @@ | ||
var expressions = require('derby-templates').expressions; | ||
var expressions = require('./templates').expressions; | ||
@@ -3,0 +3,0 @@ // The many trees of bindings: |
@@ -1,2 +0,2 @@ | ||
var derbyTemplates = require('derby-templates'); | ||
var derbyTemplates = require('./templates'); | ||
var contexts = derbyTemplates.contexts; | ||
@@ -3,0 +3,0 @@ var expressions = derbyTemplates.expressions; |
@@ -1,7 +0,853 @@ | ||
// TODO: Refactor and include derby-parsing module in derby itself | ||
exports = module.exports = require('derby-parsing'); | ||
var derbyTemplates = require('../templates'); | ||
var htmlUtil = require('html-util'); | ||
var path = require('path'); | ||
var App = require('../App'); | ||
var createPathExpression = require('./createPathExpression'); | ||
var markup = require('./markup'); | ||
var templates = derbyTemplates.templates; | ||
var expressions = derbyTemplates.expressions; | ||
exports.createTemplate = createTemplate; | ||
exports.createStringTemplate = createStringTemplate; | ||
exports.createExpression = createExpression; | ||
exports.createPathExpression = createPathExpression; | ||
exports.markup = markup; | ||
// View.prototype._parse is defined here, so that it doesn't have to | ||
// be included in the client if templates are all parsed server-side | ||
templates.View.prototype._parse = function() { | ||
// Wrap parsing in a try / catch to add context to message when throwing | ||
var template; | ||
try { | ||
if (this.literal) { | ||
var source = (this.unminified) ? this.source : | ||
// Remove leading and trailing whitespace only lines by default | ||
this.source.replace(/^\s*\n/, '').replace(/\s*$/, ''); | ||
template = new templates.Text(source); | ||
} else if (this.string) { | ||
template = createStringTemplate(this.source, this); | ||
} else { | ||
var source = (this.unminified) ? this.source : | ||
htmlUtil.minify(this.source).replace(/&sp;/g, ' '); | ||
template = createTemplate(source, this); | ||
} | ||
} catch (err) { | ||
var message = '\n\nWithin template "' + this.name + '":\n' + this.source; | ||
throw appendErrorMessage(err, message); | ||
} | ||
this.template = template; | ||
return template; | ||
}; | ||
// Modified and shared among the following parse functions. It's OK for this | ||
// to be shared at the module level, since it is only used by synchronous code | ||
var parseNode; | ||
function createTemplate(source, view) { | ||
source = escapeBraced(source); | ||
parseNode = new ParseNode(view); | ||
htmlUtil.parse(source, { | ||
start: parseHtmlStart, | ||
end: parseHtmlEnd, | ||
text: parseHtmlText, | ||
comment: parseHtmlComment, | ||
other: parseHtmlOther | ||
}); | ||
// Allow for certain elements at the end of a template to not be closed. This | ||
// is especially important so that </body> and </html> tags can be omitted, | ||
// since Derby sends an additional script tag after the HTML for the page | ||
while (parseNode.parent) { | ||
parseNode = parseNode.parent; | ||
var last = parseNode.last(); | ||
if (last instanceof templates.Element) { | ||
if (last.tagName === 'body' || last.tagName === 'html') { | ||
last.notClosed = true; | ||
last.endTag = ''; | ||
continue; | ||
} else { | ||
throw new Error('Missing closing HTML tag: ' + last.endTag); | ||
} | ||
} | ||
unexpected(); | ||
} | ||
return new templates.Template(parseNode.content); | ||
} | ||
function createStringTemplate(source, view) { | ||
source = escapeBraced(source); | ||
parseNode = new ParseNode(view); | ||
parseText(source, parseTextLiteral, parseTextExpression, 'string'); | ||
return new templates.Template(parseNode.content); | ||
} | ||
function parseHtmlStart(tag, tagName, attributes, selfClosing) { | ||
var lowerTagName = tagName.toLowerCase(); | ||
var hooks; | ||
if (lowerTagName !== 'view' && !viewForTagName(lowerTagName)) { | ||
hooks = elementHooksFromAttributes(attributes); | ||
} | ||
var attributesMap = parseAttributes(attributes); | ||
var namespaceUri = (lowerTagName === 'svg') ? | ||
templates.NAMESPACE_URIS.svg : parseNode.namespaceUri; | ||
var Constructor = templates.Element; | ||
if (lowerTagName === 'tag') { | ||
Constructor = templates.DynamicElement; | ||
tagName = attributesMap.is; | ||
delete attributesMap.is; | ||
} | ||
if (selfClosing || templates.VOID_ELEMENTS[lowerTagName]) { | ||
var element = new Constructor(tagName, attributesMap, null, hooks, selfClosing, null, namespaceUri); | ||
parseNode.content.push(element); | ||
parseElementClose(lowerTagName); | ||
} else { | ||
parseNode = parseNode.child(); | ||
parseNode.namespaceUri = namespaceUri; | ||
var element = new Constructor(tagName, attributesMap, parseNode.content, hooks, selfClosing, null, namespaceUri); | ||
parseNode.parent.content.push(element); | ||
} | ||
} | ||
function parseAttributes(attributes) { | ||
var attributesMap; | ||
for (var key in attributes) { | ||
if (!attributesMap) attributesMap = {}; | ||
var value = attributes[key]; | ||
var match = /([^:]+):[^:]/.exec(key); | ||
var nsUri = match && templates.NAMESPACE_URIS[match[1]]; | ||
if (value === '' || typeof value !== 'string') { | ||
attributesMap[key] = new templates.Attribute(value, nsUri); | ||
continue; | ||
} | ||
parseNode = parseNode.child(); | ||
parseText(value, parseTextLiteral, parseTextExpression, 'attribute'); | ||
if (parseNode.content.length === 1) { | ||
var item = parseNode.content[0]; | ||
attributesMap[key] = | ||
(item instanceof templates.Text) ? | ||
new templates.Attribute(item.data, nsUri) : | ||
(item instanceof templates.DynamicText) ? | ||
(item.expression instanceof expressions.LiteralExpression) ? | ||
new templates.Attribute(item.expression.value, nsUri) : | ||
new templates.DynamicAttribute(item.expression, nsUri) | ||
: | ||
new templates.DynamicAttribute(item, nsUri); | ||
} else if (parseNode.content.length > 1) { | ||
var template = new templates.Template(parseNode.content, value); | ||
attributesMap[key] = new templates.DynamicAttribute(template, nsUri); | ||
} else { | ||
throw new Error('Error parsing ' + key + ' attribute: ' + value); | ||
} | ||
parseNode = parseNode.parent; | ||
} | ||
return attributesMap; | ||
} | ||
function parseHtmlEnd(tag, tagName) { | ||
parseNode = parseNode.parent; | ||
var last = parseNode.last(); | ||
if (!( | ||
(last instanceof templates.DynamicElement && tagName.toLowerCase() === 'tag') || | ||
(last instanceof templates.Element && last.tagName === tagName) | ||
)) { | ||
throw new Error('Mismatched closing HTML tag: ' + tag); | ||
} | ||
parseElementClose(tagName); | ||
} | ||
function parseElementClose(tagName) { | ||
if (tagName === 'view') { | ||
var element = parseNode.content.pop(); | ||
parseViewElement(element); | ||
return; | ||
} | ||
var view = viewForTagName(tagName); | ||
if (view) { | ||
var element = parseNode.content.pop(); | ||
parseNamedViewElement(element, view, view.name); | ||
return; | ||
} | ||
var element = parseNode.last(); | ||
markup.emit('element', element); | ||
markup.emit('element:' + tagName, element); | ||
} | ||
function viewForTagName(tagName) { | ||
return parseNode.view && parseNode.view.views.tagMap[tagName]; | ||
} | ||
function parseHtmlText(data, isRawText) { | ||
var environment = (isRawText) ? 'string' : 'html'; | ||
parseText(data, parseTextLiteral, parseTextExpression, environment); | ||
} | ||
function parseHtmlComment(tag, data) { | ||
// Only output comments that start with `<!--[` and end with `]-->` | ||
if (!htmlUtil.isConditionalComment(tag)) return; | ||
var comment = new templates.Comment(data); | ||
parseNode.content.push(comment); | ||
} | ||
var doctypeRegExp = /^<!DOCTYPE\s+([^\s]+)(?:\s+(PUBLIC|SYSTEM)\s+"([^"]+)"(?:\s+"([^"]+)")?)?\s*>/i; | ||
function parseHtmlOther(tag) { | ||
var match = doctypeRegExp.exec(tag); | ||
if (match) { | ||
var name = match[1]; | ||
var idType = match[2] && match[2].toLowerCase(); | ||
var publicId, systemId; | ||
if (idType === 'public') { | ||
publicId = match[3]; | ||
systemId = match[4]; | ||
} else if (idType === 'system') { | ||
systemId = match[3]; | ||
} | ||
var doctype = new templates.Doctype(name, publicId, systemId); | ||
parseNode.content.push(doctype); | ||
} else { | ||
unexpected(tag); | ||
} | ||
} | ||
function parseTextLiteral(data) { | ||
var text = new templates.Text(data); | ||
parseNode.content.push(text); | ||
} | ||
function parseTextExpression(source, environment) { | ||
var expression = createExpression(source); | ||
if (expression.meta.blockType) { | ||
parseBlockExpression(expression); | ||
} else if (expression.meta.valueType === 'view') { | ||
parseViewExpression(expression); | ||
} else if (expression.meta.unescaped && environment === 'html') { | ||
var html = new templates.DynamicHtml(expression); | ||
parseNode.content.push(html); | ||
} else { | ||
var text = new templates.DynamicText(expression); | ||
parseNode.content.push(text); | ||
} | ||
} | ||
function parseBlockExpression(expression) { | ||
var blockType = expression.meta.blockType; | ||
// Block ending | ||
if (expression.meta.isEnd) { | ||
parseNode = parseNode.parent; | ||
// Validate that the block ending matches an appropriate block start | ||
var last = parseNode.last(); | ||
var lastExpression = last && (last.expression || (last.expressions && last.expressions[0])); | ||
if (!( | ||
lastExpression && | ||
(blockType === 'end' && lastExpression.meta.blockType) || | ||
(blockType === lastExpression.meta.blockType) | ||
)) { | ||
throw new Error('Mismatched closing template tag: ' + expression.meta.source); | ||
} | ||
// Continuing block | ||
} else if (blockType === 'else' || blockType === 'else if') { | ||
parseNode = parseNode.parent; | ||
var last = parseNode.last(); | ||
parseNode = parseNode.child(); | ||
if (last instanceof templates.ConditionalBlock) { | ||
last.expressions.push(expression); | ||
last.contents.push(parseNode.content); | ||
} else if (last instanceof templates.EachBlock) { | ||
if (blockType !== 'else') unexpected(expression.meta.source); | ||
last.elseContent = parseNode.content; | ||
} else { | ||
unexpected(expression.meta.source); | ||
} | ||
// Block start | ||
} else { | ||
var nextNode = parseNode.child(); | ||
var block; | ||
if (blockType === 'if' || blockType === 'unless') { | ||
block = new templates.ConditionalBlock([expression], [nextNode.content]); | ||
} else if (blockType === 'each') { | ||
block = new templates.EachBlock(expression, nextNode.content); | ||
} else { | ||
block = new templates.Block(expression, nextNode.content); | ||
} | ||
parseNode.content.push(block); | ||
parseNode = nextNode; | ||
} | ||
} | ||
function parseViewElement(element) { | ||
// TODO: "name" is deprecated in lieu of "is". Remove "name" in Derby 0.6.0 | ||
var nameAttribute = element.attributes.is || element.attributes.name; | ||
if (!nameAttribute) { | ||
throw new Error('The <view> element requires an "is" attribute'); | ||
} | ||
delete element.attributes.is; | ||
delete element.attributes.name; | ||
if (nameAttribute.expression) { | ||
var viewAttributes = viewAttributesFromElement(element); | ||
var componentHooks = componentHooksFromAttributes(viewAttributes); | ||
var remaining = element.content || []; | ||
var viewInstance = createDynamicViewInstance(nameAttribute.expression, viewAttributes, componentHooks.hooks, componentHooks.initHooks); | ||
finishParseViewElement(viewAttributes, remaining, viewInstance); | ||
} else { | ||
var name = nameAttribute.data; | ||
var view = findView(name); | ||
parseNamedViewElement(element, view, name); | ||
} | ||
} | ||
function findView(name) { | ||
var view = parseNode.view.views.find(name, parseNode.view.namespace); | ||
if (!view) { | ||
var message = parseNode.view.views.findErrorMessage(name); | ||
throw new Error(message); | ||
} | ||
return view; | ||
} | ||
function parseNamedViewElement(element, view, name) { | ||
var viewAttributes = viewAttributesFromElement(element); | ||
var componentHooks = componentHooksFromAttributes(viewAttributes); | ||
var remaining = parseContentAttributes(element.content, view, viewAttributes); | ||
var viewInstance = new templates.ViewInstance(view.registeredName, viewAttributes, componentHooks.hooks, componentHooks.initHooks); | ||
finishParseViewElement(viewAttributes, remaining, viewInstance); | ||
} | ||
function createDynamicViewInstance(expression, attributes, hooks, initHooks) { | ||
var viewInstance = new templates.DynamicViewInstance(expression, attributes, hooks, initHooks); | ||
// Wrap the viewInstance in a block with the same expression, so that it is | ||
// re-rendered when any of its dependencies change | ||
return new templates.Block(expression, [viewInstance]); | ||
} | ||
function finishParseViewElement(viewAttributes, remaining, viewInstance) { | ||
setContentAttribute(viewAttributes, remaining); | ||
delete viewAttributes.within; | ||
parseNode.content.push(viewInstance); | ||
} | ||
function setContentAttribute(attributes, content) { | ||
if (attributes.hasOwnProperty('content')) return; | ||
if (!content.length) return; | ||
attributes.content = attributeValueFromContent(content, attributes.within); | ||
} | ||
function attributeValueFromContent(content, isWithin) { | ||
// Optimize common cases where content can be a literal or a single expression | ||
if (content.length === 1) { | ||
var item = content[0]; | ||
if (item instanceof templates.Text) { | ||
return item.data; | ||
} | ||
if (item instanceof templates.DynamicText) { | ||
var expression = item.expression; | ||
if (expression instanceof expressions.LiteralExpression) { | ||
return expression.value; | ||
} | ||
// In the case of within attributes, always use a template, never an | ||
// expression. A within value depends on the rendering context, so we | ||
// cannot get a single value for the attribute and store it on the | ||
// component model when the component is initialized | ||
if (isWithin) return item; | ||
// Create an expression in cases where it is safe to do so. This allows | ||
// derby to get the intended value and store it on the component model | ||
return new expressions.ViewParentExpression(expression); | ||
} | ||
} | ||
// Otherwise, wrap a template as needed for the context | ||
var template = new templates.Template(content); | ||
return (isWithin) ? template : new templates.ViewParent(template); | ||
} | ||
function viewAttributesFromElement(element) { | ||
var viewAttributes = {}; | ||
for (var key in element.attributes) { | ||
var attribute = element.attributes[key]; | ||
var camelCased = dashToCamelCase(key); | ||
viewAttributes[camelCased] = | ||
(attribute.expression instanceof templates.Template) ? | ||
new templates.ViewParent(attribute.expression) : | ||
(attribute.expression instanceof expressions.Expression) ? | ||
new expressions.ViewParentExpression(attribute.expression) : | ||
attribute.data; | ||
} | ||
return viewAttributes; | ||
} | ||
function parseAsAttribute(key, value) { | ||
var expression = createPathExpression(value); | ||
if (!(expression instanceof expressions.PathExpression)) { | ||
throw new Error(key + ' attribute must be a path: ' + key + '="' + value + '"'); | ||
} | ||
return expression.segments; | ||
} | ||
function parseAsObjectAttribute(key, value) { | ||
var expression = createPathExpression(value); | ||
if (!( | ||
expression instanceof expressions.SequenceExpression && | ||
expression.args.length === 2 && | ||
expression.args[0] instanceof expressions.PathExpression | ||
)) { | ||
throw new Error(key + ' attribute requires a path and a key argument: ' + key + '="' + value + '"'); | ||
} | ||
var segments = expression.args[0].segments; | ||
var expression = expression.args[1]; | ||
return {segments: segments, expression: expression}; | ||
} | ||
function parseOnAttribute(key, value) { | ||
// TODO: Argument checking | ||
return createPathExpression(value); | ||
} | ||
function elementHooksFromAttributes(attributes, type) { | ||
if (!attributes) return; | ||
var hooks = []; | ||
for (var key in attributes) { | ||
var value = attributes[key]; | ||
// Parse `as` assignments | ||
if (key === 'as') { | ||
var segments = parseAsAttribute(key, value); | ||
hooks.push(new templates.AsProperty(segments)); | ||
delete attributes[key]; | ||
continue; | ||
} | ||
if (key === 'as-array') { | ||
var segments = parseAsAttribute(key, value); | ||
hooks.push(new templates.AsArray(segments)); | ||
delete attributes[key]; | ||
continue; | ||
} | ||
if (key === 'as-object') { | ||
var parsed = parseAsObjectAttribute(key, value); | ||
hooks.push(new templates.AsObject(parsed.segments, parsed.expression)); | ||
delete attributes[key]; | ||
continue; | ||
} | ||
// Parse event listeners | ||
var match = /^on-(.+)/.exec(key); | ||
var eventName = match && match[1]; | ||
if (eventName) { | ||
var expression = parseOnAttribute(key, value); | ||
hooks.push(new templates.ElementOn(eventName, expression)); | ||
delete attributes[key]; | ||
} | ||
} | ||
if (hooks.length) return hooks; | ||
} | ||
function componentHooksFromAttributes(attributes) { | ||
if (!attributes) return {}; | ||
var hooks = []; | ||
var initHooks = []; | ||
for (var key in attributes) { | ||
var value = attributes[key]; | ||
// Parse `as` assignments | ||
if (key === 'as') { | ||
var segments = parseAsAttribute(key, value); | ||
hooks.push(new templates.AsPropertyComponent(segments)); | ||
delete attributes[key]; | ||
continue; | ||
} | ||
if (key === 'asArray') { | ||
var segments = parseAsAttribute('as-array', value); | ||
hooks.push(new templates.AsArrayComponent(segments)); | ||
delete attributes[key]; | ||
continue; | ||
} | ||
if (key === 'asObject') { | ||
var parsed = parseAsObjectAttribute('as-object', value); | ||
hooks.push(new templates.AsObjectComponent(parsed.segments, parsed.expression)); | ||
delete attributes[key]; | ||
continue; | ||
} | ||
// Parse event listeners | ||
var match = /^on([A-Z_].*)/.exec(key); | ||
var eventName = match && match[1].charAt(0).toLowerCase() + match[1].slice(1); | ||
if (eventName) { | ||
var expression = parseOnAttribute(key, value); | ||
initHooks.push(new templates.ComponentOn(eventName, expression)); | ||
delete attributes[key]; | ||
} | ||
} | ||
return { | ||
hooks: (hooks.length) ? hooks : null, | ||
initHooks: (initHooks.length) ? initHooks : null | ||
}; | ||
} | ||
function dashToCamelCase(string) { | ||
return string.replace(/-./g, function(match) { | ||
return match.charAt(1).toUpperCase(); | ||
}); | ||
} | ||
function parseContentAttributes(content, view, viewAttributes) { | ||
var remaining = []; | ||
if (!content) return remaining; | ||
for (var i = 0, len = content.length; i < len; i++) { | ||
var item = content[i]; | ||
var name = (item instanceof templates.Element) && item.tagName; | ||
if (name === 'attribute') { | ||
var name = parseNameAttribute(item); | ||
parseAttributeElement(item, name, viewAttributes); | ||
} else if (view.attributesMap && view.attributesMap[name]) { | ||
parseAttributeElement(item, name, viewAttributes); | ||
} else if (name === 'array') { | ||
var name = parseNameAttribute(item); | ||
parseArrayElement(item, name, viewAttributes); | ||
} else if (view.arraysMap && view.arraysMap[name]) { | ||
parseArrayElement(item, view.arraysMap[name], viewAttributes); | ||
} else { | ||
remaining.push(item); | ||
} | ||
} | ||
return remaining; | ||
} | ||
function parseNameAttribute(element) { | ||
// TODO: "name" is deprecated in lieu of "is". Remove "name" in Derby 0.6.0 | ||
var nameAttribute = element.attributes.is || element.attributes.name; | ||
var name = nameAttribute.data; | ||
if (!name) { | ||
throw new Error('The <' + element.tagName + '> element requires a literal "is" attribute'); | ||
} | ||
delete element.attributes.is; | ||
delete element.attributes.name; | ||
return name; | ||
} | ||
function parseAttributeElement(element, name, viewAttributes) { | ||
var camelName = dashToCamelCase(name); | ||
var isWithin = element.attributes && element.attributes.within; | ||
viewAttributes[camelName] = attributeValueFromContent(element.content, isWithin); | ||
} | ||
function createAttributesExpression(attributes) { | ||
var dynamicAttributes = {}; | ||
var literalAttributes = {}; | ||
var isLiteral = true; | ||
for (var key in attributes) { | ||
var attribute = attributes[key]; | ||
if (attribute instanceof expressions.Expression) { | ||
dynamicAttributes[key] = attribute; | ||
isLiteral = false; | ||
} else if (attribute instanceof templates.Template) { | ||
dynamicAttributes[key] = new expressions.DeferRenderExpression(attribute); | ||
isLiteral = false; | ||
} else { | ||
dynamicAttributes[key] = new expressions.LiteralExpression(attribute); | ||
literalAttributes[key] = attribute; | ||
} | ||
} | ||
return (isLiteral) ? | ||
new expressions.LiteralExpression(literalAttributes) : | ||
new expressions.ObjectExpression(dynamicAttributes); | ||
} | ||
function parseArrayElement(element, name, viewAttributes) { | ||
var attributes = viewAttributesFromElement(element); | ||
setContentAttribute(attributes, element.content); | ||
delete attributes.within; | ||
var expression = createAttributesExpression(attributes); | ||
var camelName = dashToCamelCase(name); | ||
var viewAttribute = viewAttributes[camelName]; | ||
// If viewAttribute is already an ArrayExpression, push the expression for | ||
// the current array element | ||
if (viewAttribute instanceof expressions.ArrayExpression) { | ||
viewAttribute.items.push(expression); | ||
// Alternatively, viewAttribute will be an array if its items have all been | ||
// literal values thus far | ||
} else if (Array.isArray(viewAttribute)) { | ||
if (expression instanceof expressions.LiteralExpression) { | ||
// If the current array element continues to be a literal value, push it | ||
// on the existing array | ||
viewAttribute.push(expression.value); | ||
} else { | ||
// However, if the array element produces a non-literal expression, | ||
// convert the values in the array into an equivalent ArrayExpression of | ||
// LiteralExpressions, then push on this expression as well | ||
var items = []; | ||
for (var i = 0; i < viewAttribute.length; i++) { | ||
items[i] = new expressions.LiteralExpression(viewAttribute[i]); | ||
} | ||
items.push(expression); | ||
viewAttributes[camelName] = new expressions.ArrayExpression(items); | ||
} | ||
// For the first array element encountered, create a containing array or | ||
// ArrayExpression. Create an array of raw values in the literal case and an | ||
// ArrayExpression of expressions in the non-literal case | ||
} else if (viewAttribute == null) { | ||
viewAttributes[camelName] = (expression instanceof expressions.LiteralExpression) ? | ||
[expression.value] : new expressions.ArrayExpression([expression]); | ||
} else { | ||
unexpected(); | ||
} | ||
} | ||
function parseViewExpression(expression) { | ||
// If there are multiple arguments separated by commas, they will get parsed | ||
// as a SequenceExpression | ||
var nameExpression, attributesExpression; | ||
if (expression instanceof expressions.SequenceExpression) { | ||
nameExpression = expression.args[0]; | ||
attributesExpression = expression.args[1]; | ||
} else { | ||
nameExpression = expression; | ||
} | ||
var viewAttributes = viewAttributesFromExpression(attributesExpression); | ||
var componentHooks = componentHooksFromAttributes(viewAttributes); | ||
// A ViewInstance has a static name, and a DynamicViewInstance gets its name | ||
// at render time | ||
var viewInstance; | ||
if (nameExpression instanceof expressions.LiteralExpression) { | ||
var name = nameExpression.get(); | ||
// Will throw if the view can't be found immediately | ||
findView(name); | ||
viewInstance = new templates.ViewInstance(name, viewAttributes, componentHooks.hooks, componentHooks.initHooks); | ||
} else { | ||
viewInstance = createDynamicViewInstance(nameExpression, viewAttributes, componentHooks.hooks, componentHooks.initHooks); | ||
} | ||
parseNode.content.push(viewInstance); | ||
} | ||
function viewAttributesFromExpression(expression) { | ||
if (!expression) return; | ||
var object = (expression instanceof expressions.ObjectExpression) ? expression.properties : | ||
(expression instanceof expressions.LiteralExpression) ? expression.value : null; | ||
if (typeof object !== 'object') unexpected(); | ||
var viewAttributes = {}; | ||
for (var key in object) { | ||
var value = object[key]; | ||
viewAttributes[key] = | ||
(value instanceof expressions.LiteralExpression) ? value.value : | ||
(value instanceof expressions.Expression) ? new expressions.ViewParentExpression(value) : | ||
value; | ||
} | ||
return viewAttributes; | ||
} | ||
function ParseNode(view, parent) { | ||
this.view = view; | ||
this.parent = parent; | ||
this.content = []; | ||
this.namespaceUri = parent && parent.namespaceUri; | ||
} | ||
ParseNode.prototype.child = function() { | ||
return new ParseNode(this.view, this); | ||
}; | ||
ParseNode.prototype.last = function() { | ||
return this.content[this.content.length - 1]; | ||
}; | ||
function escapeBraced(source) { | ||
var out = ''; | ||
parseText(source, onLiteral, onExpression, 'string'); | ||
function onLiteral(text) { | ||
out += text; | ||
} | ||
function onExpression(text) { | ||
var escaped = text.replace(/[&<]/g, function(match) { | ||
return (match === '&') ? '&' : '<'; | ||
}); | ||
out += '{{' + escaped + '}}'; | ||
} | ||
return out; | ||
} | ||
function unescapeBraced(source) { | ||
return source.replace(/(?:&|<)/g, function(match) { | ||
return (match === '&') ? '&' : '<'; | ||
}); | ||
} | ||
function unescapeTextLiteral(text, environment) { | ||
return (environment === 'html' || environment === 'attribute') ? | ||
htmlUtil.unescapeEntities(text) : | ||
text; | ||
} | ||
function parseText(data, onLiteral, onExpression, environment) { | ||
var current = data; | ||
var last; | ||
while (current) { | ||
if (current === last) throw new Error('Error parsing template text: ' + data); | ||
last = current; | ||
var start = current.indexOf('{{'); | ||
if (start === -1) { | ||
var unescapedCurrent = unescapeTextLiteral(current, environment); | ||
onLiteral(unescapedCurrent); | ||
return; | ||
} | ||
var end = matchBraces(current, 2, start, '{', '}'); | ||
if (end === -1) throw new Error('Mismatched braces in: ' + data); | ||
if (start > 0) { | ||
var before = current.slice(0, start); | ||
var unescapedBefore = unescapeTextLiteral(before, environment); | ||
onLiteral(unescapedBefore); | ||
} | ||
var inside = current.slice(start + 2, end - 2); | ||
if (inside) { | ||
var unescapedInside = unescapeBraced(inside); | ||
unescapedInside = unescapeTextLiteral(unescapedInside, environment); | ||
onExpression(unescapedInside, environment); | ||
} | ||
current = current.slice(end); | ||
} | ||
} | ||
function matchBraces(text, num, i, openChar, closeChar) { | ||
i += num; | ||
while (num) { | ||
var close = text.indexOf(closeChar, i); | ||
var open = text.indexOf(openChar, i); | ||
var hasClose = close !== -1; | ||
var hasOpen = open !== -1; | ||
if (hasClose && (!hasOpen || (close < open))) { | ||
i = close + 1; | ||
num--; | ||
continue; | ||
} else if (hasOpen) { | ||
i = open + 1; | ||
num++; | ||
continue; | ||
} else { | ||
return -1; | ||
} | ||
} | ||
return i; | ||
} | ||
var blockRegExp = /^(if|unless|else if|each|with|on)\s+([\s\S]+?)(?:\s+as\s+([^,\s]+)\s*(?:,\s*(\S+))?)?$/; | ||
var valueRegExp = /^(?:(view|unbound|bound|unescaped)\s+)?([\s\S]*)/; | ||
function createExpression(source) { | ||
source = source.trim(); | ||
var meta = new expressions.ExpressionMeta(source); | ||
// Parse block expression // | ||
// The block expressions `if`, `unless`, `else if`, `each`, `with`, and `on` | ||
// must have a single blockType keyword and a path. They may have an optional | ||
// alias assignment | ||
var match = blockRegExp.exec(source); | ||
var path, as, keyAs; | ||
if (match) { | ||
meta.blockType = match[1]; | ||
path = match[2]; | ||
as = match[3]; | ||
keyAs = match[4]; | ||
// The blocks `else`, `unbound`, and `bound` may not have a path or alias | ||
} else if (source === 'else' || source === 'unbound' || source === 'bound') { | ||
meta.blockType = source; | ||
// Any source that starts with a `/` is treated as an end block. Either a | ||
// `{{/}}` with no following characters or a `{{/if}}` style ending is valid | ||
} else if (source.charAt(0) === '/') { | ||
meta.isEnd = true; | ||
meta.blockType = source.slice(1).trim() || 'end'; | ||
// Parse value expression // | ||
// A value expression has zero or many keywords and an expression | ||
} else { | ||
path = source; | ||
var keyword; | ||
do { | ||
match = valueRegExp.exec(path); | ||
keyword = match[1]; | ||
path = match[2]; | ||
if (keyword === 'unescaped') { | ||
meta.unescaped = true; | ||
} else if (keyword === 'unbound' || keyword === 'bound') { | ||
meta.bindType = keyword; | ||
} else if (keyword) { | ||
meta.valueType = keyword; | ||
} | ||
} while (keyword); | ||
} | ||
// Wrap parsing in a try / catch to add context to message when throwing | ||
var expression; | ||
try { | ||
expression = (path) ? | ||
createPathExpression(path) : | ||
new expressions.Expression(); | ||
if (as) { | ||
meta.as = parseAlias(as); | ||
} | ||
if (keyAs) { | ||
meta.keyAs = parseAlias(keyAs); | ||
} | ||
} catch (err) { | ||
var message = '\n\nWithin expression: ' + source; | ||
throw appendErrorMessage(err, message); | ||
} | ||
expression.meta = meta; | ||
return expression; | ||
} | ||
function unexpected(source) { | ||
throw new Error('Error parsing template: ' + source); | ||
} | ||
function appendErrorMessage(err, message) { | ||
if (err instanceof Error) { | ||
err.message += message; | ||
return err; | ||
} | ||
return new Error(err + message); | ||
} | ||
function parseAlias(source) { | ||
// Try parsing into a path expression. This throws on invalid expressions. | ||
var expression = createPathExpression(source); | ||
// Verify that it's an AliasPathExpression with no segments, i.e. that | ||
// it has the format "#IDENTIFIER". | ||
if (expression instanceof expressions.AliasPathExpression) { | ||
if (expression.segments.length === 0) { | ||
return expression.alias; | ||
} | ||
throw new Error('Alias must not have dots or brackets: ' + source); | ||
} | ||
throw new Error('Alias must be an identifier starting with "#": ' + source); | ||
} | ||
App.prototype.addViews = function(file, namespace) { | ||
@@ -8,0 +854,0 @@ var views = exports.parseViews(file, namespace); |
@@ -1,2 +0,5 @@ | ||
// TODO: Refactor and include derby-templates module in derby itself | ||
module.exports = require('derby-templates'); | ||
exports.contexts = require('./contexts'); | ||
exports.expressions = require('./expressions'); | ||
exports.operatorFns = require('./operatorFns'); | ||
exports.options = require('./dependencyOptions'); | ||
exports.templates = require('./templates'); |
{ | ||
"name": "derby", | ||
"description": "MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.", | ||
"version": "2.1.0", | ||
"version": "2.2.0", | ||
"homepage": "http://derbyjs.com/", | ||
@@ -11,6 +11,11 @@ "repository": { | ||
"main": "index.js", | ||
"files": [ | ||
"*.js", | ||
"lib/**/*.js", | ||
"test-utils/**/*.js" | ||
], | ||
"scripts": { | ||
"checks": "npm run lint && npm test", | ||
"lint": "npx eslint *.js lib/**/*.js test/**/*.js test-utils/**/*.js", | ||
"test": "npx mocha test/all/*.mocha.js test/dom/*.mocha.js test/server/*.mocha.js", | ||
"lint": "npx eslint '**/*.js'", | ||
"test": "npx mocha 'test/all/**/*.mocha.js' 'test/dom/**/*.mocha.js' 'test/server/**/*.mocha.js'", | ||
"test-browser": "node test/server.js" | ||
@@ -20,4 +25,3 @@ }, | ||
"chokidar": "^3.5.3", | ||
"derby-parsing": "^0.8.0", | ||
"derby-templates": "^0.8.1", | ||
"esprima-derby": "^0.1.0", | ||
"html-util": "^0.2.3", | ||
@@ -27,2 +31,3 @@ "qs": "^6.11.0", | ||
"resolve": "^1.22.1", | ||
"serialize-object": "^1.0.0", | ||
"through": "^2.3.8", | ||
@@ -29,0 +34,0 @@ "tracks": "^0.5.8" |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
277471
7223
4
35
+ Addedesprima-derby@^0.1.0
+ Addedserialize-object@^1.0.0
- Removedderby-parsing@^0.8.0
- Removedderby-templates@^0.8.1
- Removedderby-parsing@0.8.0(transitive)
- Removedderby-templates@0.8.4(transitive)
- Removedsaddle@0.9.7(transitive)