| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837 | 'use strict';var doctypes = require('doctypes');var makeError = require('pug-error');var buildRuntime = require('pug-runtime/build');var runtime = require('pug-runtime');var compileAttrs = require('pug-attrs');var selfClosing = require('void-elements');var constantinople = require('constantinople');var stringify = require('js-stringify');var addWith = require('with');// This is used to prevent pretty printing inside certain tagsvar WHITE_SPACE_SENSITIVE_TAGS = {  pre: true,  textarea: true};var INTERNAL_VARIABLES = [  'pug',  'pug_mixins',  'pug_interp',  'pug_debug_filename',  'pug_debug_line',  'pug_debug_sources',  'pug_html'];module.exports = generateCode;module.exports.CodeGenerator = Compiler;function generateCode(ast, options) {  return (new Compiler(ast, options)).compile();}function isConstant(src) {  return constantinople(src, {pug: runtime, 'pug_interp': undefined});}function toConstant(src) {  return constantinople.toConstant(src, {pug: runtime, 'pug_interp': undefined});}/** * Initialize `Compiler` with the given `node`. * * @param {Node} node * @param {Object} options * @api public */function Compiler(node, options) {  this.options = options = options || {};  this.node = node;  this.bufferedConcatenationCount = 0;  this.hasCompiledDoctype = false;  this.hasCompiledTag = false;  this.pp = options.pretty || false;  if (this.pp && typeof this.pp !== 'string') {    this.pp = '  ';  }  if (this.pp && !/^\s+$/.test(this.pp)) {    throw new Error(      'The pretty parameter should either be a boolean or whitespace only string'    );  }  this.debug = false !== options.compileDebug;  this.indents = 0;  this.parentIndents = 0;  this.terse = false;  this.mixins = {};  this.dynamicMixins = false;  this.eachCount = 0;  if (options.doctype) this.setDoctype(options.doctype);  this.runtimeFunctionsUsed = [];  this.inlineRuntimeFunctions = options.inlineRuntimeFunctions || false;  if (this.debug && this.inlineRuntimeFunctions) {    this.runtimeFunctionsUsed.push('rethrow');  }};/** * Compiler prototype. */Compiler.prototype = {  runtime: function (name) {    if (this.inlineRuntimeFunctions) {      this.runtimeFunctionsUsed.push(name);      return 'pug_' + name;    } else {      return 'pug.' + name;    }  },  error: function (message, code, node) {    var err = makeError(code, message, {      line: node.line,      column: node.column,      filename: node.filename,    });    throw err;  },  /**   * Compile parse tree to JavaScript.   *   * @api public   */  compile: function(){    this.buf = [];    if (this.pp) this.buf.push("var pug_indent = [];");    this.lastBufferedIdx = -1;    this.visit(this.node);    if (!this.dynamicMixins) {      // if there are no dynamic mixins we can remove any un-used mixins      var mixinNames = Object.keys(this.mixins);      for (var i = 0; i < mixinNames.length; i++) {        var mixin = this.mixins[mixinNames[i]];        if (!mixin.used) {          for (var x = 0; x < mixin.instances.length; x++) {            for (var y = mixin.instances[x].start; y < mixin.instances[x].end; y++) {              this.buf[y] = '';            }          }        }      }    }    var js = this.buf.join('\n');    var globals = this.options.globals ? this.options.globals.concat(INTERNAL_VARIABLES) : INTERNAL_VARIABLES;    if (this.options.self) {      js = 'var self = locals || {};' + js;    } else {      js = addWith('locals || {}', js, globals.concat(this.runtimeFunctionsUsed.map(function (name) { return 'pug_' + name; })));    }    if (this.debug) {      if (this.options.includeSources) {        js = 'var pug_debug_sources = ' + stringify(this.options.includeSources) + ';\n' + js;      }      js = 'var pug_debug_filename, pug_debug_line;' +        'try {' +        js +        '} catch (err) {' +        (this.inlineRuntimeFunctions ? 'pug_rethrow' : 'pug.rethrow') +        '(err, pug_debug_filename, pug_debug_line' +        (          this.options.includeSources          ? ', pug_debug_sources[pug_debug_filename]'          : ''        ) +        ');' +        '}';    }    return buildRuntime(this.runtimeFunctionsUsed) + 'function ' + (this.options.templateName || 'template') + '(locals) {var pug_html = "", pug_mixins = {}, pug_interp;' + js + ';return pug_html;}';  },  /**   * Sets the default doctype `name`. Sets terse mode to `true` when   * html 5 is used, causing self-closing tags to end with ">" vs "/>",   * and boolean attributes are not mirrored.   *   * @param {string} name   * @api public   */  setDoctype: function(name){    this.doctype = doctypes[name.toLowerCase()] || '<!DOCTYPE ' + name + '>';    this.terse = this.doctype.toLowerCase() == '<!doctype html>';    this.xml = 0 == this.doctype.indexOf('<?xml');  },  /**   * Buffer the given `str` exactly as is or with interpolation   *   * @param {String} str   * @param {Boolean} interpolate   * @api public   */  buffer: function (str) {    var self = this;    str = stringify(str);    str = str.substr(1, str.length - 2);    if (this.lastBufferedIdx == this.buf.length && this.bufferedConcatenationCount < 100) {      if (this.lastBufferedType === 'code') {        this.lastBuffered += ' + "';        this.bufferedConcatenationCount++;      }      this.lastBufferedType = 'text';      this.lastBuffered += str;      this.buf[this.lastBufferedIdx - 1] = 'pug_html = pug_html + ' + this.bufferStartChar + this.lastBuffered + '";';    } else {      this.bufferedConcatenationCount = 0;      this.buf.push('pug_html = pug_html + "' + str + '";');      this.lastBufferedType = 'text';      this.bufferStartChar = '"';      this.lastBuffered = str;      this.lastBufferedIdx = this.buf.length;    }  },  /**   * Buffer the given `src` so it is evaluated at run time   *   * @param {String} src   * @api public   */  bufferExpression: function (src) {    if (isConstant(src)) {      return this.buffer(toConstant(src) + '')    }    if (this.lastBufferedIdx == this.buf.length && this.bufferedConcatenationCount < 100) {      this.bufferedConcatenationCount++;      if (this.lastBufferedType === 'text') this.lastBuffered += '"';      this.lastBufferedType = 'code';      this.lastBuffered += ' + (' + src + ')';      this.buf[this.lastBufferedIdx - 1] = 'pug_html = pug_html + (' + this.bufferStartChar + this.lastBuffered + ');';    } else {      this.bufferedConcatenationCount = 0;      this.buf.push('pug_html = pug_html + (' + src + ');');      this.lastBufferedType = 'code';      this.bufferStartChar = '';      this.lastBuffered = '(' + src + ')';      this.lastBufferedIdx = this.buf.length;    }  },  /**   * Buffer an indent based on the current `indent`   * property and an additional `offset`.   *   * @param {Number} offset   * @param {Boolean} newline   * @api public   */  prettyIndent: function(offset, newline){    offset = offset || 0;    newline = newline ? '\n' : '';    this.buffer(newline + Array(this.indents + offset).join(this.pp));    if (this.parentIndents)      this.buf.push('pug_html = pug_html + pug_indent.join("");');  },  /**   * Visit `node`.   *   * @param {Node} node   * @api public   */  visit: function(node, parent){    var debug = this.debug;    if (!node) {      var msg;      if (parent) {        msg = 'A child of ' + parent.type + ' (' + (parent.filename || 'Pug') + ':' + parent.line + ')';      } else {        msg = 'A top-level node';      }      msg += ' is ' + node + ', expected a Pug AST Node.';      throw new TypeError(msg);    }    if (debug && node.debug !== false && node.type !== 'Block') {      if (node.line) {        var js = ';pug_debug_line = ' + node.line;        if (node.filename) js += ';pug_debug_filename = ' + stringify(node.filename);        this.buf.push(js + ';');      }    }    if (!this['visit' + node.type]) {      var msg;      if (parent) {        msg = 'A child of ' + parent.type      } else {        msg = 'A top-level node';      }      msg += ' (' + (node.filename || 'Pug') + ':' + node.line + ')'           + ' is of type ' + node.type + ','           + ' which is not supported by pug-code-gen.'      switch (node.type) {      case 'Filter':        msg += ' Please use pug-filters to preprocess this AST.'        break;      case 'Extends':      case 'Include':      case 'NamedBlock':      case 'FileReference': // unlikely but for the sake of completeness        msg += ' Please use pug-linker to preprocess this AST.'        break;      }      throw new TypeError(msg);    }    this.visitNode(node);  },  /**   * Visit `node`.   *   * @param {Node} node   * @api public   */  visitNode: function(node){    return this['visit' + node.type](node);  },  /**   * Visit case `node`.   *   * @param {Literal} node   * @api public   */  visitCase: function(node){    this.buf.push('switch (' + node.expr + '){');    this.visit(node.block, node);    this.buf.push('}');  },  /**   * Visit when `node`.   *   * @param {Literal} node   * @api public   */  visitWhen: function(node){    if ('default' == node.expr) {      this.buf.push('default:');    } else {      this.buf.push('case ' + node.expr + ':');    }    if (node.block) {      this.visit(node.block, node);      this.buf.push('  break;');    }  },  /**   * Visit literal `node`.   *   * @param {Literal} node   * @api public   */  visitLiteral: function(node){    this.buffer(node.str);  },  visitNamedBlock: function(block){    return this.visitBlock(block);  },  /**   * Visit all nodes in `block`.   *   * @param {Block} block   * @api public   */  visitBlock: function(block){    var escapePrettyMode = this.escapePrettyMode;    var pp = this.pp;    // Pretty print multi-line text    if (pp && block.nodes.length > 1 && !escapePrettyMode &&        block.nodes[0].type === 'Text' && block.nodes[1].type === 'Text' ) {      this.prettyIndent(1, true);    }    for (var i = 0; i < block.nodes.length; ++i) {      // Pretty print text      if (pp && i > 0 && !escapePrettyMode &&          block.nodes[i].type === 'Text' && block.nodes[i-1].type === 'Text' &&          /\n$/.test(block.nodes[i - 1].val)) {        this.prettyIndent(1, false);      }      this.visit(block.nodes[i], block);    }  },  /**   * Visit a mixin's `block` keyword.   *   * @param {MixinBlock} block   * @api public   */  visitMixinBlock: function(block){    if (this.pp) this.buf.push("pug_indent.push('" + Array(this.indents + 1).join(this.pp) + "');");    this.buf.push('block && block();');    if (this.pp) this.buf.push("pug_indent.pop();");  },  /**   * Visit `doctype`. Sets terse mode to `true` when html 5   * is used, causing self-closing tags to end with ">" vs "/>",   * and boolean attributes are not mirrored.   *   * @param {Doctype} doctype   * @api public   */  visitDoctype: function(doctype){    if (doctype && (doctype.val || !this.doctype)) {      this.setDoctype(doctype.val || 'html');    }    if (this.doctype) this.buffer(this.doctype);    this.hasCompiledDoctype = true;  },  /**   * Visit `mixin`, generating a function that   * may be called within the template.   *   * @param {Mixin} mixin   * @api public   */  visitMixin: function(mixin){    var name = 'pug_mixins[';    var args = mixin.args || '';    var block = mixin.block;    var attrs = mixin.attrs;    var attrsBlocks = this.attributeBlocks(mixin.attributeBlocks);    var pp = this.pp;    var dynamic = mixin.name[0]==='#';    var key = mixin.name;    if (dynamic) this.dynamicMixins = true;    name += (dynamic ? mixin.name.substr(2,mixin.name.length-3):'"'+mixin.name+'"')+']';    this.mixins[key] = this.mixins[key] || {used: false, instances: []};    if (mixin.call) {      this.mixins[key].used = true;      if (pp) this.buf.push("pug_indent.push('" + Array(this.indents + 1).join(pp) + "');")      if (block || attrs.length || attrsBlocks.length) {        this.buf.push(name + '.call({');        if (block) {          this.buf.push('block: function(){');          // Render block with no indents, dynamically added when rendered          this.parentIndents++;          var _indents = this.indents;          this.indents = 0;          this.visit(mixin.block, mixin);          this.indents = _indents;          this.parentIndents--;          if (attrs.length || attrsBlocks.length) {            this.buf.push('},');          } else {            this.buf.push('}');          }        }        if (attrsBlocks.length) {          if (attrs.length) {            var val = this.attrs(attrs);            attrsBlocks.unshift(val);          }          if (attrsBlocks.length > 1) {            this.buf.push('attributes: ' + this.runtime('merge') + '([' + attrsBlocks.join(',') + '])');          } else {            this.buf.push('attributes: ' + attrsBlocks[0]);          }        } else if (attrs.length) {          var val = this.attrs(attrs);          this.buf.push('attributes: ' + val);        }        if (args) {          this.buf.push('}, ' + args + ');');        } else {          this.buf.push('});');        }      } else {        this.buf.push(name + '(' + args + ');');      }      if (pp) this.buf.push("pug_indent.pop();")    } else {      var mixin_start = this.buf.length;      args = args ? args.split(',') : [];      var rest;      if (args.length && /^\.\.\./.test(args[args.length - 1].trim())) {        rest = args.pop().trim().replace(/^\.\.\./, '');      }      // we need use pug_interp here for v8: https://code.google.com/p/v8/issues/detail?id=4165      // once fixed, use this: this.buf.push(name + ' = function(' + args.join(',') + '){');      this.buf.push(name + ' = pug_interp = function(' + args.join(',') + '){');      this.buf.push('var block = (this && this.block), attributes = (this && this.attributes) || {};');      if (rest) {        this.buf.push('var ' + rest + ' = [];');        this.buf.push('for (pug_interp = ' + args.length + '; pug_interp < arguments.length; pug_interp++) {');        this.buf.push('  ' + rest + '.push(arguments[pug_interp]);');        this.buf.push('}');      }      this.parentIndents++;      this.visit(block, mixin);      this.parentIndents--;      this.buf.push('};');      var mixin_end = this.buf.length;      this.mixins[key].instances.push({start: mixin_start, end: mixin_end});    }  },  /**   * Visit `tag` buffering tag markup, generating   * attributes, visiting the `tag`'s code and block.   *   * @param {Tag} tag   * @param {boolean} interpolated   * @api public   */  visitTag: function(tag, interpolated){    this.indents++;    var name = tag.name      , pp = this.pp      , self = this;    function bufferName() {      if (interpolated) self.bufferExpression(tag.expr);      else self.buffer(name);    }    if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true) this.escapePrettyMode = true;    if (!this.hasCompiledTag) {      if (!this.hasCompiledDoctype && 'html' == name) {        this.visitDoctype();      }      this.hasCompiledTag = true;    }    // pretty print    if (pp && !tag.isInline)      this.prettyIndent(0, true);    if (tag.selfClosing || (!this.xml && selfClosing[tag.name])) {      this.buffer('<');      bufferName();      this.visitAttributes(tag.attrs, this.attributeBlocks(tag.attributeBlocks));      if (this.terse && !tag.selfClosing) {        this.buffer('>');      } else {        this.buffer('/>');      }      // if it is non-empty throw an error      if (tag.code ||          tag.block &&          !(tag.block.type === 'Block' && tag.block.nodes.length === 0) &&          tag.block.nodes.some(function (tag) {            return tag.type !== 'Text' || !/^\s*$/.test(tag.val)          })) {        this.error(name + ' is a self closing element: <'+name+'/> but contains nested content.', 'SELF_CLOSING_CONTENT', tag);      }    } else {      // Optimize attributes buffering      this.buffer('<');      bufferName();      this.visitAttributes(tag.attrs, this.attributeBlocks(tag.attributeBlocks));      this.buffer('>');      if (tag.code) this.visitCode(tag.code);      this.visit(tag.block, tag);      // pretty print      if (pp && !tag.isInline && WHITE_SPACE_SENSITIVE_TAGS[tag.name] !== true && !tagCanInline(tag))        this.prettyIndent(0, true);      this.buffer('</');      bufferName();      this.buffer('>');    }    if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true) this.escapePrettyMode = false;    this.indents--;  },  /**   * Visit InterpolatedTag.   *   * @param {InterpolatedTag} tag   * @api public   */  visitInterpolatedTag: function(tag) {    return this.visitTag(tag, true);  },  /**   * Visit `text` node.   *   * @param {Text} text   * @api public   */  visitText: function(text){    this.buffer(text.val);  },  /**   * Visit a `comment`, only buffering when the buffer flag is set.   *   * @param {Comment} comment   * @api public   */  visitComment: function(comment){    if (!comment.buffer) return;    if (this.pp) this.prettyIndent(1, true);    this.buffer('<!--' + comment.val + '-->');  },  /**   * Visit a `YieldBlock`.   *   * This is necessary since we allow compiling a file with `yield`.   *   * @param {YieldBlock} block   * @api public   */  visitYieldBlock: function(block) {},  /**   * Visit a `BlockComment`.   *   * @param {Comment} comment   * @api public   */  visitBlockComment: function(comment){    if (!comment.buffer) return;    if (this.pp) this.prettyIndent(1, true);    this.buffer('<!--' + (comment.val || ''));    this.visit(comment.block, comment);    if (this.pp) this.prettyIndent(1, true);    this.buffer('-->');  },  /**   * Visit `code`, respecting buffer / escape flags.   * If the code is followed by a block, wrap it in   * a self-calling function.   *   * @param {Code} code   * @api public   */  visitCode: function(code){    // Wrap code blocks with {}.    // we only wrap unbuffered code blocks ATM    // since they are usually flow control    // Buffer code    if (code.buffer) {      var val = code.val.trim();      val = 'null == (pug_interp = '+val+') ? "" : pug_interp';      if (code.mustEscape !== false) val = this.runtime('escape') + '(' + val + ')';      this.bufferExpression(val);    } else {      this.buf.push(code.val);    }    // Block support    if (code.block) {      if (!code.buffer) this.buf.push('{');      this.visit(code.block, code);      if (!code.buffer) this.buf.push('}');    }  },  /**   * Visit `Conditional`.   *   * @param {Conditional} cond   * @api public   */  visitConditional: function(cond){    var test = cond.test;    this.buf.push('if (' + test + ') {');    this.visit(cond.consequent, cond);    this.buf.push('}')    if (cond.alternate) {      if (cond.alternate.type === 'Conditional') {        this.buf.push('else')        this.visitConditional(cond.alternate);      } else {        this.buf.push('else {');        this.visit(cond.alternate, cond);        this.buf.push('}');      }    }  },  /**   * Visit `While`.   *   * @param {While} loop   * @api public   */  visitWhile: function(loop){    var test = loop.test;    this.buf.push('while (' + test + ') {');    this.visit(loop.block, loop);    this.buf.push('}');  },  /**   * Visit `each` block.   *   * @param {Each} each   * @api public   */  visitEach: function(each){    var indexVarName = each.key || 'pug_index' + this.eachCount;    this.eachCount++;    this.buf.push(''      + '// iterate ' + each.obj + '\n'      + ';(function(){\n'      + '  var $$obj = ' + each.obj + ';\n'      + '  if (\'number\' == typeof $$obj.length) {');    if (each.alternate) {      this.buf.push('    if ($$obj.length) {');    }    this.buf.push(''      + '      for (var ' + indexVarName + ' = 0, $$l = $$obj.length; ' + indexVarName + ' < $$l; ' + indexVarName + '++) {\n'      + '        var ' + each.val + ' = $$obj[' + indexVarName + '];');    this.visit(each.block, each);    this.buf.push('      }');    if (each.alternate) {      this.buf.push('    } else {');      this.visit(each.alternate, each);      this.buf.push('    }');    }    this.buf.push(''      + '  } else {\n'      + '    var $$l = 0;\n'      + '    for (var ' + indexVarName + ' in $$obj) {\n'      + '      $$l++;\n'      + '      var ' + each.val + ' = $$obj[' + indexVarName + '];');    this.visit(each.block, each);    this.buf.push('    }');    if (each.alternate) {      this.buf.push('    if ($$l === 0) {');      this.visit(each.alternate, each);      this.buf.push('    }');    }    this.buf.push('  }\n}).call(this);\n');  },  /**   * Visit `attrs`.   *   * @param {Array} attrs   * @api public   */  visitAttributes: function(attrs, attributeBlocks){    if (attributeBlocks.length) {      if (attrs.length) {        var val = this.attrs(attrs);        attributeBlocks.unshift(val);      }      if (attributeBlocks.length > 1) {        this.bufferExpression(this.runtime('attrs') + '(' + this.runtime('merge') + '([' + attributeBlocks.join(',') + ']), ' + stringify(this.terse) + ')');      } else {        this.bufferExpression(this.runtime('attrs') + '(' + attributeBlocks[0] + ', ' + stringify(this.terse) + ')');      }    } else if (attrs.length) {      this.attrs(attrs, true);    }  },  /**   * Compile attributes.   */  attrs: function(attrs, buffer){    var res = compileAttrs(attrs, {      terse: this.terse,      format: buffer ? 'html' : 'object',      runtime: this.runtime.bind(this)    });    if (buffer)  {      this.bufferExpression(res);    }    return res;  },  /**   * Compile attribute blocks.   */  attributeBlocks: function (attributeBlocks) {    return attributeBlocks && attributeBlocks.slice().map(function(attrBlock){      return attrBlock.val;    });  }};function tagCanInline(tag) {  function isInline(node){    // Recurse if the node is a block    if (node.type === 'Block') return node.nodes.every(isInline);    // When there is a YieldBlock here, it is an indication that the file is    // expected to be included but is not. If this is the case, the block    // must be empty.    if (node.type === 'YieldBlock') return true;    return (node.type === 'Text' && !/\n/.test(node.val)) || node.isInline;  }  return tag.block.nodes.every(isInline);}
 |