A Twitch.tv viewer reward and games system.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

790 lines
18 KiB

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