1'use strict'; 2 3var assert = require('assert'); 4var walk = require('pug-walk'); 5 6function error() { 7 throw require('pug-error').apply(null, arguments); 8} 9 10module.exports = link; 11function link(ast) { 12 assert(ast.type === 'Block', 'The top level element should always be a block'); 13 var extendsNode = null; 14 if (ast.nodes.length) { 15 var hasExtends = ast.nodes[0].type === 'Extends'; 16 checkExtendPosition(ast, hasExtends); 17 if (hasExtends) { 18 extendsNode = ast.nodes.shift(); 19 } 20 } 21 ast = applyIncludes(ast); 22 ast.declaredBlocks = findDeclaredBlocks(ast); 23 if (extendsNode) { 24 var mixins = []; 25 var expectedBlocks = []; 26 ast.nodes.forEach(function addNode(node) { 27 if (node.type === 'NamedBlock') { 28 expectedBlocks.push(node); 29 } else if (node.type === 'Block') { 30 node.nodes.forEach(addNode); 31 } else if (node.type === 'Mixin' && node.call === false) { 32 mixins.push(node); 33 } else { 34 error('UNEXPECTED_NODES_IN_EXTENDING_ROOT', 'Only named blocks and mixins can appear at the top level of an extending template', node); 35 } 36 }); 37 var parent = link(extendsNode.file.ast); 38 extend(parent.declaredBlocks, ast); 39 var foundBlockNames = []; 40 walk(parent, function (node) { 41 if (node.type === 'NamedBlock') { 42 foundBlockNames.push(node.name); 43 } 44 }); 45 expectedBlocks.forEach(function (expectedBlock) { 46 if (foundBlockNames.indexOf(expectedBlock.name) === -1) { 47 error( 48 'UNEXPECTED_BLOCK', 49 'Unexpected block ' + expectedBlock.name, 50 expectedBlock 51 ); 52 } 53 }); 54 Object.keys(ast.declaredBlocks).forEach(function (name) { 55 parent.declaredBlocks[name] = ast.declaredBlocks[name]; 56 }); 57 parent.nodes = mixins.concat(parent.nodes); 58 parent.hasExtends = true; 59 return parent; 60 } 61 return ast; 62} 63 64function findDeclaredBlocks(ast) /*: {[name: string]: Array<BlockNode>}*/ { 65 var definitions = {}; 66 walk(ast, function before(node) { 67 if (node.type === 'NamedBlock' && node.mode === 'replace') { 68 definitions[node.name] = definitions[node.name] || []; 69 definitions[node.name].push(node); 70 } 71 }); 72 return definitions; 73} 74 75function flattenParentBlocks(parentBlocks, accumulator) { 76 accumulator = accumulator || []; 77 parentBlocks.forEach(function (parentBlock) { 78 if (parentBlock.parents) { 79 flattenParentBlocks(parentBlock.parents, accumulator); 80 } 81 accumulator.push(parentBlock); 82 }); 83 return accumulator; 84} 85 86function extend(parentBlocks, ast) { 87 var stack = {}; 88 walk(ast, function before(node) { 89 if (node.type === 'NamedBlock') { 90 if (stack[node.name] === node.name) { 91 return node.ignore = true; 92 } 93 stack[node.name] = node.name; 94 var parentBlockList = parentBlocks[node.name] ? flattenParentBlocks(parentBlocks[node.name]) : []; 95 if (parentBlockList.length) { 96 node.parents = parentBlockList; 97 parentBlockList.forEach(function (parentBlock) { 98 switch (node.mode) { 99 case 'append': 100 parentBlock.nodes = parentBlock.nodes.concat(node.nodes); 101 break; 102 case 'prepend': 103 parentBlock.nodes = node.nodes.concat(parentBlock.nodes); 104 break; 105 case 'replace': 106 parentBlock.nodes = node.nodes; 107 break; 108 } 109 }); 110 } 111 } 112 }, function after(node) { 113 if (node.type === 'NamedBlock' && !node.ignore) { 114 delete stack[node.name]; 115 } 116 }); 117} 118 119function applyIncludes(ast, child) { 120 return walk(ast, function before(node, replace) { 121 if (node.type === 'RawInclude') { 122 replace({type: 'Text', val: node.file.str.replace(/\r/g, '')}); 123 } 124 }, function after(node, replace) { 125 if (node.type === 'Include') { 126 var childAST = link(node.file.ast); 127 if (childAST.hasExtends) { 128 childAST = removeBlocks(childAST); 129 } 130 replace(applyYield(childAST, node.block)); 131 } 132 }); 133} 134function removeBlocks(ast) { 135 return walk(ast, function (node, replace) { 136 if (node.type === 'NamedBlock') { 137 replace({ 138 type: 'Block', 139 nodes: node.nodes 140 }); 141 } 142 }); 143} 144 145function applyYield(ast, block) { 146 if (!block || !block.nodes.length) return ast; 147 var replaced = false; 148 ast = walk(ast, null, function (node, replace) { 149 if (node.type === 'YieldBlock') { 150 replaced = true; 151 node.type = 'Block'; 152 node.nodes = [block]; 153 } 154 }); 155 function defaultYieldLocation(node) { 156 var res = node; 157 for (var i = 0; i < node.nodes.length; i++) { 158 if (node.nodes[i].textOnly) continue; 159 if (node.nodes[i].type === 'Block') { 160 res = defaultYieldLocation(node.nodes[i]); 161 } else if (node.nodes[i].block && node.nodes[i].block.nodes.length) { 162 res = defaultYieldLocation(node.nodes[i].block); 163 } 164 } 165 return res; 166 } 167 if (!replaced) { 168 // todo: probably should deprecate this with a warning 169 defaultYieldLocation(ast).nodes.push(block); 170 } 171 return ast; 172} 173 174function checkExtendPosition(ast, hasExtends) { 175 var legitExtendsReached = false; 176 walk(ast, function (node) { 177 if (node.type === 'Extends') { 178 if (hasExtends && !legitExtendsReached) { 179 legitExtendsReached = true; 180 } else { 181 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); 182 } 183 } 184 }); 185} 186