1010 lines
24 KiB
JavaScript
1010 lines
24 KiB
JavaScript
|
'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 tags
|
||
|
var 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(' +
|
||
|
stringify(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(' +
|
||
|
stringify(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');
|
||
|
},
|
||
|
|
||
|
visitEachOf: function(each) {
|
||
|
this.buf.push(
|
||
|
'' +
|
||
|
'// iterate ' +
|
||
|
each.obj +
|
||
|
'\n' +
|
||
|
'for (const ' +
|
||
|
each.val +
|
||
|
' of ' +
|
||
|
each.obj +
|
||
|
') {\n'
|
||
|
);
|
||
|
|
||
|
this.visit(each.block, each);
|
||
|
|
||
|
this.buf.push('}\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);
|
||
|
}
|