You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
206 lines
5.6 KiB
206 lines
5.6 KiB
'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<BlockNode>}*/ { |
|
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 |
|
); |
|
} |
|
} |
|
}); |
|
}
|
|
|