UNPKG

vash

Version:

Razor syntax for JS templating

1,840 lines (1,521 loc) 159 kB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.vash = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ var debug = require('debug') var lg = debug('vash:main'); var Lexer = require('./lib/lexer'); var Parser = require('./lib/parser'); var codegen = require('./lib/codegen'); var runtime = require('./runtime'); var helperbatch = require('./lib/helperbatch'); var copyrtl = require('./lib/util/copyrtl'); // Attach all runtime exports to enable backwards compatible behavior, // like `vash.install` to still be accessible in a full build. require('./lib/helpers'); copyrtl(exports, runtime); exports.config = { // Parser Options favorText: false, // TODO: are these even needed with proper codegen? saveAT: false, saveTextTag: false, // Compiler Options useWith: false, htmlEscape: true, helpersName: 'html', modelName: 'model', debug: true, source: null, simple: false, // Runtime options asHelper: false, args: null // Internal, for compiled helpers } exports.version = require('./package.json').version; exports.compileStream = function() { // This could eventually handle waiting until a `null` // is pushed into the lexer, etc. throw new Error('NotImplemented'); } exports.compile = function(markup, options) { if(markup === '' || typeof markup !== 'string') { throw new Error('Empty or non-string cannot be compiled'); } var opts = copyrtl({}, exports.config, options || {}); var l = new Lexer(); l.write(markup); var tokens = l.read(); var p = new Parser(opts); p.write(tokens); var more = true; while(more !== null) more = p.read(); p.checkStack(); // Stash the original input (new lines normalized by the lexer). opts.source = l.originalInput; p.lg(p.dumpAST()); var compiled = codegen(p.stack[0], opts); lg(compiled); var tpl = runtime.link(compiled, opts); return tpl; } /////////////////////////////////////////////////////////////////////////// // VASH.COMPILEHELPER // // Allow multiple helpers to be compiled as templates, for helpers that // do a lot of markup output. // // Takes a template such as: // // vash.helpers.p = function(text){ // <p>@text</p> // } // // And compiles it. The template is then added to `vash.helpers`. // // Returns the compiled templates as named properties of an object. // // This is string manipulation at its... something. It grabs the arguments // and function name using a regex, not actual parsing. Definitely error- // prone, but good enough. This is meant to facilitate helpers with complex // markup, but if something more advanced needs to happen, a plain helper // can be defined and markup added using the manual Buffer API. exports['compileHelper'] = helperbatch.bind(null, 'helper', exports.compile); /////////////////////////////////////////////////////////////////////////// // VASH.COMPILEBATCH // // Allow multiple templates to be contained within the same string. // Templates are separated via a sourceURL-esque string: // // //@batch = tplname/or/path // // The separator is forgiving in terms of whitespace: // // // @ batch=tplname/or/path // // Is just as valid. // // Returns the compiled templates as named properties of an object. exports['compileBatch'] = exports['batch'] = helperbatch.bind(null, 'batch', exports.compile); },{"./lib/codegen":2,"./lib/helperbatch":4,"./lib/helpers":6,"./lib/lexer":9,"./lib/parser":24,"./lib/util/copyrtl":26,"./package.json":35,"./runtime":36,"debug":30}],2:[function(require,module,exports){ var debug = require('debug'); var lg = debug('vash:codegen'); var gens = {} gens.VashProgram = function(node, opts, generate) { return node.body.map(generate).join(''); } gens.VashExplicitExpression = function(node, opts, generate) { var str = node.values.map(generate).join(''); str = '(' + maybeHTMLEscape(node, opts, str) + ')'; if (parentIsContent(node)) { str = bewrap(str); } return str; } gens.VashExpression = function(node, opts, generate) { var str = node.values.map(generate).join(''); str = bewrap(maybeHTMLEscape(node, opts, str)); return str; } gens.VashRegex = function(node, opts, generate) { var str = node.values.map(generate).join(''); str = maybeHTMLEscape(node, opts, str); if (parentIsContent(node)) { str = bewrap(str); } return str; } gens.VashMarkup = function(node, opts, generate) { var isText = node.name === 'text'; var name = node.name ? bcwrap(node.name) : ''; var tagNameValue = name + (node.expression ? generate(node.expression) : ''); var tagOpen = '' + bcwrap('<') + tagNameValue + bcwrap(node.attributes.length ? ' ' : '') + node.attributes.map(generate).join(bcwrap(' ')) var values; var tagClose; if (node.isVoid) { tagOpen += bcwrap(node.voidClosed ? ' />' : '>'); values = ''; tagClose = ''; } else { tagOpen += bcwrap('>'); values = node.values.map(generate).join(''); tagClose = node.isClosed ? bcwrap('</') + tagNameValue + bcwrap('>') : ''; } if (isText) { tagOpen = tagClose = ''; } return '' + (parentIsExpression(node) ? '(function () {' : '') + dbgstart(node, opts) + tagOpen + values + tagClose + dbgend(node, opts) + (parentIsExpression(node) ? '}())' : '') } gens.VashMarkupAttribute = function(node, opts, generate) { var quote = node.rightIsQuoted || ''; quote = escapeMarkupContent(quote); return '' + dbgstart(node, opts) + node.left.map(generate).join('') + (node.right.length || node.rightIsQuoted ? bcwrap('=' + quote) + node.right.map(generate).join('') + bcwrap(quote) : '') + dbgend(node, opts); } gens.VashMarkupContent = function(node, opts, generate) { return '' + dbgstart(node, opts) + node.values.map(generate).join('') + dbgend(node, opts); } gens.VashMarkupComment = function(node, opts, generate) { return '' + bcwrap('<!--') + dbgstart(node, opts) + node.values.map(generate).join('') + dbgend(node, opts) + bcwrap('-->'); } gens.VashBlock = function(node, opts, generate) { var hasValues = node.values.length > 0; var unsafeForDbg = node.keyword === 'switch' || !node.name || !hasValues; var openBrace = hasValues || node.hasBraces ? '{' + (unsafeForDbg ? '' : dbgstart(node, opts)) : ''; var closeBrace = hasValues || node.hasBraces ? (unsafeForDbg ? '' : dbgend(node, opts)) + '}' : ''; return '' + (node.keyword ? node.keyword : '') + node.head.map(generate).join('') + openBrace + node.values.map(generate).join('') + closeBrace + node.tail.map(generate).join(''); } gens.VashIndexExpression = function(node, opts, generate) { var str = node.values.map(generate).join(''); return '[' + str + ']'; } gens.VashText = function(node, opts, generate) { if (!node.value.length) return ''; return parentIsContent(node) ? '' + dbgstart(node, opts) + bcwrap(escapeMarkupContent(node.value)) + dbgend(node, opts) : node.value; } gens.VashComment = function(node, opts, generate) { return ''; } var reQuote = /(['"])/g; var reEscapedQuote = /\\+(["'])/g; var reLineBreak = /\n/g; var reHelpersName = /HELPERSNAME/g; var reModelName = /MODELNAME/g; var reOriginalMarkup = /ORIGINALMARKUP/g; function escapeMarkupContent(str) { return str .replace(/\\/g, '\\\\') .replace(reQuote, '\\$1') .replace(reLineBreak, '\\n'); } var BUFFER_HEAD = '\n__vbuffer.push('; var BUFFER_TAIL = ');\n'; // buffer content wrap function bcwrap(str) { return BUFFER_HEAD + '\'' + str.replace(/\n/, '\\n') + '\'' + BUFFER_TAIL; } // buffer expression wrap function bewrap(str) { return BUFFER_HEAD + str + BUFFER_TAIL; } function parentIsContent(node) { return node.parent.type === 'VashMarkup' || node.parent.type === 'VashMarkupContent' || node.parent.type === 'VashMarkupComment' || node.parent.type === 'VashMarkupAttribute' || node.parent.type === 'VashProgram'; } function parentIsExpression(node) { return node.parent.type === 'VashExpression' || node.parent.type === 'VashExplicitExpression' || node.parent.type === 'VashIndexExpression'; } function dbgstart(node, opts) { return opts.debug ? '' + opts.helpersName + '.vl = ' + node.startloc.line + ', ' + opts.helpersName + '.vc = ' + node.startloc.column + '; \n' : ''; } function dbgend(node, opts) { return opts.debug ? '' + opts.helpersName + '.vl = ' + node.endloc.line + ', ' + opts.helpersName + '.vc = ' + node.endloc.column + '; \n' : ''; } function maybeHTMLEscape(node, opts, str) { if (parentIsContent(node) && opts.htmlEscape) { return opts.helpersName + '.escape(' + str + ').toHtmlString()'; } else { return str; } } function replaceDevTokens(str, opts){ return str .replace( reHelpersName, opts.helpersName ) .replace( reModelName, opts.modelName ); } function head(opts){ var str = '' + (opts.debug ? 'try { \n' : '') + 'var __vbuffer = HELPERSNAME.buffer; \n' + 'HELPERSNAME.options = __vopts; \n' + 'MODELNAME = MODELNAME || {}; \n' + (opts.useWith ? 'with( MODELNAME ){ \n' : ''); str = replaceDevTokens(str, opts); return str; } function helperHead(opts){ var str = '' + (opts.debug ? 'try { \n' : '') + 'var __vbuffer = this.buffer; \n' + 'var MODELNAME = this.model; \n' + 'var HELPERSNAME = this; \n'; str = replaceDevTokens(str, opts); return str; } function tail(opts){ var str = '' + (opts.simple ? 'return HELPERSNAME.buffer.join(""); \n' : ';(__vopts && __vopts.onRenderEnd && __vopts.onRenderEnd(null, HELPERSNAME)); \n' + 'return (__vopts && __vopts.asContext) \n' + ' ? HELPERSNAME \n' + ' : HELPERSNAME.toString(); \n' ) + (opts.useWith ? '} \n' : '') + (opts.debug ? '} catch( e ){ \n' + ' HELPERSNAME.reportError( e, HELPERSNAME.vl, HELPERSNAME.vc, "ORIGINALMARKUP", "!LB!", true ); \n' + '} \n' : ''); str = replaceDevTokens(str, opts) .replace(reOriginalMarkup, escapeForDebug(opts.source)); return str; } function helperTail(opts){ var str = '' + (opts.debug ? '} catch( e ){ \n' + ' HELPERSNAME.reportError( e, HELPERSNAME.vl, HELPERSNAME.vc, "ORIGINALMARKUP", "!LB!", true ); \n' + '} \n' : ''); str = replaceDevTokens(str, opts) .replace(reOriginalMarkup, escapeForDebug(opts.source)); return str; } function escapeForDebug( str ){ return str .replace(reLineBreak, '!LB!') .replace(reQuote, '\\$1') .replace(reEscapedQuote, '\\$1') } // Not necessary, but provides faster execution when not in debug mode // and looks nicer. function condenseContent(str) { return str .replace(/'\);\n+__vbuffer.push\('/g, '') .replace(/\n+/g, '\n'); } function generate(node, opts) { function gen(opts, node) { lg('Entering ' + node.type); var str = gens[node.type](node, opts, genChild); lg('Leaving ' + node.type); return str; function genChild(child) { if (!child.parent) child.parent = node; lg('Generating child type %s of parent type %s', child.type, node.type) return gen(opts, child); } } var generated = gen(opts, node); var body; if(!opts.asHelper){ body = head(opts) + generated + tail(opts); } else { body = helperHead(opts) + generated + helperTail(opts); } return opts.debug ? body : condenseContent(body); } module.exports = generate; },{"debug":30}],3:[function(require,module,exports){ exports.context = function(input, lineno, columnno, linebreak) { linebreak = linebreak || '!LB!'; var lines = input.split(linebreak) , contextSize = lineno === 0 && columnno === 0 ? lines.length - 1 : 3 , start = Math.max(0, lineno - contextSize) , end = Math.min(lines.length, lineno + contextSize); return lines .slice(start, end) .map(function(line, i, all){ var curr = i + start + 1; return (curr === lineno ? ' > ' : ' ') + (curr < 10 ? ' ' : '') + curr + ' | ' + line; }).join('\n'); } },{}],4:[function(require,module,exports){ var slice = Array.prototype.slice , reHelperFuncHead = /vash\.helpers\.([^= ]+?)\s*=\s*function([^(]*?)\(([^)]*?)\)\s*{/ , reHelperFuncTail = /\}$/ , reBatchSeparator = /^\/\/\s*@\s*batch\s*=\s*(.*?)$/ // The logic for compiling a giant batch of templates or several // helpers is nearly exactly the same. The only difference is the // actual compilation method called, and the regular expression that // determines how the giant string is split into named, uncompiled // template strings. module.exports = function compile(type, compile, str, options){ var separator = type === 'helper' ? reHelperFuncHead : reBatchSeparator; var tpls = splitByNamedTpl(separator, str, function(ma, name){ return name.replace(/^\s+|\s+$/, ''); }, type === 'helper' ? true : false); if(tpls){ Object.keys(tpls).forEach(function(path){ tpls[path] = type === 'helper' ? compileSingleHelper(compile, tpls[path], options) : compile('@{' + tpls[path] + '}', options); }); tpls.toClientString = function(){ return Object.keys(tpls).reduce(function(prev, curr){ if(curr === 'toClientString'){ return prev; } return prev + tpls[curr].toClientString() + '\n'; }, '') } } return tpls; } // Given a separator regex and a function to transform the regex result // into a name, take a string, split it, and group the rejoined strings // into an object. // This is useful for taking a string, such as // // // tpl1 // what what // and more // // // tpl2 // what what again // // and returning: // // { // tpl1: 'what what\nand more\n', // tpl2: 'what what again' // } var splitByNamedTpl = function(reSeparator, markup, resultHandler, keepSeparator){ var lines = markup.split(/[\n\r]/g) ,tpls = {} ,paths = [] ,currentPath = '' lines.forEach(function(line, i){ var pathResult = reSeparator.exec(line) ,handlerResult = pathResult ? resultHandler.apply(pathResult, pathResult) : null if(handlerResult){ currentPath = handlerResult; tpls[currentPath] = []; } if((!handlerResult || keepSeparator) && line){ tpls[currentPath].push(line); } }); Object.keys(tpls).forEach(function(key){ tpls[key] = tpls[key].join('\n'); }) return tpls; } var compileSingleHelper = function(compile, str, options){ options = options || {}; // replace leading/trailing spaces, and parse the function head var def = str.replace(/^[\s\n\r]+|[\s\n\r]+$/, '').match(reHelperFuncHead) // split the function arguments, kill all whitespace ,args = def[3].split(',').map(function(arg){ return arg.replace(' ', '') }) ,name = def[1] ,body = str .replace( reHelperFuncHead, '' ) .replace( reHelperFuncTail, '' ) // Wrap body in @{} to simulate it actually being inside a function // definition, since we manually stripped it. Without this, statements // such as `this.what = "what";` that are at the beginning of the body // will be interpreted as markup. body = '@{' + body + '}'; // `args` and `asHelper` inform `vash.compile/link` that this is a helper options.args = args; options.asHelper = name; return compile(body, options); } },{}],5:[function(require,module,exports){ var helpers = require('../../runtime').helpers; /////////////////////////////////////////////////////////////////////////// // EXAMPLE HELPER: syntax highlighting helpers.config.highlighter = null; helpers.highlight = function(lang, cb){ // context (this) is and instance of Helpers, aka a rendering context // mark() returns an internal `Mark` object // Use it to easily capture output... var startMark = this.buffer.mark(); // cb() is simply a user-defined function. It could (and should) contain // buffer additions, so we call it... cb( this.model ); // ... and then use fromMark() to grab the output added by cb(). var cbOutLines = this.buffer.fromMark(startMark); // The internal buffer should now be back to where it was before this // helper started, and the output is completely contained within cbOutLines. this.buffer.push( '<pre><code>' ); if( helpers.config.highlighter ){ this.buffer.push( helpers.config.highlighter(lang, cbOutLines.join('')).value ); } else { this.buffer.push( cbOutLines ); } this.buffer.push( '</code></pre>' ); // returning is allowed, but could cause surprising effects. A return // value will be directly added to the output directly following the above. } },{"../../runtime":36}],6:[function(require,module,exports){ require('./trim'); require('./highlight'); require('./layout'); module.exports = require('../../runtime'); },{"../../runtime":36,"./highlight":5,"./layout":7,"./trim":8}],7:[function(require,module,exports){ (function (global){ var helpers = require('../../runtime').helpers; var copyrtl = require('../util/copyrtl'); // For now, using the layout helpers requires a full build. For now. var vash = require('../../index'); module.exports = vash; /////////////////////////////////////////////////////////////////////////// // LAYOUT HELPERS // semi hacky guard to prevent non-nodejs erroring // switched from window not existing to global existing // to avoid conflict with Jest where window is always defined(!) if( typeof global !== 'undefined' ){ var fs = require('fs') ,path = require('path') } // TRUE implies that all TPLS are loaded and waiting in cache helpers.config.browser = false; vash.loadFile = function(filepath, options, cb){ // options are passed in via Express // { // settings: // { // env: 'development', // 'jsonp callback name': 'callback', // 'json spaces': 2, // views: '/Users/drew/Dropbox/js/vash/test/fixtures/views', // 'view engine': 'vash' // }, // _locals: [Function: locals], // cache: false // } // The only required options are: // // settings: { // views: '' // } options = copyrtl({}, vash.config, options || {}); var browser = helpers.config.browser ,tpl if( !browser && options.settings && options.settings.views ){ // this will really only have an effect on windows filepath = path.normalize( filepath ); if( filepath.indexOf( path.normalize( options.settings.views ) ) === -1 ){ // not an absolute path filepath = path.join( options.settings.views, filepath ); } if( !path.extname( filepath ) ){ filepath += '.' + ( options.settings['view engine'] || 'vash' ) } } // TODO: auto insert 'model' into arguments try { // if browser, tpl must exist in tpl cache tpl = options.cache || browser ? helpers.tplcache[filepath] || ( helpers.tplcache[filepath] = vash.compile(fs.readFileSync(filepath, 'utf8')) ) : vash.compile( fs.readFileSync(filepath, 'utf8') ) cb && cb(null, tpl); } catch(e) { cb && cb(e, null); } } vash.renderFile = vash.__express = function(filepath, options, cb){ vash.loadFile(filepath, options, function(err, tpl){ // auto setup an `onRenderEnd` callback to seal the layout var prevORE = options.onRenderEnd; cb( err, !err && tpl(options, function(err, ctx){ ctx.finishLayout() if( prevORE ) prevORE(err, ctx); }) ); }) } helpers._ensureLayoutProps = function(){ this.appends = this.appends || {}; this.prepends = this.prepends || {}; this.blocks = this.blocks || {}; this.blockMarks = this.blockMarks || {}; } helpers.finishLayout = function(){ this._ensureLayoutProps(); var self = this, name, marks, blocks, prepends, appends, injectMark, m, content, block // each time `.block` is called, a mark is added to the buffer and // the `blockMarks` stack. Find the newest/"highest" mark on the stack // for each named block, and insert the rendered content (prepends, block, appends) // in place of that mark for( name in this.blockMarks ){ marks = this.blockMarks[name]; prepends = this.prepends[name]; blocks = this.blocks[name]; appends = this.appends[name]; injectMark = marks.pop(); // mark current point in buffer in prep to grab rendered content m = this.buffer.mark(); prepends && prepends.forEach(function(p){ self.buffer.pushConcat( p ); }); // a block might never have a callback defined, e.g. is optional // with no default content block = blocks.pop(); block && this.buffer.pushConcat( block ); appends && appends.forEach(function(a){ self.buffer.pushConcat( a ); }); // grab rendered content content = this.buffer.fromMark( m ) // Join, but split out the VASHMARKS so further buffer operations are still // sane. Join is required to prevent max argument errors when large templates // are being used. content = compactContent(content); // Prep for apply, ensure the right location (mark) is used for injection. content.unshift( injectMark, 0 ); this.buffer.spliceMark.apply( this.buffer, content ); } for( name in this.blockMarks ){ // kill all other marks registered as blocks this.blockMarks[name].forEach(function(m){ m.destroy(); }); } // this should only be able to happen once delete this.blockMarks; delete this.prepends; delete this.blocks; delete this.appends; // and return the whole thing return this.toString(); } // Given an array, condense all the strings to as few array elements // as possible, while preserving `Mark`s as individual elements. function compactContent(content) { var re = vash.Mark.re; var parts = []; var str = ''; content.forEach(function(part) { if (re.exec(part)) { parts.push(str, part); str = ''; } else { // Ensure `undefined`s are not `toString`ed str += (part || ''); } }); // And don't forget the rest. parts.push(str); return parts; } helpers.extend = function(path, ctn){ var self = this ,buffer = this.buffer ,origModel = this.model ,layoutCtx; this._ensureLayoutProps(); // this is a synchronous callback vash.loadFile(path, this.model, function(err, tpl){ if (err) throw err; // any content that is outside of a block but within an "extend" // callback is completely thrown away, as the destination for such // content is undefined var start = self.buffer.mark(); ctn(self.model); // ... and just throw it away var content = self.buffer.fromMark( start ) // TODO: unless it's a mark id? Removing everything means a block // MUST NOT be defined in an extend callback //,filtered = content.filter( vash.Mark.uidLike ) //self.buffer.push( filtered ); // `isExtending` is necessary because named blocks in the layout // will be interpreted after named blocks in the content. Since // layout named blocks should only be used as placeholders in the // event that their content is redefined, `block` must know to add // the defined content at the head or tail or the block stack. self.isExtending = true; tpl( self.model, { context: self } ); self.isExtending = false; }); this.model = origModel; } helpers.include = function(name, model){ var self = this ,buffer = this.buffer ,origModel = this.model; // TODO: should this be in a new context? Jade looks like an include // is not shared with parent context // this is a synchronous callback vash.loadFile(name, this.model, function(err, tpl){ if (err) throw err; tpl( model || self.model, { context: self } ); }); this.model = origModel; } helpers.block = function(name, ctn){ this._ensureLayoutProps(); var self = this // ensure that we have a list of marks for this name ,marks = this.blockMarks[name] || ( this.blockMarks[name] = [] ) // ensure a list of blocks for this name ,blocks = this.blocks[name] || ( this.blocks[name] = [] ) ,start ,content; // render out the content immediately, if defined, to attempt to grab // "dependencies" like other includes, blocks, etc if( ctn ){ start = this.buffer.mark(); ctn( this.model ); content = this.buffer.fromMark( start ); // add rendered content to named list of blocks if( content.length && !this.isExtending ){ blocks.push( content ); } // if extending the rendered content must be allowed to be redefined if( content.length && this.isExtending ){ blocks.unshift( content ); } } // mark the current location as "where this block will end up" marks.push( this.buffer.mark( 'block-' + name ) ); } helpers._handlePrependAppend = function( type, name, ctn ){ this._ensureLayoutProps(); var start = this.buffer.mark() ,content ,stack = this[type] ,namedStack = stack[name] || ( stack[name] = [] ) ctn( this.model ); content = this.buffer.fromMark( start ); namedStack.push( content ); } helpers.append = function(name, ctn){ this._handlePrependAppend( 'appends', name, ctn ); } helpers.prepend = function(name, ctn){ this._handlePrependAppend( 'prepends', name, ctn ); } }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"../../index":1,"../../runtime":36,"../util/copyrtl":26,"fs":"fs","path":"path"}],8:[function(require,module,exports){ var helpers = require('../../runtime').helpers; // Trim whitespace from the start and end of a string helpers.trim = function(val){ return val.replace(/^\s*|\s*$/g, ''); } },{"../../runtime":36}],9:[function(require,module,exports){ var debug = require('debug'); var tokens = require('./tokens'); // This pattern and basic lexer code were originally from the // Jade lexer, but have been modified: // https://github.com/visionmedia/jade/blob/master/lib/lexer.js function VLexer(){ this.lg = debug('vash:lexer'); this.input = ''; this.originalInput = ''; this.lineno = 1; this.charno = 0; } module.exports = VLexer; VLexer.prototype = { write: function(input) { var normalized = input.replace(/\r\n|\r/g, '\n'); // Kill BOM if this is the first chunk. if (this.originalInput.length == 0) { normalized = normalized.replace(/^\uFEFF/, ''); } this.input += normalized; this.originalInput += normalized; return true; }, read: function() { var out = [] , result; while(this.input.length) { result = this.advance(); if (result) { out.push(result); this.lg('Read %s at line %d, column %d with content %s', result.type, result.line, result.chr, result.val.replace(/(\n)/, '\\n')); } } return out; }, scan: function(regexp, type){ var captures, token; if (captures = regexp.exec(this.input)) { this.input = this.input.substr((captures[1].length)); token = { type: type ,line: this.lineno ,chr: this.charno ,val: captures[1] || '' ,toString: function(){ return '[' + this.type + ' (' + this.line + ',' + this.chr + '): ' + this.val.replace(/(\n)/, '\\n') + ']'; } }; this.charno += captures[0].length; return token; } } ,advance: function() { var i, name, test, result; for(i = 0; i < tokens.tests.length; i += 2){ test = tokens.tests[i+1]; test.displayName = tokens.tests[i]; if(typeof test === 'function'){ // assume complex callback result = test.call(this); } if(typeof test.exec === 'function'){ // assume regex result = this.scan(test, tokens.tests[i]); } if( result ){ return result; } } } } },{"./tokens":25,"debug":30}],10:[function(require,module,exports){ var Node = module.exports = function BlockNode() { this.type = 'VashBlock'; this.keyword = null; this.head = []; this.values = []; this.tail = []; this.hasBraces = null; this.startloc = null; this.endloc = null; this._reachedOpenBrace = false; this._reachedCloseBrace = false; this._withinCommentLine = false; this._waitingForEndQuote = null; } Node.prototype.endOk = function() { var gradeSchool = this.hasBraces && (!this._reachedOpenBrace || !this._reachedCloseBrace); return (gradeSchool || this._withinCommentLine || this._waitingForEndQuote) ? false : true; } },{}],11:[function(require,module,exports){ var Node = module.exports = function CommentNode() { this.type = 'VashComment'; this.values = []; this.startloc = null; this.endloc = null; this._waitingForClose = null; } Node.prototype.endOk = function() { return this._waitingForClose ? false : true; } },{}],12:[function(require,module,exports){ var Node = module.exports = function ExplicitExpressionNode() { this.type = 'VashExplicitExpression'; this.values = []; this.startloc = null; this.endloc = null; this._waitingForParenClose = null; this._waitingForEndQuote = null; } Node.prototype.endOk = function() { return this._waitingForEndQuote || this._waitingForParenClose ? false : true; } },{}],13:[function(require,module,exports){ var Node = module.exports = function ExpressionNode() { this.type = 'VashExpression'; this.values = []; this.startloc = null; this.endloc = null; } },{}],14:[function(require,module,exports){ var Node = module.exports = function IndexExpressionNode() { this.type = 'VashIndexExpression'; this.values = []; this.startloc = null; this.endloc = null; this._waitingForEndQuote = null; this._waitingForHardParenClose = null; } Node.prototype.endOk = function() { return (this._waitingForEndQuote || this._waitingForHardParenClose) ? false : true; } },{}],15:[function(require,module,exports){ module.exports = function LocationNode() { this.line = 1; this.column = 0; } },{}],16:[function(require,module,exports){ var Node = module.exports = function MarkupNode() { this.type = 'VashMarkup'; this.name = null; this.expression = null; // or ExpressionNode this.attributes = []; this.values = []; this.isVoid = false; this.voidClosed = false; this.isClosed = false; this.startloc = null; this.endloc = null; this._finishedOpen = false; // Waiting for the finishing > of the </close> this._waitingForFinishedClose = false; } var voids = module.exports.voids = [ // Just a little bit of cheating. '!DOCTYPE', '!doctype', 'doctype', // From the spec 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ]; Node.isVoid = function(name) { return voids.indexOf(name) > -1; } // HTML5 allows these to be non-closed. // http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html#generate-implied-end-tags var implieds = [ 'dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt' ] Node.isImplied = function(name) { return implieds.indexOf(name) > -1; } Node.prototype.endOk = function() { if ( this._finishedOpen && (this.isClosed || this.voidClosed) ) { return true; } return false; } },{}],17:[function(require,module,exports){ var Node = module.exports = function MarkupAttributeNode() { this.type = 'VashMarkupAttribute'; this.left = []; this.right = []; this.rightIsQuoted = false; this.startloc = null; this.endloc = null; this._finishedLeft = false; this._expectRight = false; } Node.prototype.endOk = function() { // TODO: this should include expecting right + found quotes or not. return this._finishedLeft ? true : false; } },{}],18:[function(require,module,exports){ var Node = module.exports = function MarkupCommentNode() { this.type = 'VashMarkupComment'; this.values = []; this.startloc = null; this.endloc = null; this._finishedOpen = false this._waitingForClose = null; } Node.prototype.endOk = function() { return this._waitingForClose || this._finishedOpen ? false : true; } },{}],19:[function(require,module,exports){ var Node = module.exports = function MarkupContentNode() { this.type = 'VashMarkupContent'; this.values = []; this.startloc = null; this.endloc = null; this._waitingForNewline = null; } Node.prototype.endOk = function() { return this._waitingForNewline ? false : true; } },{}],20:[function(require,module,exports){ module.exports = function ProgramNode() { this.type = 'VashProgram'; this.body = []; } },{}],21:[function(require,module,exports){ // Need to handle: // if (true) /abc/.test() // if (/abc/.test('what')) // @(/abc/.exec('abc')) // @{ var re = /abc/gi; } // if (a/b) {} // @(a/=b) // Previous is IDENTIFIER or WHITESPACE var Node = module.exports = function RegexNode() { this.type = 'VashRegex'; this.values = []; this.startloc = null; this.endloc = null; this._waitingForForwardSlash = null; this._waitingForFlags = null; } Node.prototype.endOk = function() { return this._waitingForForwardSlash || this._waitingForFlags ? false : true; } },{}],22:[function(require,module,exports){ module.exports = function TextNode() { this.type = 'VashText'; this.value = ''; this.startloc = null; this.endloc = null; } },{}],23:[function(require,module,exports){ function clean(node) { return Object.keys(node).reduce(function(out, key) { var value = node[key]; if (key[0] !== '_' && typeof value !== 'function') { if (Array.isArray(value)) { out[key] = value.map(clean); } else { out[key] = value; } } return out; }, {}); } exports.clean = clean; },{}],24:[function(require,module,exports){ 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 favorTex