index.js 30 KB

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