parser.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  1. 'use strict';
  2. var Lexer = require('./lexer');
  3. var nodes = require('./nodes');
  4. var utils = require('./utils');
  5. var filters = require('./filters');
  6. var path = require('path');
  7. var constantinople = require('constantinople');
  8. var parseJSExpression = require('character-parser').parseMax;
  9. var extname = path.extname;
  10. /**
  11. * Initialize `Parser` with the given input `str` and `filename`.
  12. *
  13. * @param {String} str
  14. * @param {String} filename
  15. * @param {Object} options
  16. * @api public
  17. */
  18. var Parser = exports = module.exports = function Parser(str, filename, options){
  19. //Strip any UTF-8 BOM off of the start of `str`, if it exists.
  20. this.input = str.replace(/^\uFEFF/, '');
  21. this.lexer = new Lexer(this.input, filename);
  22. this.filename = filename;
  23. this.blocks = {};
  24. this.mixins = {};
  25. this.options = options;
  26. this.contexts = [this];
  27. this.inMixin = 0;
  28. this.dependencies = [];
  29. this.inBlock = 0;
  30. };
  31. /**
  32. * Parser prototype.
  33. */
  34. Parser.prototype = {
  35. /**
  36. * Save original constructor
  37. */
  38. constructor: Parser,
  39. /**
  40. * Push `parser` onto the context stack,
  41. * or pop and return a `Parser`.
  42. */
  43. context: function(parser){
  44. if (parser) {
  45. this.contexts.push(parser);
  46. } else {
  47. return this.contexts.pop();
  48. }
  49. },
  50. /**
  51. * Return the next token object.
  52. *
  53. * @return {Object}
  54. * @api private
  55. */
  56. advance: function(){
  57. return this.lexer.advance();
  58. },
  59. /**
  60. * Single token lookahead.
  61. *
  62. * @return {Object}
  63. * @api private
  64. */
  65. peek: function() {
  66. return this.lookahead(1);
  67. },
  68. /**
  69. * Return lexer lineno.
  70. *
  71. * @return {Number}
  72. * @api private
  73. */
  74. line: function() {
  75. return this.lexer.lineno;
  76. },
  77. /**
  78. * `n` token lookahead.
  79. *
  80. * @param {Number} n
  81. * @return {Object}
  82. * @api private
  83. */
  84. lookahead: function(n){
  85. return this.lexer.lookahead(n);
  86. },
  87. /**
  88. * Parse input returning a string of js for evaluation.
  89. *
  90. * @return {String}
  91. * @api public
  92. */
  93. parse: function(){
  94. var block = new nodes.Block, parser;
  95. block.line = 0;
  96. block.filename = this.filename;
  97. while ('eos' != this.peek().type) {
  98. if ('newline' == this.peek().type) {
  99. this.advance();
  100. } else {
  101. var next = this.peek();
  102. var expr = this.parseExpr();
  103. expr.filename = expr.filename || this.filename;
  104. expr.line = next.line;
  105. block.push(expr);
  106. }
  107. }
  108. if (parser = this.extending) {
  109. this.context(parser);
  110. var ast = parser.parse();
  111. this.context();
  112. // hoist mixins
  113. for (var name in this.mixins)
  114. ast.unshift(this.mixins[name]);
  115. return ast;
  116. }
  117. if (!this.extending && !this.included && Object.keys(this.blocks).length){
  118. var blocks = [];
  119. utils.walkAST(block, function (node) {
  120. if (node.type === 'Block' && node.name) {
  121. blocks.push(node.name);
  122. }
  123. });
  124. Object.keys(this.blocks).forEach(function (name) {
  125. if (blocks.indexOf(name) === -1 && !this.blocks[name].isSubBlock) {
  126. console.warn('Warning: Unexpected block "'
  127. + name
  128. + '" '
  129. + ' on line '
  130. + this.blocks[name].line
  131. + ' of '
  132. + (this.blocks[name].filename)
  133. + '. This block is never used. This warning will be an error in v2.0.0');
  134. }
  135. }.bind(this));
  136. }
  137. return block;
  138. },
  139. /**
  140. * Expect the given type, or throw an exception.
  141. *
  142. * @param {String} type
  143. * @api private
  144. */
  145. expect: function(type){
  146. if (this.peek().type === type) {
  147. return this.advance();
  148. } else {
  149. throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
  150. }
  151. },
  152. /**
  153. * Accept the given `type`.
  154. *
  155. * @param {String} type
  156. * @api private
  157. */
  158. accept: function(type){
  159. if (this.peek().type === type) {
  160. return this.advance();
  161. }
  162. },
  163. /**
  164. * tag
  165. * | doctype
  166. * | mixin
  167. * | include
  168. * | filter
  169. * | comment
  170. * | text
  171. * | each
  172. * | code
  173. * | yield
  174. * | id
  175. * | class
  176. * | interpolation
  177. */
  178. parseExpr: function(){
  179. switch (this.peek().type) {
  180. case 'tag':
  181. return this.parseTag();
  182. case 'mixin':
  183. return this.parseMixin();
  184. case 'block':
  185. return this.parseBlock();
  186. case 'mixin-block':
  187. return this.parseMixinBlock();
  188. case 'case':
  189. return this.parseCase();
  190. case 'extends':
  191. return this.parseExtends();
  192. case 'include':
  193. return this.parseInclude();
  194. case 'doctype':
  195. return this.parseDoctype();
  196. case 'filter':
  197. return this.parseFilter();
  198. case 'comment':
  199. return this.parseComment();
  200. case 'text':
  201. return this.parseText();
  202. case 'each':
  203. return this.parseEach();
  204. case 'code':
  205. return this.parseCode();
  206. case 'blockCode':
  207. return this.parseBlockCode();
  208. case 'call':
  209. return this.parseCall();
  210. case 'interpolation':
  211. return this.parseInterpolation();
  212. case 'yield':
  213. this.advance();
  214. var block = new nodes.Block;
  215. block.yield = true;
  216. return block;
  217. case 'id':
  218. case 'class':
  219. var tok = this.advance();
  220. this.lexer.defer(this.lexer.tok('tag', 'div'));
  221. this.lexer.defer(tok);
  222. return this.parseExpr();
  223. default:
  224. throw new Error('unexpected token "' + this.peek().type + '"');
  225. }
  226. },
  227. /**
  228. * Text
  229. */
  230. parseText: function(){
  231. var tok = this.expect('text');
  232. var tokens = this.parseInlineTagsInText(tok.val);
  233. if (tokens.length === 1) return tokens[0];
  234. var node = new nodes.Block;
  235. for (var i = 0; i < tokens.length; i++) {
  236. node.push(tokens[i]);
  237. };
  238. return node;
  239. },
  240. /**
  241. * ':' expr
  242. * | block
  243. */
  244. parseBlockExpansion: function(){
  245. if (':' == this.peek().type) {
  246. this.advance();
  247. return new nodes.Block(this.parseExpr());
  248. } else {
  249. return this.block();
  250. }
  251. },
  252. /**
  253. * case
  254. */
  255. parseCase: function(){
  256. var val = this.expect('case').val;
  257. var node = new nodes.Case(val);
  258. node.line = this.line();
  259. var block = new nodes.Block;
  260. block.line = this.line();
  261. block.filename = this.filename;
  262. this.expect('indent');
  263. while ('outdent' != this.peek().type) {
  264. switch (this.peek().type) {
  265. case 'comment':
  266. case 'newline':
  267. this.advance();
  268. break;
  269. case 'when':
  270. block.push(this.parseWhen());
  271. break;
  272. case 'default':
  273. block.push(this.parseDefault());
  274. break;
  275. default:
  276. throw new Error('Unexpected token "' + this.peek().type
  277. + '", expected "when", "default" or "newline"');
  278. }
  279. }
  280. this.expect('outdent');
  281. node.block = block;
  282. return node;
  283. },
  284. /**
  285. * when
  286. */
  287. parseWhen: function(){
  288. var val = this.expect('when').val;
  289. if (this.peek().type !== 'newline')
  290. return new nodes.Case.When(val, this.parseBlockExpansion());
  291. else
  292. return new nodes.Case.When(val);
  293. },
  294. /**
  295. * default
  296. */
  297. parseDefault: function(){
  298. this.expect('default');
  299. return new nodes.Case.When('default', this.parseBlockExpansion());
  300. },
  301. /**
  302. * code
  303. */
  304. parseCode: function(afterIf){
  305. var tok = this.expect('code');
  306. var node = new nodes.Code(tok.val, tok.buffer, tok.escape);
  307. var block;
  308. node.line = this.line();
  309. // throw an error if an else does not have an if
  310. if (tok.isElse && !tok.hasIf) {
  311. throw new Error('Unexpected else without if');
  312. }
  313. // handle block
  314. block = 'indent' == this.peek().type;
  315. if (block) {
  316. node.block = this.block();
  317. }
  318. // handle missing block
  319. if (tok.requiresBlock && !block) {
  320. node.block = new nodes.Block();
  321. }
  322. // mark presense of if for future elses
  323. if (tok.isIf && this.peek().isElse) {
  324. this.peek().hasIf = true;
  325. } else if (tok.isIf && this.peek().type === 'newline' && this.lookahead(2).isElse) {
  326. this.lookahead(2).hasIf = true;
  327. }
  328. return node;
  329. },
  330. /**
  331. * block code
  332. */
  333. parseBlockCode: function(){
  334. var tok = this.expect('blockCode');
  335. var node;
  336. var body = this.peek();
  337. var text;
  338. if (body.type === 'pipeless-text') {
  339. this.advance();
  340. text = body.val.join('\n');
  341. } else {
  342. text = '';
  343. }
  344. node = new nodes.Code(text, false, false);
  345. return node;
  346. },
  347. /**
  348. * comment
  349. */
  350. parseComment: function(){
  351. var tok = this.expect('comment');
  352. var node;
  353. var block;
  354. if (block = this.parseTextBlock()) {
  355. node = new nodes.BlockComment(tok.val, block, tok.buffer);
  356. } else {
  357. node = new nodes.Comment(tok.val, tok.buffer);
  358. }
  359. node.line = this.line();
  360. return node;
  361. },
  362. /**
  363. * doctype
  364. */
  365. parseDoctype: function(){
  366. var tok = this.expect('doctype');
  367. var node = new nodes.Doctype(tok.val);
  368. node.line = this.line();
  369. return node;
  370. },
  371. /**
  372. * filter attrs? text-block
  373. */
  374. parseFilter: function(){
  375. var tok = this.expect('filter');
  376. var attrs = this.accept('attrs');
  377. var block;
  378. block = this.parseTextBlock() || new nodes.Block();
  379. var options = {};
  380. if (attrs) {
  381. attrs.attrs.forEach(function (attribute) {
  382. options[attribute.name] = constantinople.toConstant(attribute.val);
  383. });
  384. }
  385. var node = new nodes.Filter(tok.val, block, options);
  386. node.line = this.line();
  387. return node;
  388. },
  389. /**
  390. * each block
  391. */
  392. parseEach: function(){
  393. var tok = this.expect('each');
  394. var node = new nodes.Each(tok.code, tok.val, tok.key);
  395. node.line = this.line();
  396. node.block = this.block();
  397. if (this.peek().type == 'code' && this.peek().val == 'else') {
  398. this.advance();
  399. node.alternative = this.block();
  400. }
  401. return node;
  402. },
  403. /**
  404. * Resolves a path relative to the template for use in
  405. * includes and extends
  406. *
  407. * @param {String} path
  408. * @param {String} purpose Used in error messages.
  409. * @return {String}
  410. * @api private
  411. */
  412. resolvePath: function (path, purpose) {
  413. var p = require('path');
  414. var dirname = p.dirname;
  415. var basename = p.basename;
  416. var join = p.join;
  417. if (path[0] !== '/' && !this.filename)
  418. throw new Error('the "filename" option is required to use "' + purpose + '" with "relative" paths');
  419. if (path[0] === '/' && !this.options.basedir)
  420. throw new Error('the "basedir" option is required to use "' + purpose + '" with "absolute" paths');
  421. path = join(path[0] === '/' ? this.options.basedir : dirname(this.filename), path);
  422. if (basename(path).indexOf('.') === -1) path += '.jade';
  423. return path;
  424. },
  425. /**
  426. * 'extends' name
  427. */
  428. parseExtends: function(){
  429. var fs = require('fs');
  430. var path = this.resolvePath(this.expect('extends').val.trim(), 'extends');
  431. if ('.jade' != path.substr(-5)) path += '.jade';
  432. this.dependencies.push(path);
  433. var str = fs.readFileSync(path, 'utf8');
  434. var parser = new this.constructor(str, path, this.options);
  435. parser.dependencies = this.dependencies;
  436. parser.blocks = this.blocks;
  437. parser.included = this.included;
  438. parser.contexts = this.contexts;
  439. this.extending = parser;
  440. // TODO: null node
  441. return new nodes.Literal('');
  442. },
  443. /**
  444. * 'block' name block
  445. */
  446. parseBlock: function(){
  447. var block = this.expect('block');
  448. var mode = block.mode;
  449. var name = block.val.trim();
  450. var line = block.line;
  451. this.inBlock++;
  452. block = 'indent' == this.peek().type
  453. ? this.block()
  454. : new nodes.Block(new nodes.Literal(''));
  455. this.inBlock--;
  456. block.name = name;
  457. block.line = line;
  458. var prev = this.blocks[name] || {prepended: [], appended: []}
  459. if (prev.mode === 'replace') return this.blocks[name] = prev;
  460. var allNodes = prev.prepended.concat(block.nodes).concat(prev.appended);
  461. switch (mode) {
  462. case 'append':
  463. prev.appended = prev.parser === this ?
  464. prev.appended.concat(block.nodes) :
  465. block.nodes.concat(prev.appended);
  466. break;
  467. case 'prepend':
  468. prev.prepended = prev.parser === this ?
  469. block.nodes.concat(prev.prepended) :
  470. prev.prepended.concat(block.nodes);
  471. break;
  472. }
  473. block.nodes = allNodes;
  474. block.appended = prev.appended;
  475. block.prepended = prev.prepended;
  476. block.mode = mode;
  477. block.parser = this;
  478. block.isSubBlock = this.inBlock > 0;
  479. return this.blocks[name] = block;
  480. },
  481. parseMixinBlock: function () {
  482. var block = this.expect('mixin-block');
  483. if (!this.inMixin) {
  484. throw new Error('Anonymous blocks are not allowed unless they are part of a mixin.');
  485. }
  486. return new nodes.MixinBlock();
  487. },
  488. /**
  489. * include block?
  490. */
  491. parseInclude: function(){
  492. var fs = require('fs');
  493. var tok = this.expect('include');
  494. var path = this.resolvePath(tok.val.trim(), 'include');
  495. this.dependencies.push(path);
  496. // has-filter
  497. if (tok.filter) {
  498. var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
  499. var options = {filename: path};
  500. if (tok.attrs) {
  501. tok.attrs.attrs.forEach(function (attribute) {
  502. options[attribute.name] = constantinople.toConstant(attribute.val);
  503. });
  504. }
  505. str = filters(tok.filter, str, options);
  506. return new nodes.Literal(str);
  507. }
  508. // non-jade
  509. if ('.jade' != path.substr(-5)) {
  510. var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
  511. return new nodes.Literal(str);
  512. }
  513. var str = fs.readFileSync(path, 'utf8');
  514. var parser = new this.constructor(str, path, this.options);
  515. parser.dependencies = this.dependencies;
  516. parser.blocks = utils.merge({}, this.blocks);
  517. parser.included = true;
  518. parser.mixins = this.mixins;
  519. this.context(parser);
  520. var ast = parser.parse();
  521. this.context();
  522. ast.filename = path;
  523. if ('indent' == this.peek().type) {
  524. ast.includeBlock().push(this.block());
  525. }
  526. return ast;
  527. },
  528. /**
  529. * call ident block
  530. */
  531. parseCall: function(){
  532. var tok = this.expect('call');
  533. var name = tok.val;
  534. var args = tok.args;
  535. var mixin = new nodes.Mixin(name, args, new nodes.Block, true);
  536. this.tag(mixin);
  537. if (mixin.code) {
  538. mixin.block.push(mixin.code);
  539. mixin.code = null;
  540. }
  541. if (mixin.block.isEmpty()) mixin.block = null;
  542. return mixin;
  543. },
  544. /**
  545. * mixin block
  546. */
  547. parseMixin: function(){
  548. var tok = this.expect('mixin');
  549. var name = tok.val;
  550. var args = tok.args;
  551. var mixin;
  552. // definition
  553. if ('indent' == this.peek().type) {
  554. this.inMixin++;
  555. mixin = new nodes.Mixin(name, args, this.block(), false);
  556. this.mixins[name] = mixin;
  557. this.inMixin--;
  558. return mixin;
  559. // call
  560. } else {
  561. return new nodes.Mixin(name, args, null, true);
  562. }
  563. },
  564. parseInlineTagsInText: function (str) {
  565. var line = this.line();
  566. var match = /(\\)?#\[((?:.|\n)*)$/.exec(str);
  567. if (match) {
  568. if (match[1]) { // escape
  569. var text = new nodes.Text(str.substr(0, match.index) + '#[');
  570. text.line = line;
  571. var rest = this.parseInlineTagsInText(match[2]);
  572. if (rest[0].type === 'Text') {
  573. text.val += rest[0].val;
  574. rest.shift();
  575. }
  576. return [text].concat(rest);
  577. } else {
  578. var text = new nodes.Text(str.substr(0, match.index));
  579. text.line = line;
  580. var buffer = [text];
  581. var rest = match[2];
  582. var range = parseJSExpression(rest);
  583. var inner = new Parser(range.src, this.filename, this.options);
  584. buffer.push(inner.parse());
  585. return buffer.concat(this.parseInlineTagsInText(rest.substr(range.end + 1)));
  586. }
  587. } else {
  588. var text = new nodes.Text(str);
  589. text.line = line;
  590. return [text];
  591. }
  592. },
  593. /**
  594. * indent (text | newline)* outdent
  595. */
  596. parseTextBlock: function(){
  597. var block = new nodes.Block;
  598. block.line = this.line();
  599. var body = this.peek();
  600. if (body.type !== 'pipeless-text') return;
  601. this.advance();
  602. block.nodes = body.val.reduce(function (accumulator, text) {
  603. return accumulator.concat(this.parseInlineTagsInText(text));
  604. }.bind(this), []);
  605. return block;
  606. },
  607. /**
  608. * indent expr* outdent
  609. */
  610. block: function(){
  611. var block = new nodes.Block;
  612. block.line = this.line();
  613. block.filename = this.filename;
  614. this.expect('indent');
  615. while ('outdent' != this.peek().type) {
  616. if ('newline' == this.peek().type) {
  617. this.advance();
  618. } else {
  619. var expr = this.parseExpr();
  620. expr.filename = this.filename;
  621. block.push(expr);
  622. }
  623. }
  624. this.expect('outdent');
  625. return block;
  626. },
  627. /**
  628. * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
  629. */
  630. parseInterpolation: function(){
  631. var tok = this.advance();
  632. var tag = new nodes.Tag(tok.val);
  633. tag.buffer = true;
  634. return this.tag(tag);
  635. },
  636. /**
  637. * tag (attrs | class | id)* (text | code | ':')? newline* block?
  638. */
  639. parseTag: function(){
  640. var tok = this.advance();
  641. var tag = new nodes.Tag(tok.val);
  642. tag.selfClosing = tok.selfClosing;
  643. return this.tag(tag);
  644. },
  645. /**
  646. * Parse tag.
  647. */
  648. tag: function(tag){
  649. tag.line = this.line();
  650. var seenAttrs = false;
  651. // (attrs | class | id)*
  652. out:
  653. while (true) {
  654. switch (this.peek().type) {
  655. case 'id':
  656. case 'class':
  657. var tok = this.advance();
  658. tag.setAttribute(tok.type, "'" + tok.val + "'");
  659. continue;
  660. case 'attrs':
  661. if (seenAttrs) {
  662. console.warn(this.filename + ', line ' + this.peek().line + ':\nYou should not have jade tags with multiple attributes.');
  663. }
  664. seenAttrs = true;
  665. var tok = this.advance();
  666. var attrs = tok.attrs;
  667. if (tok.selfClosing) tag.selfClosing = true;
  668. for (var i = 0; i < attrs.length; i++) {
  669. tag.setAttribute(attrs[i].name, attrs[i].val, attrs[i].escaped);
  670. }
  671. continue;
  672. case '&attributes':
  673. var tok = this.advance();
  674. tag.addAttributes(tok.val);
  675. break;
  676. default:
  677. break out;
  678. }
  679. }
  680. // check immediate '.'
  681. if ('dot' == this.peek().type) {
  682. tag.textOnly = true;
  683. this.advance();
  684. }
  685. // (text | code | ':')?
  686. switch (this.peek().type) {
  687. case 'text':
  688. tag.block.push(this.parseText());
  689. break;
  690. case 'code':
  691. tag.code = this.parseCode();
  692. break;
  693. case ':':
  694. this.advance();
  695. tag.block = new nodes.Block;
  696. tag.block.push(this.parseExpr());
  697. break;
  698. case 'newline':
  699. case 'indent':
  700. case 'outdent':
  701. case 'eos':
  702. case 'pipeless-text':
  703. break;
  704. default:
  705. throw new Error('Unexpected token `' + this.peek().type + '` expected `text`, `code`, `:`, `newline` or `eos`')
  706. }
  707. // newline*
  708. while ('newline' == this.peek().type) this.advance();
  709. // block?
  710. if (tag.textOnly) {
  711. tag.block = this.parseTextBlock() || new nodes.Block();
  712. } else if ('indent' == this.peek().type) {
  713. var block = this.block();
  714. for (var i = 0, len = block.nodes.length; i < len; ++i) {
  715. tag.block.push(block.nodes[i]);
  716. }
  717. }
  718. return tag;
  719. }
  720. };