'use strict'; var assert = require('assert'); var walk = require('pug-walk'); function error() { throw require('pug-error').apply(null, arguments); } module.exports = link; function link(ast) { assert(ast.type === 'Block', 'The top level element should always be a block'); var extendsNode = null; if (ast.nodes.length) { var hasExtends = ast.nodes[0].type === 'Extends'; checkExtendPosition(ast, hasExtends); if (hasExtends) { extendsNode = ast.nodes.shift(); } } ast = applyIncludes(ast); ast.declaredBlocks = findDeclaredBlocks(ast); if (extendsNode) { var mixins = []; var expectedBlocks = []; ast.nodes.forEach(function addNode(node) { if (node.type === 'NamedBlock') { expectedBlocks.push(node); } else if (node.type === 'Block') { node.nodes.forEach(addNode); } else if (node.type === 'Mixin' && node.call === false) { mixins.push(node); } else { error('UNEXPECTED_NODES_IN_EXTENDING_ROOT', 'Only named blocks and mixins can appear at the top level of an extending template', node); } }); var parent = link(extendsNode.file.ast); extend(parent.declaredBlocks, ast); var foundBlockNames = []; walk(parent, function (node) { if (node.type === 'NamedBlock') { foundBlockNames.push(node.name); } }); expectedBlocks.forEach(function (expectedBlock) { if (foundBlockNames.indexOf(expectedBlock.name) === -1) { error( 'UNEXPECTED_BLOCK', 'Unexpected block ' + expectedBlock.name, expectedBlock ); } }); Object.keys(ast.declaredBlocks).forEach(function (name) { parent.declaredBlocks[name] = ast.declaredBlocks[name]; }); parent.nodes = mixins.concat(parent.nodes); parent.hasExtends = true; return parent; } return ast; } function findDeclaredBlocks(ast) /*: {[name: string]: Array}*/ { var definitions = {}; walk(ast, function before(node) { if (node.type === 'NamedBlock' && node.mode === 'replace') { definitions[node.name] = definitions[node.name] || []; definitions[node.name].push(node); } }); return definitions; } function flattenParentBlocks(parentBlocks, accumulator) { accumulator = accumulator || []; parentBlocks.forEach(function (parentBlock) { if (parentBlock.parents) { flattenParentBlocks(parentBlock.parents, accumulator); } accumulator.push(parentBlock); }); return accumulator; } function extend(parentBlocks, ast) { var stack = {}; walk(ast, function before(node) { if (node.type === 'NamedBlock') { if (stack[node.name] === node.name) { return node.ignore = true; } stack[node.name] = node.name; var parentBlockList = parentBlocks[node.name] ? flattenParentBlocks(parentBlocks[node.name]) : []; if (parentBlockList.length) { node.parents = parentBlockList; parentBlockList.forEach(function (parentBlock) { switch (node.mode) { case 'append': parentBlock.nodes = parentBlock.nodes.concat(node.nodes); break; case 'prepend': parentBlock.nodes = node.nodes.concat(parentBlock.nodes); break; case 'replace': parentBlock.nodes = node.nodes; break; } }); } } }, function after(node) { if (node.type === 'NamedBlock' && !node.ignore) { delete stack[node.name]; } }); } function applyIncludes(ast, child) { return walk(ast, function before(node, replace) { if (node.type === 'RawInclude') { replace({type: 'Text', val: node.file.str.replace(/\r/g, '')}); } }, function after(node, replace) { if (node.type === 'Include') { var childAST = link(node.file.ast); if (childAST.hasExtends) { childAST = removeBlocks(childAST); } replace(applyYield(childAST, node.block)); } }); } function removeBlocks(ast) { return walk(ast, function (node, replace) { if (node.type === 'NamedBlock') { replace({ type: 'Block', nodes: node.nodes }); } }); } function applyYield(ast, block) { if (!block || !block.nodes.length) return ast; var replaced = false; ast = walk(ast, null, function (node, replace) { if (node.type === 'YieldBlock') { replaced = true; node.type = 'Block'; node.nodes = [block]; } }); function defaultYieldLocation(node) { var res = node; for (var i = 0; i < node.nodes.length; i++) { if (node.nodes[i].textOnly) continue; if (node.nodes[i].type === 'Block') { res = defaultYieldLocation(node.nodes[i]); } else if (node.nodes[i].block && node.nodes[i].block.nodes.length) { res = defaultYieldLocation(node.nodes[i].block); } } return res; } if (!replaced) { // todo: probably should deprecate this with a warning defaultYieldLocation(ast).nodes.push(block); } return ast; } function checkExtendPosition(ast, hasExtends) { var legitExtendsReached = false; walk(ast, function (node) { if (node.type === 'Extends') { if (hasExtends && !legitExtendsReached) { legitExtendsReached = true; } else { error('EXTENDS_NOT_FIRST', 'Declaration of template inheritance ("extends") should be the first thing in the file. There can only be one extends statement per file.', node); } } }); }