/**
* A tree representation of the template tokens.
*
* @module tree
*/
define(function(require, exports, module) {
"use strict";
var isString = /['"]+/;
// Support.
require("./support/string/trim");
/**
* Represents a Tree.
*
* @class
* @memberOf module:tree
* @param {array} stack - A stack of tokens to parse.
*/
function Tree(stack) {
// Internally use a copy of the stack.
this.stack = stack.slice();
// The root tree node.
this.root = {
type: "Template",
nodes: []
};
}
/**
* Takes in an element from the stack of generated tokens.
*
* @memberOf module:tree.Tree
* @param {object} root - Current token in stack or tree node to process.
* @param {string} END - Token name to cause an expression to end processing.
* @return {object} The root element decorated or null to stop.
*/
Tree.prototype.make = function(root, END) {
root = root || this.root;
var result;
// Pull out the first item in the stack.
while (this.stack.length) {
var node = this.stack.shift();
var prev = root.nodes[root.nodes.length - 1];
switch (node.name) {
case "START_RAW": {
root.nodes.push(this.constructProperty(false));
break;
}
case "START_PROP": {
root.nodes.push(this.constructProperty(true));
break;
}
case "START_EXPR": {
if (result = this.constructExpression(root, END)) {
root.nodes.push(result);
break;
}
// Comments return false.
else if (result !== false) {
return null;
}
break;
}
case "END_EXPR": {
break;
}
default: {
var prevWhitespace = "";
// Detect previous whitespace to condense.
if (prev && prev.type === "Text") {
root.nodes.pop();
prevWhitespace = prev.value;
}
root.nodes.push({
type: "Text",
value: prevWhitespace + node.capture[0]
});
break;
}
}
}
return root;
};
/**
* Build a descriptor to describe an instance of a property.
*
* @memberOf module:tree.Tree
* @param {boolean} encoded - Whether or not to encode this property.
* @return {object} Either a property descriptor or filter pass.
*/
Tree.prototype.constructProperty = function(encoded) {
var propertyDescriptor = {
type: encoded ? "Property" : "RawProperty",
value: "",
args: [],
filters: []
};
var input = "";
// Keep iterating through the stack until END_PROP is found.
while (this.stack.length) {
var node = this.stack.shift();
switch (node.name) {
case "FILTER": {
propertyDescriptor.value = input;
return this.constructFilter(propertyDescriptor);
}
case "END_RAW":
case "END_PROP": {
// Split up the input into space-delimited parts.
var parts = input.trim().split(" ");
// The property name.
propertyDescriptor.value = parts[0];
// Properties can have arguments passed.
propertyDescriptor.args = parts.slice(1);
return propertyDescriptor;
}
default: {
input += node.capture[0];
}
}
}
throw new Error("Unterminated property.");
};
/**
* Build a descriptor to describe an instance of an extend.
*
* @memberOf module:tree.Tree
* @param {object} root - Current token in stack or tree node to process.
* @return {object} The root element decorated.
*/
Tree.prototype.constructExtend = function(root) {
root.type = "ExtendExpression";
// What to return to the compiler.
var value = {
template: "",
partial: ""
};
// Start filling the template first.
var side = "template";
LOOP:
while (this.stack.length) {
var node = this.stack.shift();
switch (node.name) {
case "END_EXPR": {
break LOOP;
}
case "ASSIGN": {
side = "partial";
break;
}
default: {
// Capture the template and partial values.
value[side] += node.capture[0];
}
}
}
// Trim the template and partial values.
value.template = value.template.trim();
value.partial = value.partial.trim();
// Assign this value.
root.value = value;
// If no template, this is an error.
if (!root.value.template) {
throw new Error("Missing valid template name.");
}
// If no partial, this is an error.
if (!root.value.partial) {
throw new Error("Missing valid partial name.");
}
this.make(root, "END_EXTEND");
return root;
};
/**
* Build a descriptor to describe an instance of a partial.
*
* @memberOf module:tree.Tree
* @param {object} root - Current token in stack or tree node to process.
* @return {object} The root element decorated.
*/
Tree.prototype.constructPartial = function(root) {
root.type = "PartialExpression";
// By default isolate the partial from the parent's data.
root.data = "null";
// No node in a partial expression?
delete root.nodes;
// All stringified contents found.
var input = "";
LOOP:
while (this.stack.length) {
var node = this.stack.shift();
switch (node.name) {
case "END_EXPR": {
break LOOP;
}
case "MAGIC": {
// If requested, pass the parent's data to the partial.
root.data = "data";
break;
}
default: {
// Accumulate all values into the input variable, will split later.
input += node.capture[0];
}
}
}
// Split up the input into space-delimited parts.
var parts = input.trim().split(" ");
// The partial name.
root.value = parts[0];
// If no value, this is an error.
if (!root.value) {
throw new Error("Missing valid partials name.");
}
// Partials have arguments passed.
root.args = parts.slice(1);
return root;
};
/**
* Build a descriptor to describe an instance of a filter.
*
* @memberOf module:tree.Tree
* @param {object} root - Current token in stack or tree node to process.
* @return {object} The root element decorated.
*/
Tree.prototype.constructFilter = function(root) {
var current = {
type: "Filter",
args: []
};
var previous = {};
// All stringified contents found.
var input = "";
LOOP:
while (this.stack.length) {
var node = this.stack.shift();
switch (node.name) {
case "END_RAW":
case "END_PROP": {
root.filters.push(current);
break LOOP;
}
// Allow nested filters.
case "FILTER": {
root.filters.push(current);
this.constructFilter(root);
break;
}
// Accumulate all values into the input variable, will split later.
default: {
input += node.capture[0];
}
}
previous = node;
}
// Split up the input into space-delimited parts.
var parts = input.trim().split(" ");
// The partial name.
current.value = parts[0];
// If no value, this is an error.
if (!current.value) {
throw new Error("Missing valid filter name.");
}
// Partials have arguments passed.
current.args = parts.slice(1);
return root;
};
/**
* Build a descriptor to describe an instance of a loop.
*
* @memberOf module:tree.Tree
* @param {object} root - Current token in stack or tree node to process.
* @return {object} The root element decorated.
*/
Tree.prototype.constructEach = function(root) {
root.type = "LoopExpression";
root.conditions = [];
// Find the left side identifier.
var isLeftSide = true;
LOOP:
while (this.stack.length) {
var node = this.stack.shift();
switch (node.name) {
case "ASSIGN": {
isLeftSide = false;
root.conditions.push({
type: "Assignment",
value: node.capture[0].trim()
});
break;
}
case "END_EXPR": {
break LOOP;
}
case "WHITESPACE": {
break;
}
default: {
// If we're on the left hand side and there are already conditions,
// this is the only time we're aren't pushing a value descriptor.
if (isLeftSide && root.conditions.length) {
root.conditions[0].value += node.capture[0].trim();
break;
}
root.conditions.push({
type: "Identifier",
value: node.capture[0].trim()
});
break;
}
}
}
this.make(root, "END_EACH");
return root;
};
/**
* Build a descriptor to describe an instance of a comment.
*
* @memberOf module:tree.Tree
* @param {object} root - Current token in stack or tree node to process.
* @return {object} The root element decorated.
*/
Tree.prototype.constructComment = function(root) {
var previous = {};
while (this.stack.length) {
var node = this.stack.shift();
switch (node.name) {
case "COMMENT": {
if (previous.name === "START_EXPR") {
this.constructComment(root);
break;
}
break;
}
case "END_EXPR": {
if (previous.name === "COMMENT") {
return false;
}
break;
}
}
previous = node;
}
return false;
};
/**
* Build a descriptor to describe an instance of a conditional.
*
* @memberOf module:tree.Tree
* @param {object} root - Current token in stack or tree node to process.
* @param {string} kind - A way to determine else from elsif.
* @return {object} The root element decorated.
*/
Tree.prototype.constructConditional = function(root, kind) {
root.type = root.type || "ConditionalExpression";
root.conditions = root.conditions || [];
var prev = {};
if (kind === "ELSE") {
root.els = { nodes: [] };
return this.make(root.els, "END_IF");
}
if (kind === "ELSIF") {
root.elsif = { nodes: [] };
this.constructConditional(root.elsif, "SKIP");
return this.make(root.elsif, "END_IF");
}
LOOP:
while (this.stack.length) {
var node = this.stack.shift();
var value = node.capture[0].trim();
switch (node.name) {
case "NOT": {
root.conditions.push({
type: "Not"
});
break;
}
case "EQUALITY":
case "NOT_EQUALITY":
case "GREATER_THAN":
case "GREATER_THAN_EQUAL":
case "LESS_THAN":
case "LESS_THAN_EQUAL": {
root.conditions.push({
type: "Equality",
value: node.capture[0].trim()
});
break;
}
case "END_EXPR": {
break LOOP;
}
case "WHITESPACE": {
break;
}
default: {
if (value === "false" || value === "true") {
root.conditions.push({
type: "Literal",
value: value
});
break;
}
// Easy way to determine if the value is NaN or not.
else if (Number(value) === Number(value)) {
root.conditions.push({
type: "Literal",
value: value
});
}
else if (isString.test(value)) {
root.conditions.push({
type: "Literal",
value: value
});
break;
}
else if (prev.type === "Identifier" || prev.type === "Literal") {
prev.value += value;
break;
}
else {
root.conditions.push({
type: "Identifier",
value: value
});
break;
}
}
}
// Store the previous condition object if it exists.
prev = root.conditions[root.conditions.length - 1] || {};
}
if (kind !== "SKIP") {
this.make(root, "END_IF");
}
return root;
};
/**
* Build a descriptor to describe an instance of an expression.
*
* @memberOf module:tree.Tree
* @param {object} root - Current token in stack or tree node to process.
* @param {string} END - Token name to cause an expression to end processing.
* @return {object} The root element decorated.
*/
Tree.prototype.constructExpression = function(root, END) {
var expressionRoot = {
nodes: []
};
// Find the type.
while (this.stack.length) {
var type = this.stack.shift();
switch (type.name) {
// WHEN ANY OF THESE ARE HIT, BREAK OUT.
case END: {
return;
}
case "WHITESPACE": {
break;
}
case "COMMENT": {
return this.constructComment(expressionRoot);
}
case "START_EACH": {
return this.constructEach(expressionRoot);
}
case "ELSIF":
case "ELSE":
case "START_IF": {
if (type.name !== "START_IF") {
expressionRoot = root;
}
return this.constructConditional(expressionRoot, type.name);
}
case "PARTIAL": {
return this.constructPartial(expressionRoot);
}
case "START_EXTEND": {
return this.constructExtend(expressionRoot);
}
default: {
throw new Error("Invalid expression type: " + type.name);
}
}
}
};
module.exports = Tree;
});