UNPKG

vash

Version:

Razor syntax for JS templating

1,215 lines (1,034 loc) 30.6 kB
var debug = require('debug'); var tks = require('./tokens'); var nodestuff = require('./nodestuff'); var error = require('./error'); var namer = require('./util/fn-namer'); var ProgramNode = namer(require('./nodes/program')); var TextNode = namer(require('./nodes/text')); var MarkupNode = namer(require('./nodes/markup')); var MarkupCommentNode = namer(require('./nodes/markupcomment')); var MarkupContentNode = namer(require('./nodes/markupcontent')); var MarkupAttributeNode = namer(require('./nodes/markupattribute')); var ExpressionNode = namer(require('./nodes/expression')); var ExplicitExpressionNode = namer(require('./nodes/explicitexpression')); var IndexExpressionNode = namer(require('./nodes/indexexpression')); var LocationNode = namer(require('./nodes/location')); var BlockNode = namer(require('./nodes/block')); var CommentNode = namer(require('./nodes/comment')); var RegexNode = namer(require('./nodes/regex')); function Parser(opts) { this.lg = debug('vash:parser'); this.tokens = []; this.deferredTokens = []; this.node = null; this.stack = []; this.inputText = ''; this.opts = opts || {}; this.previousNonWhitespace = null } module.exports = Parser; Parser.prototype.decorateError = function(err, line, column) { err.message = '' + err.message + ' at template line ' + line + ', column ' + column + '\n\n' + 'Context: \n' + error.context(this.inputText, line, column, '\n') + '\n'; return err; } Parser.prototype.write = function(tokens) { if (!Array.isArray(tokens)) tokens = [tokens]; this.inputText += tokens.map(function(tok) { return tok.val; }).join(''); this.tokens.unshift.apply(this.tokens, tokens.reverse()); } Parser.prototype.read = function() { if (!this.tokens.length && !this.deferredTokens.length) return null; if (!this.node) { this.openNode(new ProgramNode()); this.openNode(new MarkupNode(), this.node.body); this.node._finishedOpen = true; this.node.name = 'text'; updateLoc(this.node, { line: 0, chr: 0 }) this.openNode(new MarkupContentNode(), this.node.values); updateLoc(this.node, { line: 0, chr: 0 }) } var curr = this.deferredTokens.pop() || this.tokens.pop(); // To find this value we must search through both deferred and // non-deferred tokens, since there could be more than just 3 // deferred tokens. // nextNonWhitespaceOrNewline var nnwon = null; for (var i = this.deferredTokens.length-1; i >= 0; i--) { if ( nnwon && nnwon.type !== tks.WHITESPACE && nnwon.type !== tks.NEWLINE ) break; nnwon = this.deferredTokens[i]; } for (var i = this.tokens.length-1; i >= 0; i--) { if ( nnwon && nnwon.type !== tks.WHITESPACE && nnwon.type !== tks.NEWLINE ) break; nnwon = this.tokens[i]; } var next = this.deferredTokens.pop() || this.tokens.pop(); var ahead = this.deferredTokens.pop() || this.tokens.pop(); var dispatch = 'continue' + this.node.constructor.name; this.lg('Read: %s', dispatch); this.lg(' curr %s', curr); this.lg(' next %s', next); this.lg(' ahead %s', ahead); this.lg(' nnwon %s', nnwon); if (curr._considerEscaped) { this.lg(' Previous token was marked as escaping'); } var consumed = this[dispatch](this.node, curr, next, ahead, nnwon); if (ahead) { // ahead may be undefined when about to run out of tokens. this.deferredTokens.push(ahead); } if (next) { // Next may be undefined when about to run out of tokens. this.deferredTokens.push(next); } if (!consumed) { this.lg('Deferring curr %s', curr); this.deferredTokens.push(curr); } else { if (curr.type !== tks.WHITESPACE) { this.lg('set previousNonWhitespace %s', curr); this.previousNonWhitespace = curr; } // Poor man's ASI. if (curr.type === tks.NEWLINE) { this.lg('set previousNonWhitespace %s', null); this.previousNonWhitespace = null; } if (!curr._considerEscaped && curr.type === tks.BACKSLASH) { next._considerEscaped = true; } } } Parser.prototype.checkStack = function() { // Throw if something is unclosed that should be. var i = this.stack.length-1; var node; var msg; // A full AST is always: // Program, Markup, MarkupContent, ... while(i >= 2) { node = this.stack[i]; if (node.endOk && !node.endOk()) { // Attempt to make the error readable delete node.values; msg = 'Found unclosed ' + node.type; var err = new Error(msg); err.name = 'UnclosedNodeError'; throw this.decorateError( err, node.startloc.line, node.startloc.column); } i--; } } // This is purely a utility for debugging, to more easily inspect // what happened while parsing something. Parser.prototype.flag = function(node, name, value) { var printVal = (value && typeof value === 'object') ? value.type : value; this.lg('Flag %s on node %s was %s now %s', name, node.type, node[name], printVal); node[name] = value; } Parser.prototype.dumpAST = function() { if (!this.stack.length) { var msg = 'No AST to dump.'; throw new Error(msg); } return JSON.stringify(this.stack[0], null, ' '); } Parser.prototype.openNode = function(node, opt_insertArr) { this.stack.push(node); this.lg('Opened node %s from %s', node.type, (this.node ? this.node.type : null)); this.node = node; if (opt_insertArr) { opt_insertArr.push(node); } return node; } Parser.prototype.closeNode = function(node) { var toClose = this.stack[this.stack.length-1]; if (node !== toClose) { var msg = 'InvalidCloseAction: ' + 'Expected ' + node.type + ' in stack, instead found ' + toClose.type; throw new Error(msg); } this.stack.pop(); var last = this.stack[this.stack.length-1]; this.lg('Closing node %s (%s), returning to node %s', node.type, node.name, last.type) this.node = last; } Parser.prototype.continueCommentNode = function(node, curr, next) { var valueNode = ensureTextNode(node.values); if (curr.type === tks.AT_STAR_OPEN && !node._waitingForClose) { this.flag(node, '_waitingForClose', tks.AT_STAR_CLOSE) updateLoc(node, curr); return true; } if (curr.type === node._waitingForClose) { this.flag(node, '_waitingForClose', null) updateLoc(node, curr); this.closeNode(node); return true; } if (curr.type === tks.DOUBLE_FORWARD_SLASH && !node._waitingForClose){ this.flag(node, '_waitingForClose', tks.NEWLINE); updateLoc(node, curr); return true; } appendTextValue(valueNode, curr); return true; } Parser.prototype.continueMarkupNode = function(node, curr, next) { var valueNode = node.values[node.values.length-1]; if (curr.type === tks.LT_SIGN && !node._finishedOpen) { updateLoc(node, curr); return true; } if ( !node._finishedOpen && curr.type !== tks.GT_SIGN && curr.type !== tks.LT_SIGN && curr.type !== tks.WHITESPACE && curr.type !== tks.NEWLINE && curr.type !== tks.HTML_TAG_VOID_CLOSE ) { // Assume tag name if ( curr.type === tks.AT && !curr._considerEscaped && next && next.type === tks.AT ) { next._considerEscaped = true; return true; } if (curr.type === tks.AT && !curr._considerEscaped) { this.flag(node, 'expression', this.openNode(new ExpressionNode())); updateLoc(node.expression, curr); return true; } node.name = node.name ? node.name + curr.val : curr.val; updateLoc(node, curr); return true; } if (curr.type === tks.GT_SIGN && !node._waitingForFinishedClose) { this.flag(node, '_finishedOpen', true); if (MarkupNode.isVoid(node.name)) { this.flag(node, 'isVoid', true); this.closeNode(node); updateLoc(node, curr); } else { valueNode = this.openNode(new MarkupContentNode(), node.values); updateLoc(valueNode, curr); } return true; } if (curr.type === tks.GT_SIGN && node._waitingForFinishedClose) { this.flag(node, '_waitingForFinishedClose', false); this.closeNode(node); updateLoc(node, curr); return true; } // </VOID if ( curr.type === tks.HTML_TAG_CLOSE && next && next.type === tks.IDENTIFIER && MarkupNode.isVoid(next.val) ) { throw newUnexpectedClosingTagError(this, curr, curr.val + next.val); } // </ if (curr.type === tks.HTML_TAG_CLOSE) { this.flag(node, '_waitingForFinishedClose', true); this.flag(node, 'isClosed', true); return true; } // --> if (curr.type === tks.HTML_COMMENT_CLOSE) { this.flag(node, '_waitingForFinishedClose', false); this.closeNode(node); return false; } if (curr.type === tks.HTML_TAG_VOID_CLOSE) { this.closeNode(node); this.flag(node, 'isVoid', true); this.flag(node, 'voidClosed', true); this.flag(node, 'isClosed', true); updateLoc(node, curr); return true; } if (node._waitingForFinishedClose) { this.lg('Ignoring %s while waiting for closing GT_SIGN', curr); return true; } if ( (curr.type === tks.WHITESPACE || curr.type === tks.NEWLINE) && !node._finishedOpen && next.type !== tks.HTML_TAG_VOID_CLOSE && next.type !== tks.GT_SIGN && next.type !== tks.NEWLINE && next.type !== tks.WHITESPACE ) { // enter attribute valueNode = this.openNode(new MarkupAttributeNode(), node.attributes); updateLoc(valueNode, curr); return true; } // Whitespace between attributes should be ignored. if ( (curr.type === tks.WHITESPACE || curr.type === tks.NEWLINE) && !node._finishedOpen ) { updateLoc(node, curr); return true; } // Can't really have non-markupcontent within markup, so implicitly open // a node. #68. if (node._finishedOpen) { valueNode = this.openNode(new MarkupContentNode(), this.node.values); updateLoc(valueNode, curr); return false; // defer } // Default //valueNode = ensureTextNode(node.values); //appendTextValue(valueNode, curr); //return true; } Parser.prototype.continueMarkupAttributeNode = function(node, curr, next) { var valueNode; if ( curr.type === tks.AT && !curr._considerEscaped && next && next.type === tks.AT ) { next._considerEscaped = true; return true; } if (curr.type === tks.AT && !curr._considerEscaped) { // To expression valueNode = this.openNode(new ExpressionNode(), !node._finishedLeft ? node.left : node.right); updateLoc(valueNode, curr); return true; } // End of left, value only if ( !node._expectRight && (curr.type === tks.WHITESPACE || curr.type === tks.GT_SIGN || curr.type === tks.HTML_TAG_VOID_CLOSE) ) { this.flag(node, '_finishedLeft', true); updateLoc(node, curr); this.closeNode(node); return false; // defer } // End of left. if (curr.type === tks.EQUAL_SIGN && !node._finishedLeft) { this.flag(node, '_finishedLeft', true); this.flag(node, '_expectRight', true); return true; } // Beginning of quoted value. if ( node._expectRight && !node.rightIsQuoted && (curr.type === tks.DOUBLE_QUOTE || curr.type === tks.SINGLE_QUOTE) ) { this.flag(node, 'rightIsQuoted', curr.val); return true; } // End of quoted value. if (node.rightIsQuoted === curr.val) { updateLoc(node, curr); this.closeNode(node); return true; } // Default if (!node._finishedLeft) { valueNode = ensureTextNode(node.left); } else { valueNode = ensureTextNode(node.right); } appendTextValue(valueNode, curr); return true; } Parser.prototype.continueMarkupContentNode = function(node, curr, next, ahead) { var valueNode = ensureTextNode(node.values); if (curr.type === tks.HTML_COMMENT_OPEN) { valueNode = this.openNode(new MarkupCommentNode(), node.values); updateLoc(valueNode, curr); return false; } if (curr.type === tks.HTML_COMMENT_CLOSE) { updateLoc(node, curr); this.closeNode(node); return false; } if (curr.type === tks.AT_COLON && !curr._considerEscaped) { this.flag(node, '_waitingForNewline', true); updateLoc(valueNode, curr); return true; } if (curr.type === tks.NEWLINE && node._waitingForNewline === true) { this.flag(node, '_waitingForNewline', false); appendTextValue(valueNode, curr); updateLoc(node, curr); this.closeNode(node); return true; } if ( curr.type === tks.AT && !curr._considerEscaped && next.type === tks.BRACE_OPEN ) { valueNode = this.openNode(new BlockNode(), node.values); updateLoc(valueNode, curr); return true; } if ( curr.type === tks.AT && !curr._considerEscaped && (next.type === tks.BLOCK_KEYWORD || next.type === tks.FUNCTION) ) { valueNode = this.openNode(new BlockNode(), node.values); updateLoc(valueNode, curr); return true; } // Mark @@: or @@ as escaped. if ( curr.type === tks.AT && !curr._considerEscaped && next && ( next.type === tks.AT_COLON || next.type === tks.AT || next.type === tks.AT_STAR_OPEN ) ) { next._considerEscaped = true; return true; } // @something if (curr.type === tks.AT && !curr._considerEscaped) { valueNode = this.openNode(new ExpressionNode(), node.values); updateLoc(valueNode, curr); return true; } if (curr.type === tks.AT_STAR_OPEN && !curr._considerEscaped) { this.openNode(new CommentNode(), node.values); return false; } var parent = this.stack[this.stack.length-2]; // If this MarkupContent is the direct child of a block, it has no way to // know when to close. So in this case it should assume a } means it's // done. Or if it finds a closing html tag, of course. if ( curr.type === tks.HTML_TAG_CLOSE || (curr.type === tks.BRACE_CLOSE && parent && parent.type === 'VashBlock') ) { this.closeNode(node); updateLoc(node, curr); return false; } if ( curr.type === tks.LT_SIGN && next && ( // If next is an IDENTIFIER, then try to ensure that it's likely an HTML // tag, which really can only be something like: // <identifier> // <identifer morestuff (whitespace) // <identifier\n // <identifier@ // <identifier- // <identifier:identifier // XML namespaces etc etc (next.type === tks.IDENTIFIER && ahead && ( ahead.type === tks.GT_SIGN || ahead.type === tks.WHITESPACE || ahead.type === tks.NEWLINE || ahead.type === tks.AT || ahead.type === tks.UNARY_OPERATOR || ahead.type === tks.COLON ) ) || next.type === tks.AT) ) { // TODO: possibly check for same tag name, and if HTML5 incompatible, // such as p within p, then close current. valueNode = this.openNode(new MarkupNode(), node.values); updateLoc(valueNode, curr); return false; } // Ignore whitespace if the direct parent is a block. This is for backwards // compatibility with { @what() }, where the ' ' between ) and } should not // be included as content. This rule should not be followed if the // whitespace is contained within an @: escape or within favorText mode. if ( curr.type === tks.WHITESPACE && !node._waitingForNewline && !this.opts.favorText && parent && parent.type === 'VashBlock' ) { return true; } appendTextValue(valueNode, curr); return true; } Parser.prototype.continueMarkupCommentNode = function(node, curr, next) { var valueNode = node.values[node.values.length-1]; if (curr.type === tks.HTML_COMMENT_OPEN) { this.flag(node, '_finishedOpen', true); this.flag(node, '_waitingForClose', tks.HTML_COMMENT_CLOSE); updateLoc(node, curr); valueNode = this.openNode(new MarkupContentNode(), node.values); return true; } if (curr.type === tks.HTML_COMMENT_CLOSE && node._finishedOpen) { this.flag(node, '_waitingForClose', null); updateLoc(node, curr); this.closeNode(node); return true; } valueNode = ensureTextNode(node.values); appendTextValue(valueNode, curr); return true; } Parser.prototype.continueExpressionNode = function(node, curr, next) { var valueNode = node.values[node.values.length-1]; var pnw = this.previousNonWhitespace; if ( curr.type === tks.AT && next.type === tks.HARD_PAREN_OPEN ) { // LEGACY: @[], which means a legacy escape to content. updateLoc(node, curr); this.closeNode(node); return true; } if (curr.type === tks.PAREN_OPEN) { this.openNode(new ExplicitExpressionNode(), node.values); return false; } if ( curr.type === tks.HARD_PAREN_OPEN && node.values[0] && node.values[0].type === 'VashExplicitExpression' ) { // @()[0], hard parens should be content updateLoc(node, curr); this.closeNode(node); return false; } if ( curr.type === tks.HARD_PAREN_OPEN && next.type === tks.HARD_PAREN_CLOSE ) { // [], empty index should be content (php forms...) updateLoc(node, curr); this.closeNode(node); return false; } if (curr.type === tks.HARD_PAREN_OPEN) { this.openNode(new IndexExpressionNode(), node.values); return false; } if ( curr.type === tks.FORWARD_SLASH && pnw && pnw.type === tks.AT ) { this.openNode(new RegexNode(), node.values) return false; } // Default // Consume only specific cases, otherwise close. if (curr.type === tks.PERIOD && next && next.type === tks.IDENTIFIER) { valueNode = ensureTextNode(node.values); appendTextValue(valueNode, curr); return true; } if (curr.type === tks.IDENTIFIER) { if (node.values.length > 0 && valueNode && valueNode.type !== 'VashText') { // Assume we just ended an explicit expression. this.closeNode(node); return false; } valueNode = ensureTextNode(node.values); appendTextValue(valueNode, curr); return true; } else { this.closeNode(node); return false; } } Parser.prototype.continueExplicitExpressionNode = function(node, curr, next) { var valueNode = node.values[node.values.length-1]; if ( node.values.length === 0 && (curr.type === tks.AT || curr.type === tks.PAREN_OPEN) && !node._waitingForParenClose ) { // This is the beginning of the explicit (mark as consumed) this.flag(node, '_waitingForParenClose', true); updateLoc(node, curr); return true; } if (curr.type === tks.PAREN_OPEN && !node._waitingForEndQuote) { // New explicit expression valueNode = this.openNode(new ExplicitExpressionNode(), node.values); updateLoc(valueNode, curr); // And do nothing with the token (mark as consumed) return true; } if (curr.type === tks.PAREN_CLOSE && !node._waitingForEndQuote) { // Close current explicit expression this.flag(node, '_waitingForParenClose', false); updateLoc(node, curr); this.closeNode(node); // And do nothing with the token (mark as consumed) return true; } if (curr.type === tks.FUNCTION && !node._waitingForEndQuote) { valueNode = this.openNode(new BlockNode(), node.values); updateLoc(valueNode, curr); return false; } if ( curr.type === tks.LT_SIGN && next.type === tks.IDENTIFIER && !node._waitingForEndQuote ) { // Markup within expression valueNode = this.openNode(new MarkupNode(), node.values); updateLoc(valueNode, curr); return false; } var pnw = this.previousNonWhitespace; if ( curr.type === tks.FORWARD_SLASH && !node._waitingForEndQuote && pnw && pnw.type !== tks.IDENTIFIER && pnw.type !== tks.NUMERAL && pnw.type !== tks.PAREN_CLOSE ) { valueNode = this.openNode(new RegexNode(), node.values); updateLoc(valueNode, curr); return false; } // Default valueNode = ensureTextNode(node.values); if ( !node._waitingForEndQuote && (curr.type === tks.SINGLE_QUOTE || curr.type === tks.DOUBLE_QUOTE) ) { this.flag(node, '_waitingForEndQuote', curr.val); appendTextValue(valueNode, curr); return true; } if ( curr.val === node._waitingForEndQuote && !curr._considerEscaped ) { this.flag(node, '_waitingForEndQuote', null); appendTextValue(valueNode, curr); return true; } appendTextValue(valueNode, curr); return true; } Parser.prototype.continueRegexNode = function(node, curr, next) { var valueNode = ensureTextNode(node.values); if ( curr.type === tks.FORWARD_SLASH && !node._waitingForForwardSlash && !curr._considerEscaped ) { // Start of regex. this.flag(node, '_waitingForForwardSlash', true); appendTextValue(valueNode, curr); return true; } if ( curr.type === tks.FORWARD_SLASH && node._waitingForForwardSlash && !curr._considerEscaped ) { // "End" of regex. this.flag(node, '_waitingForForwardSlash', null); this.flag(node, '_waitingForFlags', true); appendTextValue(valueNode, curr); return true; } if (node._waitingForFlags) { this.flag(node, '_waitingForFlags', null); this.closeNode(node); if (curr.type === tks.IDENTIFIER) { appendTextValue(valueNode, curr); return true; } else { return false; } } if ( curr.type === tks.BACKSLASH && !curr._considerEscaped ) { next._considerEscaped = true; } appendTextValue(valueNode, curr); return true; } Parser.prototype.continueBlockNode = function(node, curr, next, ahead, nnwon) { var valueNode = node.values[node.values.length-1]; if (curr.type === tks.AT_STAR_OPEN) { this.openNode(new CommentNode(), node.body); return false; } if (curr.type === tks.DOUBLE_FORWARD_SLASH && !node._waitingForEndQuote) { this.openNode(new CommentNode(), node.body); return false; } if ( curr.type === tks.AT_COLON && (!node.hasBraces || node._reachedOpenBrace) ) { valueNode = this.openNode(new MarkupContentNode(), node.values); return false; } if ( (curr.type === tks.BLOCK_KEYWORD || curr.type === tks.FUNCTION) && !node._reachedOpenBrace && !node.keyword ) { this.flag(node, 'keyword', curr.val); return true; } if ( (curr.type === tks.BLOCK_KEYWORD || curr.type === tks.FUNCTION) && !node._reachedOpenBrace ) { // Assume something like if (test) expressionstatement; this.flag(node, 'hasBraces', false); valueNode = this.openNode(new BlockNode(), node.values); updateLoc(valueNode, curr); return false; } if ( (curr.type === tks.BLOCK_KEYWORD || curr.type === tks.FUNCTION) && !node._reachedCloseBrace && node.hasBraces && !node._waitingForEndQuote ) { valueNode = this.openNode(new BlockNode(), node.values); updateLoc(valueNode, curr); return false; } if ( (curr.type === tks.BLOCK_KEYWORD || curr.type === tks.FUNCTION) && node._reachedCloseBrace && !node._waitingForEndQuote ) { valueNode = this.openNode(new BlockNode(), node.tail); updateLoc(valueNode, curr); return false; } if ( curr.type === tks.BRACE_OPEN && !node._reachedOpenBrace && !node._waitingForEndQuote ) { this.flag(node, '_reachedOpenBrace', true); this.flag(node, 'hasBraces', true); if (this.opts.favorText) { valueNode = this.openNode(new MarkupContentNode(), node.values); updateLoc(valueNode, curr); } return true; } if ( curr.type === tks.BRACE_OPEN && !node._waitingForEndQuote ) { valueNode = this.openNode(new BlockNode(), node.values); updateLoc(valueNode, curr); return false; } if ( curr.type === tks.BRACE_CLOSE && node.hasBraces && !node._reachedCloseBrace && !node._waitingForEndQuote ) { updateLoc(node, curr); this.flag(node, '_reachedCloseBrace', true); // Try to leave whitespace where it belongs, and allow `else {` to // be continued as the tail of this block. if ( nnwon && nnwon.type !== tks.BLOCK_KEYWORD ) { this.closeNode(node); } return true; } if ( curr.type === tks.BRACE_CLOSE && !node.hasBraces ) { // Probably something like: // @{ if() <span></span> } this.closeNode(node); updateLoc(node, curr); return false; } if ( curr.type === tks.LT_SIGN && (next.type === tks.AT || next.type === tks.IDENTIFIER) && !node._waitingForEndQuote && node._reachedCloseBrace ) { this.closeNode(node); updateLoc(node, curr); return false; } if ( curr.type === tks.LT_SIGN && (next.type === tks.AT || next.type === tks.IDENTIFIER) && !node._waitingForEndQuote && !node._reachedCloseBrace ) { valueNode = this.openNode(new MarkupNode(), node.values); updateLoc(valueNode, curr); return false; } if (curr.type === tks.HTML_TAG_CLOSE) { if ( (node.hasBraces && node._reachedCloseBrace) || !node._reachedOpenBrace ) { updateLoc(node, curr); this.closeNode(node); return false; } // This is likely an invalid markup configuration, something like: // @if(bla) { <img></img> } // where <img> is an implicit void. Try to help the user in this // specific case. if ( next && next.type === tks.IDENTIFIER && MarkupNode.isVoid(next.val) ){ throw newUnexpectedClosingTagError(this, curr, curr.val + next.val); } } if ( curr.type === tks.AT && next.type === tks.PAREN_OPEN ) { // Backwards compatibility, allowing for @for() { @(exp) } valueNode = this.openNode(new ExpressionNode(), node.values); updateLoc(valueNode, curr); return true; } var attachmentNode; if (node._reachedOpenBrace && node._reachedCloseBrace) { attachmentNode = node.tail; } else if (!node._reachedOpenBrace) { attachmentNode = node.head; } else { attachmentNode = node.values; } valueNode = attachmentNode[attachmentNode.length-1]; if ( curr.type === tks.AT && (next.type === tks.BLOCK_KEYWORD || next.type === tks.BRACE_OPEN || next.type === tks.FUNCTION) ) { // Backwards compatibility, allowing for @for() { @for() { @{ } } } valueNode = this.openNode(new BlockNode(), attachmentNode); updateLoc(valueNode, curr); return true; } if ( curr.type === tks.AT && next.type === tks.IDENTIFIER && !node._waitingForEndQuote ) { if (node._reachedCloseBrace) { this.closeNode(node); return false; } else { // something like @for() { @i } valueNode = this.openNode(new MarkupContentNode(), attachmentNode); updateLoc(valueNode, curr); return false; } } if ( curr.type !== tks.BLOCK_KEYWORD && curr.type !== tks.PAREN_OPEN && curr.type !== tks.WHITESPACE && curr.type !== tks.NEWLINE && node.hasBraces && node._reachedCloseBrace ) { // Handle if (test) { } content updateLoc(node, curr); this.closeNode(node); return false; } if (curr.type === tks.PAREN_OPEN) { valueNode = this.openNode(new ExplicitExpressionNode(), attachmentNode); updateLoc(valueNode, curr); return false; } valueNode = ensureTextNode(attachmentNode); if ( curr.val === node._waitingForEndQuote && !curr._considerEscaped ) { this.flag(node, '_waitingForEndQuote', null); appendTextValue(valueNode, curr); return true; } if ( !node._waitingForEndQuote && (curr.type === tks.DOUBLE_QUOTE || curr.type === tks.SINGLE_QUOTE) ) { this.flag(node, '_waitingForEndQuote', curr.val); appendTextValue(valueNode, curr); return true; } var pnw = this.previousNonWhitespace; if ( curr.type === tks.FORWARD_SLASH && !node._waitingForEndQuote && pnw && pnw.type !== tks.IDENTIFIER && pnw.type !== tks.NUMERAL && pnw.type !== tks.PAREN_CLOSE ) { // OH GAWD IT MIGHT BE A REGEX. valueNode = this.openNode(new RegexNode(), attachmentNode); updateLoc(valueNode, curr); return false; } appendTextValue(valueNode, curr); return true; } // These are really only used when continuing on an expression (for now): // @model.what[0]() // And apparently work for array literals... Parser.prototype.continueIndexExpressionNode = function(node, curr, next) { var valueNode = node.values[node.values.length-1]; if (node._waitingForEndQuote) { if (curr.val === node._waitingForEndQuote) { this.flag(node, '_waitingForEndQuote', null); } appendTextValue(valueNode, curr); return true; } if ( curr.type === tks.HARD_PAREN_OPEN && !valueNode ) { this.flag(node, '_waitingForHardParenClose', true); updateLoc(node, curr); return true; } if (curr.type === tks.HARD_PAREN_CLOSE) { this.flag(node, '_waitingForHardParenClose', false); this.closeNode(node); updateLoc(node, curr); return true; } if (curr.type === tks.PAREN_OPEN) { valueNode = this.openNode(new ExplicitExpressionNode(), node.values); updateLoc(valueNode, curr); return false; } valueNode = ensureTextNode(node.values); if (!node._waitingForEndQuote && (curr.type === tks.DOUBLE_QUOTE || curr.type === tks.SINGLE_QUOTE) ) { this.flag(node, '_waitingForEndQuote', curr.val); appendTextValue(valueNode, curr); return true; } // Default. appendTextValue(valueNode, curr); return true; } function updateLoc(node, token) { var loc; loc = new LocationNode(); loc.line = token.line; loc.column = token.chr; if (node.startloc === null) { node.startloc = loc; } node.endloc = loc; } function ensureTextNode(valueList) { var valueNode = valueList[valueList.length-1]; if (!valueNode || valueNode.type !== 'VashText') { valueNode = new TextNode(); valueList.push(valueNode); } return valueNode; } function appendTextValue(textNode, token) { if (!('value' in textNode)) { var msg = 'Expected TextNode but found ' + textNode.type + ' when appending token ' + token; throw new Error(msg); } textNode.value += token.val; updateLoc(textNode, token); } function newUnexpectedClosingTagError(parser, tok, tagName) { var err = new Error('' + 'Found a closing tag for a known void HTML element: ' + tagName + '.'); err.name = 'UnexpectedClosingTagError'; return parser.decorateError( err, tok.line, tok.chr); }