index.js 5.6 KB

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