index.js 5.4 KB

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