hexo/node_modules/pug-code-gen/index.js

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);
}