index.js 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  1. 'use strict';
  2. var assert = require('assert');
  3. var TokenStream = require('token-stream');
  4. var error = require('pug-error');
  5. var inlineTags = require('./lib/inline-tags');
  6. module.exports = parse;
  7. module.exports.Parser = Parser;
  8. function parse(tokens, options) {
  9. var parser = new Parser(tokens, options);
  10. var ast = parser.parse();
  11. return JSON.parse(JSON.stringify(ast));
  12. };
  13. /**
  14. * Initialize `Parser` with the given input `str` and `filename`.
  15. *
  16. * @param {String} str
  17. * @param {String} filename
  18. * @param {Object} options
  19. * @api public
  20. */
  21. function Parser(tokens, options) {
  22. options = options || {};
  23. if (!Array.isArray(tokens)) {
  24. throw new Error('Expected tokens to be an Array but got "' + (typeof tokens) + '"');
  25. }
  26. if (typeof options !== 'object') {
  27. throw new Error('Expected "options" to be an object but got "' + (typeof options) + '"');
  28. }
  29. this.tokens = new TokenStream(tokens);
  30. this.filename = options.filename;
  31. this.src = options.src;
  32. this.inMixin = 0;
  33. this.plugins = options.plugins || [];
  34. };
  35. /**
  36. * Parser prototype.
  37. */
  38. Parser.prototype = {
  39. /**
  40. * Save original constructor
  41. */
  42. constructor: Parser,
  43. error: function (code, message, token) {
  44. var err = error(code, message, {
  45. line: token.loc.start.line,
  46. column: token.loc.start.column,
  47. filename: this.filename,
  48. src: this.src
  49. });
  50. throw err;
  51. },
  52. /**
  53. * Return the next token object.
  54. *
  55. * @return {Object}
  56. * @api private
  57. */
  58. advance: function(){
  59. return this.tokens.advance();
  60. },
  61. /**
  62. * Single token lookahead.
  63. *
  64. * @return {Object}
  65. * @api private
  66. */
  67. peek: function() {
  68. return this.tokens.peek();
  69. },
  70. /**
  71. * `n` token lookahead.
  72. *
  73. * @param {Number} n
  74. * @return {Object}
  75. * @api private
  76. */
  77. lookahead: function(n){
  78. return this.tokens.lookahead(n);
  79. },
  80. /**
  81. * Parse input returning a string of js for evaluation.
  82. *
  83. * @return {String}
  84. * @api public
  85. */
  86. parse: function(){
  87. var block = this.emptyBlock(0);
  88. while ('eos' != this.peek().type) {
  89. if ('newline' == this.peek().type) {
  90. this.advance();
  91. } else if ('text-html' == this.peek().type) {
  92. block.nodes = block.nodes.concat(this.parseTextHtml());
  93. } else {
  94. var expr = this.parseExpr();
  95. if (expr) {
  96. if (expr.type === 'Block') {
  97. block.nodes = block.nodes.concat(expr.nodes);
  98. } else {
  99. block.nodes.push(expr);
  100. }
  101. }
  102. }
  103. }
  104. return block;
  105. },
  106. /**
  107. * Expect the given type, or throw an exception.
  108. *
  109. * @param {String} type
  110. * @api private
  111. */
  112. expect: function(type){
  113. if (this.peek().type === type) {
  114. return this.advance();
  115. } else {
  116. this.error('INVALID_TOKEN', 'expected "' + type + '", but got "' + this.peek().type + '"', this.peek());
  117. }
  118. },
  119. /**
  120. * Accept the given `type`.
  121. *
  122. * @param {String} type
  123. * @api private
  124. */
  125. accept: function(type){
  126. if (this.peek().type === type) {
  127. return this.advance();
  128. }
  129. },
  130. initBlock: function(line, nodes) {
  131. /* istanbul ignore if */
  132. if ((line | 0) !== line) throw new Error('`line` is not an integer');
  133. /* istanbul ignore if */
  134. if (!Array.isArray(nodes)) throw new Error('`nodes` is not an array');
  135. return {
  136. type: 'Block',
  137. nodes: nodes,
  138. line: line,
  139. filename: this.filename
  140. };
  141. },
  142. emptyBlock: function(line) {
  143. return this.initBlock(line, []);
  144. },
  145. runPlugin: function(context, tok) {
  146. var rest = [this];
  147. for (var i = 2; i < arguments.length; i++) {
  148. rest.push(arguments[i]);
  149. }
  150. var pluginContext;
  151. for (var i = 0; i < this.plugins.length; i++) {
  152. var plugin = this.plugins[i];
  153. if (plugin[context] && plugin[context][tok.type]) {
  154. if (pluginContext) throw new Error('Multiple plugin handlers found for context ' + JSON.stringify(context) + ', token type ' + JSON.stringify(tok.type));
  155. pluginContext = plugin[context];
  156. }
  157. }
  158. if (pluginContext) return pluginContext[tok.type].apply(pluginContext, rest);
  159. },
  160. /**
  161. * tag
  162. * | doctype
  163. * | mixin
  164. * | include
  165. * | filter
  166. * | comment
  167. * | text
  168. * | text-html
  169. * | dot
  170. * | each
  171. * | code
  172. * | yield
  173. * | id
  174. * | class
  175. * | interpolation
  176. */
  177. parseExpr: function(){
  178. switch (this.peek().type) {
  179. case 'tag':
  180. return this.parseTag();
  181. case 'mixin':
  182. return this.parseMixin();
  183. case 'block':
  184. return this.parseBlock();
  185. case 'mixin-block':
  186. return this.parseMixinBlock();
  187. case 'case':
  188. return this.parseCase();
  189. case 'extends':
  190. return this.parseExtends();
  191. case 'include':
  192. return this.parseInclude();
  193. case 'doctype':
  194. return this.parseDoctype();
  195. case 'filter':
  196. return this.parseFilter();
  197. case 'comment':
  198. return this.parseComment();
  199. case 'text':
  200. case 'interpolated-code':
  201. case 'start-pug-interpolation':
  202. return this.parseText({block: true});
  203. case 'text-html':
  204. return this.initBlock(this.peek().loc.start.line, this.parseTextHtml());
  205. case 'dot':
  206. return this.parseDot();
  207. case 'each':
  208. return this.parseEach();
  209. case 'code':
  210. return this.parseCode();
  211. case 'blockcode':
  212. return this.parseBlockCode();
  213. case 'if':
  214. return this.parseConditional();
  215. case 'while':
  216. return this.parseWhile();
  217. case 'call':
  218. return this.parseCall();
  219. case 'interpolation':
  220. return this.parseInterpolation();
  221. case 'yield':
  222. return this.parseYield();
  223. case 'id':
  224. case 'class':
  225. if (!this.peek().loc.start) debugger;
  226. this.tokens.defer({
  227. type: 'tag',
  228. val: 'div',
  229. loc: this.peek().loc,
  230. filename: this.filename
  231. });
  232. return this.parseExpr();
  233. default:
  234. var pluginResult = this.runPlugin('expressionTokens', this.peek());
  235. if (pluginResult) return pluginResult;
  236. this.error('INVALID_TOKEN', 'unexpected token "' + this.peek().type + '"', this.peek());
  237. }
  238. },
  239. parseDot: function() {
  240. this.advance();
  241. return this.parseTextBlock();
  242. },
  243. /**
  244. * Text
  245. */
  246. parseText: function(options){
  247. var tags = [];
  248. var lineno = this.peek().loc.start.line;
  249. var nextTok = this.peek();
  250. loop:
  251. while (true) {
  252. switch (nextTok.type) {
  253. case 'text':
  254. var tok = this.advance();
  255. tags.push({
  256. type: 'Text',
  257. val: tok.val,
  258. line: tok.loc.start.line,
  259. column: tok.loc.start.column,
  260. filename: this.filename
  261. });
  262. break;
  263. case 'interpolated-code':
  264. var tok = this.advance();
  265. tags.push({
  266. type: 'Code',
  267. val: tok.val,
  268. buffer: tok.buffer,
  269. mustEscape: tok.mustEscape !== false,
  270. isInline: true,
  271. line: tok.loc.start.line,
  272. column: tok.loc.start.column,
  273. filename: this.filename
  274. });
  275. break;
  276. case 'newline':
  277. if (!options || !options.block) break loop;
  278. var tok = this.advance();
  279. var nextType = this.peek().type;
  280. if (nextType === 'text' || nextType === 'interpolated-code') {
  281. tags.push({
  282. type: 'Text',
  283. val: '\n',
  284. line: tok.loc.start.line,
  285. column: tok.loc.start.column,
  286. filename: this.filename
  287. });
  288. }
  289. break;
  290. case 'start-pug-interpolation':
  291. this.advance();
  292. tags.push(this.parseExpr());
  293. this.expect('end-pug-interpolation');
  294. break;
  295. default:
  296. var pluginResult = this.runPlugin('textTokens', nextTok, tags);
  297. if (pluginResult) break;
  298. break loop;
  299. }
  300. nextTok = this.peek();
  301. }
  302. if (tags.length === 1) return tags[0];
  303. else return this.initBlock(lineno, tags);
  304. },
  305. parseTextHtml: function () {
  306. var nodes = [];
  307. var currentNode = null;
  308. loop:
  309. while (true) {
  310. switch (this.peek().type) {
  311. case 'text-html':
  312. var text = this.advance();
  313. if (!currentNode) {
  314. currentNode = {
  315. type: 'Text',
  316. val: text.val,
  317. filename: this.filename,
  318. line: text.loc.start.line,
  319. column: text.loc.start.column,
  320. isHtml: true
  321. };
  322. nodes.push(currentNode);
  323. } else {
  324. currentNode.val += '\n' + text.val;
  325. }
  326. break;
  327. case 'indent':
  328. var block = this.block();
  329. block.nodes.forEach(function (node) {
  330. if (node.isHtml) {
  331. if (!currentNode) {
  332. currentNode = node;
  333. nodes.push(currentNode);
  334. } else {
  335. currentNode.val += '\n' + node.val;
  336. }
  337. } else {
  338. currentNode = null;
  339. nodes.push(node);
  340. }
  341. });
  342. break;
  343. case 'code':
  344. currentNode = null;
  345. nodes.push(this.parseCode(true));
  346. break;
  347. case 'newline':
  348. this.advance();
  349. break;
  350. default:
  351. break loop;
  352. }
  353. }
  354. return nodes;
  355. },
  356. /**
  357. * ':' expr
  358. * | block
  359. */
  360. parseBlockExpansion: function(){
  361. var tok = this.accept(':');
  362. if (tok) {
  363. var expr = this.parseExpr();
  364. return expr.type === 'Block' ? expr : this.initBlock(tok.loc.start.line, [expr]);
  365. } else {
  366. return this.block();
  367. }
  368. },
  369. /**
  370. * case
  371. */
  372. parseCase: function(){
  373. var tok = this.expect('case');
  374. var node = {
  375. type: 'Case',
  376. expr: tok.val,
  377. line: tok.loc.start.line,
  378. column: tok.loc.start.column,
  379. filename: this.filename
  380. };
  381. var block = this.emptyBlock(tok.loc.start.line + 1);
  382. this.expect('indent');
  383. while ('outdent' != this.peek().type) {
  384. switch (this.peek().type) {
  385. case 'comment':
  386. case 'newline':
  387. this.advance();
  388. break;
  389. case 'when':
  390. block.nodes.push(this.parseWhen());
  391. break;
  392. case 'default':
  393. block.nodes.push(this.parseDefault());
  394. break;
  395. default:
  396. var pluginResult = this.runPlugin('caseTokens', this.peek(), block);
  397. if (pluginResult) break;
  398. this.error('INVALID_TOKEN', 'Unexpected token "' + this.peek().type
  399. + '", expected "when", "default" or "newline"', this.peek());
  400. }
  401. }
  402. this.expect('outdent');
  403. node.block = block;
  404. return node;
  405. },
  406. /**
  407. * when
  408. */
  409. parseWhen: function(){
  410. var tok = this.expect('when');
  411. if (this.peek().type !== 'newline') {
  412. return {
  413. type: 'When',
  414. expr: tok.val,
  415. block: this.parseBlockExpansion(),
  416. debug: false,
  417. line: tok.loc.start.line,
  418. column: tok.loc.start.column,
  419. filename: this.filename
  420. };
  421. } else {
  422. return {
  423. type: 'When',
  424. expr: tok.val,
  425. debug: false,
  426. line: tok.loc.start.line,
  427. column: tok.loc.start.column,
  428. filename: this.filename
  429. };
  430. }
  431. },
  432. /**
  433. * default
  434. */
  435. parseDefault: function(){
  436. var tok = this.expect('default');
  437. return {
  438. type: 'When',
  439. expr: 'default',
  440. block: this.parseBlockExpansion(),
  441. debug: false,
  442. line: tok.loc.start.line,
  443. column: tok.loc.start.column,
  444. filename: this.filename
  445. };
  446. },
  447. /**
  448. * code
  449. */
  450. parseCode: function(noBlock){
  451. var tok = this.expect('code');
  452. assert(typeof tok.mustEscape === 'boolean', 'Please update to the newest version of pug-lexer.');
  453. var node = {
  454. type: 'Code',
  455. val: tok.val,
  456. buffer: tok.buffer,
  457. mustEscape: tok.mustEscape !== false,
  458. isInline: !!noBlock,
  459. line: tok.loc.start.line,
  460. column: tok.loc.start.column,
  461. filename: this.filename
  462. };
  463. // todo: why is this here? It seems like a hacky workaround
  464. if (node.val.match(/^ *else/)) node.debug = false;
  465. if (noBlock) return node;
  466. var block;
  467. // handle block
  468. block = 'indent' == this.peek().type;
  469. if (block) {
  470. if (tok.buffer) {
  471. this.error('BLOCK_IN_BUFFERED_CODE', 'Buffered code cannot have a block attached to it', this.peek());
  472. }
  473. node.block = this.block();
  474. }
  475. return node;
  476. },
  477. parseConditional: function(){
  478. var tok = this.expect('if');
  479. var node = {
  480. type: 'Conditional',
  481. test: tok.val,
  482. consequent: this.emptyBlock(tok.loc.start.line),
  483. alternate: null,
  484. line: tok.loc.start.line,
  485. column: tok.loc.start.column,
  486. filename: this.filename
  487. };
  488. // handle block
  489. if ('indent' == this.peek().type) {
  490. node.consequent = this.block();
  491. }
  492. var currentNode = node;
  493. while (true) {
  494. if (this.peek().type === 'newline') {
  495. this.expect('newline');
  496. } else if (this.peek().type === 'else-if') {
  497. tok = this.expect('else-if');
  498. currentNode = (
  499. currentNode.alternate = {
  500. type: 'Conditional',
  501. test: tok.val,
  502. consequent: this.emptyBlock(tok.loc.start.line),
  503. alternate: null,
  504. line: tok.loc.start.line,
  505. column: tok.loc.start.column,
  506. filename: this.filename
  507. }
  508. );
  509. if ('indent' == this.peek().type) {
  510. currentNode.consequent = this.block();
  511. }
  512. } else if (this.peek().type === 'else') {
  513. this.expect('else');
  514. if (this.peek().type === 'indent') {
  515. currentNode.alternate = this.block();
  516. }
  517. break;
  518. } else {
  519. break;
  520. }
  521. }
  522. return node;
  523. },
  524. parseWhile: function(){
  525. var tok = this.expect('while');
  526. var node = {
  527. type: 'While',
  528. test: tok.val,
  529. line: tok.loc.start.line,
  530. column: tok.loc.start.column,
  531. filename: this.filename
  532. };
  533. // handle block
  534. if ('indent' == this.peek().type) {
  535. node.block = this.block();
  536. } else {
  537. node.block = this.emptyBlock(tok.loc.start.line);
  538. }
  539. return node;
  540. },
  541. /**
  542. * block code
  543. */
  544. parseBlockCode: function(){
  545. var tok = this.expect('blockcode');
  546. var line = tok.loc.start.line;
  547. var column = tok.loc.start.column;
  548. var body = this.peek();
  549. var text = '';
  550. if (body.type === 'start-pipeless-text') {
  551. this.advance();
  552. while (this.peek().type !== 'end-pipeless-text') {
  553. tok = this.advance();
  554. switch (tok.type) {
  555. case 'text':
  556. text += tok.val;
  557. break;
  558. case 'newline':
  559. text += '\n';
  560. break;
  561. default:
  562. var pluginResult = this.runPlugin('blockCodeTokens', tok, tok);
  563. if (pluginResult) {
  564. text += pluginResult;
  565. break;
  566. }
  567. this.error('INVALID_TOKEN', 'Unexpected token type: ' + tok.type, tok);
  568. }
  569. }
  570. this.advance();
  571. }
  572. return {
  573. type: 'Code',
  574. val: text,
  575. buffer: false,
  576. mustEscape: false,
  577. isInline: false,
  578. line: line,
  579. column: column,
  580. filename: this.filename
  581. };
  582. },
  583. /**
  584. * comment
  585. */
  586. parseComment: function(){
  587. var tok = this.expect('comment');
  588. var block;
  589. if (block = this.parseTextBlock()) {
  590. return {
  591. type: 'BlockComment',
  592. val: tok.val,
  593. block: block,
  594. buffer: tok.buffer,
  595. line: tok.loc.start.line,
  596. column: tok.loc.start.column,
  597. filename: this.filename
  598. };
  599. } else {
  600. return {
  601. type: 'Comment',
  602. val: tok.val,
  603. buffer: tok.buffer,
  604. line: tok.loc.start.line,
  605. column: tok.loc.start.column,
  606. filename: this.filename
  607. };
  608. }
  609. },
  610. /**
  611. * doctype
  612. */
  613. parseDoctype: function(){
  614. var tok = this.expect('doctype');
  615. return {
  616. type: 'Doctype',
  617. val: tok.val,
  618. line: tok.loc.start.line,
  619. column: tok.loc.start.column,
  620. filename: this.filename
  621. };
  622. },
  623. parseIncludeFilter: function() {
  624. var tok = this.expect('filter');
  625. var attrs = [];
  626. if (this.peek().type === 'start-attributes') {
  627. attrs = this.attrs();
  628. }
  629. return {
  630. type: 'IncludeFilter',
  631. name: tok.val,
  632. attrs: attrs,
  633. line: tok.loc.start.line,
  634. column: tok.loc.start.column,
  635. filename: this.filename
  636. };
  637. },
  638. /**
  639. * filter attrs? text-block
  640. */
  641. parseFilter: function(){
  642. var tok = this.expect('filter');
  643. var block, attrs = [];
  644. if (this.peek().type === 'start-attributes') {
  645. attrs = this.attrs();
  646. }
  647. if (this.peek().type === 'text') {
  648. var textToken = this.advance();
  649. block = this.initBlock(textToken.loc.start.line, [
  650. {
  651. type: 'Text',
  652. val: textToken.val,
  653. line: textToken.loc.start.line,
  654. column: textToken.loc.start.column,
  655. filename: this.filename
  656. }
  657. ]);
  658. } else if (this.peek().type === 'filter') {
  659. block = this.initBlock(tok.loc.start.line, [this.parseFilter()]);
  660. } else {
  661. block = this.parseTextBlock() || this.emptyBlock(tok.loc.start.line);
  662. }
  663. return {
  664. type: 'Filter',
  665. name: tok.val,
  666. block: block,
  667. attrs: attrs,
  668. line: tok.loc.start.line,
  669. column: tok.loc.start.column,
  670. filename: this.filename
  671. };
  672. },
  673. /**
  674. * each block
  675. */
  676. parseEach: function(){
  677. var tok = this.expect('each');
  678. var node = {
  679. type: 'Each',
  680. obj: tok.code,
  681. val: tok.val,
  682. key: tok.key,
  683. block: this.block(),
  684. line: tok.loc.start.line,
  685. column: tok.loc.start.column,
  686. filename: this.filename
  687. };
  688. if (this.peek().type == 'else') {
  689. this.advance();
  690. node.alternate = this.block();
  691. }
  692. return node;
  693. },
  694. /**
  695. * 'extends' name
  696. */
  697. parseExtends: function(){
  698. var tok = this.expect('extends');
  699. var path = this.expect('path');
  700. return {
  701. type: 'Extends',
  702. file: {
  703. type: 'FileReference',
  704. path: path.val.trim(),
  705. line: path.loc.start.line,
  706. column: path.loc.start.column,
  707. filename: this.filename
  708. },
  709. line: tok.loc.start.line,
  710. column: tok.loc.start.column,
  711. filename: this.filename
  712. };
  713. },
  714. /**
  715. * 'block' name block
  716. */
  717. parseBlock: function(){
  718. var tok = this.expect('block');
  719. var node = 'indent' == this.peek().type ? this.block() : this.emptyBlock(tok.loc.start.line);
  720. node.type = 'NamedBlock';
  721. node.name = tok.val.trim();
  722. node.mode = tok.mode;
  723. node.line = tok.loc.start.line;
  724. node.column = tok.loc.start.column;
  725. return node;
  726. },
  727. parseMixinBlock: function () {
  728. var tok = this.expect('mixin-block');
  729. if (!this.inMixin) {
  730. this.error('BLOCK_OUTISDE_MIXIN', 'Anonymous blocks are not allowed unless they are part of a mixin.', tok);
  731. }
  732. return {
  733. type: 'MixinBlock',
  734. line: tok.loc.start.line,
  735. column: tok.loc.start.column,
  736. filename: this.filename
  737. };
  738. },
  739. parseYield: function() {
  740. var tok = this.expect('yield');
  741. return {
  742. type: 'YieldBlock',
  743. line: tok.loc.start.line,
  744. column: tok.loc.start.column,
  745. filename: this.filename
  746. };
  747. },
  748. /**
  749. * include block?
  750. */
  751. parseInclude: function(){
  752. var tok = this.expect('include');
  753. var node = {
  754. type: 'Include',
  755. file: {
  756. type: 'FileReference',
  757. filename: this.filename
  758. },
  759. line: tok.loc.start.line,
  760. column: tok.loc.start.column,
  761. filename: this.filename
  762. };
  763. var filters = [];
  764. while (this.peek().type === 'filter') {
  765. filters.push(this.parseIncludeFilter());
  766. }
  767. var path = this.expect('path');
  768. node.file.path = path.val.trim();
  769. node.file.line = path.loc.start.line;
  770. node.file.column = path.loc.start.column;
  771. if ((/\.jade$/.test(node.file.path) || /\.pug$/.test(node.file.path)) && !filters.length) {
  772. node.block = 'indent' == this.peek().type ? this.block() : this.emptyBlock(tok.loc.start.line);
  773. if (/\.jade$/.test(node.file.path)) {
  774. console.warn(
  775. this.filename + ', line ' + tok.loc.start.line +
  776. ':\nThe .jade extension is deprecated, use .pug for "' + node.file.path +'".'
  777. );
  778. }
  779. } else {
  780. node.type = 'RawInclude';
  781. node.filters = filters;
  782. if (this.peek().type === 'indent') {
  783. this.error('RAW_INCLUDE_BLOCK', 'Raw inclusion cannot contain a block', this.peek());
  784. }
  785. }
  786. return node;
  787. },
  788. /**
  789. * call ident block
  790. */
  791. parseCall: function(){
  792. var tok = this.expect('call');
  793. var name = tok.val;
  794. var args = tok.args;
  795. var mixin = {
  796. type: 'Mixin',
  797. name: name,
  798. args: args,
  799. block: this.emptyBlock(tok.loc.start.line),
  800. call: true,
  801. attrs: [],
  802. attributeBlocks: [],
  803. line: tok.loc.start.line,
  804. column: tok.loc.start.column,
  805. filename: this.filename
  806. };
  807. this.tag(mixin);
  808. if (mixin.code) {
  809. mixin.block.nodes.push(mixin.code);
  810. delete mixin.code;
  811. }
  812. if (mixin.block.nodes.length === 0) mixin.block = null;
  813. return mixin;
  814. },
  815. /**
  816. * mixin block
  817. */
  818. parseMixin: function(){
  819. var tok = this.expect('mixin');
  820. var name = tok.val;
  821. var args = tok.args;
  822. if ('indent' == this.peek().type) {
  823. this.inMixin++;
  824. var mixin = {
  825. type: 'Mixin',
  826. name: name,
  827. args: args,
  828. block: this.block(),
  829. call: false,
  830. line: tok.loc.start.line,
  831. column: tok.loc.start.column,
  832. filename: this.filename
  833. };
  834. this.inMixin--;
  835. return mixin;
  836. } else {
  837. this.error('MIXIN_WITHOUT_BODY', 'Mixin ' + name + ' declared without body', tok);
  838. }
  839. },
  840. /**
  841. * indent (text | newline)* outdent
  842. */
  843. parseTextBlock: function(){
  844. var tok = this.accept('start-pipeless-text');
  845. if (!tok) return;
  846. var block = this.emptyBlock(tok.loc.start.line);
  847. while (this.peek().type !== 'end-pipeless-text') {
  848. var tok = this.advance();
  849. switch (tok.type) {
  850. case 'text':
  851. block.nodes.push({
  852. type: 'Text',
  853. val: tok.val,
  854. line: tok.loc.start.line,
  855. column: tok.loc.start.column,
  856. filename: this.filename
  857. });
  858. break;
  859. case 'newline':
  860. block.nodes.push({
  861. type: 'Text',
  862. val: '\n',
  863. line: tok.loc.start.line,
  864. column: tok.loc.start.column,
  865. filename: this.filename
  866. });
  867. break;
  868. case 'start-pug-interpolation':
  869. block.nodes.push(this.parseExpr());
  870. this.expect('end-pug-interpolation');
  871. break;
  872. case 'interpolated-code':
  873. block.nodes.push({
  874. type: 'Code',
  875. val: tok.val,
  876. buffer: tok.buffer,
  877. mustEscape: tok.mustEscape !== false,
  878. isInline: true,
  879. line: tok.loc.start.line,
  880. column: tok.loc.start.column,
  881. filename: this.filename
  882. });
  883. break;
  884. default:
  885. var pluginResult = this.runPlugin('textBlockTokens', tok, block, tok);
  886. if (pluginResult) break;
  887. this.error('INVALID_TOKEN', 'Unexpected token type: ' + tok.type, tok);
  888. }
  889. }
  890. this.advance();
  891. return block;
  892. },
  893. /**
  894. * indent expr* outdent
  895. */
  896. block: function(){
  897. var tok = this.expect('indent');
  898. var block = this.emptyBlock(tok.loc.start.line);
  899. while ('outdent' != this.peek().type) {
  900. if ('newline' == this.peek().type) {
  901. this.advance();
  902. } else if ('text-html' == this.peek().type) {
  903. block.nodes = block.nodes.concat(this.parseTextHtml());
  904. } else {
  905. var expr = this.parseExpr();
  906. if (expr.type === 'Block') {
  907. block.nodes = block.nodes.concat(expr.nodes);
  908. } else {
  909. block.nodes.push(expr);
  910. }
  911. }
  912. }
  913. this.expect('outdent');
  914. return block;
  915. },
  916. /**
  917. * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
  918. */
  919. parseInterpolation: function(){
  920. var tok = this.advance();
  921. var tag = {
  922. type: 'InterpolatedTag',
  923. expr: tok.val,
  924. selfClosing: false,
  925. block: this.emptyBlock(tok.loc.start.line),
  926. attrs: [],
  927. attributeBlocks: [],
  928. isInline: false,
  929. line: tok.loc.start.line,
  930. column: tok.loc.start.column,
  931. filename: this.filename
  932. };
  933. return this.tag(tag, {selfClosingAllowed: true});
  934. },
  935. /**
  936. * tag (attrs | class | id)* (text | code | ':')? newline* block?
  937. */
  938. parseTag: function(){
  939. var tok = this.advance();
  940. var tag = {
  941. type: 'Tag',
  942. name: tok.val,
  943. selfClosing: false,
  944. block: this.emptyBlock(tok.loc.start.line),
  945. attrs: [],
  946. attributeBlocks: [],
  947. isInline: inlineTags.indexOf(tok.val) !== -1,
  948. line: tok.loc.start.line,
  949. column: tok.loc.start.column,
  950. filename: this.filename
  951. };
  952. return this.tag(tag, {selfClosingAllowed: true});
  953. },
  954. /**
  955. * Parse tag.
  956. */
  957. tag: function(tag, options) {
  958. var seenAttrs = false;
  959. var attributeNames = [];
  960. var selfClosingAllowed = options && options.selfClosingAllowed;
  961. // (attrs | class | id)*
  962. out:
  963. while (true) {
  964. switch (this.peek().type) {
  965. case 'id':
  966. case 'class':
  967. var tok = this.advance();
  968. if (tok.type === 'id') {
  969. if (attributeNames.indexOf('id') !== -1) {
  970. this.error('DUPLICATE_ID', 'Duplicate attribute "id" is not allowed.', tok);
  971. }
  972. attributeNames.push('id');
  973. }
  974. tag.attrs.push({
  975. name: tok.type,
  976. val: "'" + tok.val + "'",
  977. line: tok.loc.start.line,
  978. column: tok.loc.start.column,
  979. filename: this.filename,
  980. mustEscape: false
  981. });
  982. continue;
  983. case 'start-attributes':
  984. if (seenAttrs) {
  985. console.warn(this.filename + ', line ' + this.peek().loc.start.line + ':\nYou should not have pug tags with multiple attributes.');
  986. }
  987. seenAttrs = true;
  988. tag.attrs = tag.attrs.concat(this.attrs(attributeNames));
  989. continue;
  990. case '&attributes':
  991. var tok = this.advance();
  992. tag.attributeBlocks.push({
  993. type: 'AttributeBlock',
  994. val: tok.val,
  995. line: tok.loc.start.line,
  996. column: tok.loc.start.column,
  997. filename: this.filename
  998. });
  999. break;
  1000. default:
  1001. var pluginResult = this.runPlugin('tagAttributeTokens', this.peek(), tag, attributeNames);
  1002. if (pluginResult) break;
  1003. break out;
  1004. }
  1005. }
  1006. // check immediate '.'
  1007. if ('dot' == this.peek().type) {
  1008. tag.textOnly = true;
  1009. this.advance();
  1010. }
  1011. // (text | code | ':')?
  1012. switch (this.peek().type) {
  1013. case 'text':
  1014. case 'interpolated-code':
  1015. var text = this.parseText();
  1016. if (text.type === 'Block') {
  1017. tag.block.nodes.push.apply(tag.block.nodes, text.nodes);
  1018. } else {
  1019. tag.block.nodes.push(text);
  1020. }
  1021. break;
  1022. case 'code':
  1023. tag.block.nodes.push(this.parseCode(true));
  1024. break;
  1025. case ':':
  1026. this.advance();
  1027. var expr = this.parseExpr();
  1028. tag.block = expr.type === 'Block' ? expr : this.initBlock(tag.line, [expr]);
  1029. break;
  1030. case 'newline':
  1031. case 'indent':
  1032. case 'outdent':
  1033. case 'eos':
  1034. case 'start-pipeless-text':
  1035. case 'end-pug-interpolation':
  1036. break;
  1037. case 'slash':
  1038. if (selfClosingAllowed) {
  1039. this.advance();
  1040. tag.selfClosing = true;
  1041. break;
  1042. }
  1043. default:
  1044. var pluginResult = this.runPlugin('tagTokens', this.peek(), tag, options);
  1045. if (pluginResult) break;
  1046. this.error('INVALID_TOKEN', 'Unexpected token `' + this.peek().type + '` expected `text`, `interpolated-code`, `code`, `:`' + (selfClosingAllowed ? ', `slash`' : '') + ', `newline` or `eos`', this.peek())
  1047. }
  1048. // newline*
  1049. while ('newline' == this.peek().type) this.advance();
  1050. // block?
  1051. if (tag.textOnly) {
  1052. tag.block = this.parseTextBlock() || this.emptyBlock(tag.line);
  1053. } else if ('indent' == this.peek().type) {
  1054. var block = this.block();
  1055. for (var i = 0, len = block.nodes.length; i < len; ++i) {
  1056. tag.block.nodes.push(block.nodes[i]);
  1057. }
  1058. }
  1059. return tag;
  1060. },
  1061. attrs: function(attributeNames) {
  1062. this.expect('start-attributes');
  1063. var attrs = [];
  1064. var tok = this.advance();
  1065. while (tok.type === 'attribute') {
  1066. if (tok.name !== 'class' && attributeNames) {
  1067. if (attributeNames.indexOf(tok.name) !== -1) {
  1068. this.error('DUPLICATE_ATTRIBUTE', 'Duplicate attribute "' + tok.name + '" is not allowed.', tok);
  1069. }
  1070. attributeNames.push(tok.name);
  1071. }
  1072. attrs.push({
  1073. name: tok.name,
  1074. val: tok.val,
  1075. line: tok.loc.start.line,
  1076. column: tok.loc.start.column,
  1077. filename: this.filename,
  1078. mustEscape: tok.mustEscape !== false
  1079. });
  1080. tok = this.advance();
  1081. }
  1082. this.tokens.defer(tok);
  1083. this.expect('end-attributes');
  1084. return attrs;
  1085. }
  1086. };