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.

865 lines
19 KiB

  1. 'use strict';
  2. var utils = require('./utils');
  3. var characterParser = require('character-parser');
  4. /**
  5. * Initialize `Lexer` with the given `str`.
  6. *
  7. * @param {String} str
  8. * @param {String} filename
  9. * @api private
  10. */
  11. var Lexer = module.exports = function Lexer(str, filename) {
  12. this.input = str.replace(/\r\n|\r/g, '\n');
  13. this.filename = filename;
  14. this.deferredTokens = [];
  15. this.lastIndents = 0;
  16. this.lineno = 1;
  17. this.stash = [];
  18. this.indentStack = [];
  19. this.indentRe = null;
  20. this.pipeless = false;
  21. };
  22. function assertExpression(exp) {
  23. //this verifies that a JavaScript expression is valid
  24. Function('', 'return (' + exp + ')');
  25. }
  26. function assertNestingCorrect(exp) {
  27. //this verifies that code is properly nested, but allows
  28. //invalid JavaScript such as the contents of `attributes`
  29. var res = characterParser(exp)
  30. if (res.isNesting()) {
  31. throw new Error('Nesting must match on expression `' + exp + '`')
  32. }
  33. }
  34. /**
  35. * Lexer prototype.
  36. */
  37. Lexer.prototype = {
  38. /**
  39. * Construct a token with the given `type` and `val`.
  40. *
  41. * @param {String} type
  42. * @param {String} val
  43. * @return {Object}
  44. * @api private
  45. */
  46. tok: function(type, val){
  47. return {
  48. type: type
  49. , line: this.lineno
  50. , val: val
  51. }
  52. },
  53. /**
  54. * Consume the given `len` of input.
  55. *
  56. * @param {Number} len
  57. * @api private
  58. */
  59. consume: function(len){
  60. this.input = this.input.substr(len);
  61. },
  62. /**
  63. * Scan for `type` with the given `regexp`.
  64. *
  65. * @param {String} type
  66. * @param {RegExp} regexp
  67. * @return {Object}
  68. * @api private
  69. */
  70. scan: function(regexp, type){
  71. var captures;
  72. if (captures = regexp.exec(this.input)) {
  73. this.consume(captures[0].length);
  74. return this.tok(type, captures[1]);
  75. }
  76. },
  77. /**
  78. * Defer the given `tok`.
  79. *
  80. * @param {Object} tok
  81. * @api private
  82. */
  83. defer: function(tok){
  84. this.deferredTokens.push(tok);
  85. },
  86. /**
  87. * Lookahead `n` tokens.
  88. *
  89. * @param {Number} n
  90. * @return {Object}
  91. * @api private
  92. */
  93. lookahead: function(n){
  94. var fetch = n - this.stash.length;
  95. while (fetch-- > 0) this.stash.push(this.next());
  96. return this.stash[--n];
  97. },
  98. /**
  99. * Return the indexOf `(` or `{` or `[` / `)` or `}` or `]` delimiters.
  100. *
  101. * @return {Number}
  102. * @api private
  103. */
  104. bracketExpression: function(skip){
  105. skip = skip || 0;
  106. var start = this.input[skip];
  107. if (start != '(' && start != '{' && start != '[') throw new Error('unrecognized start character');
  108. var end = ({'(': ')', '{': '}', '[': ']'})[start];
  109. var range = characterParser.parseMax(this.input, {start: skip + 1});
  110. if (this.input[range.end] !== end) throw new Error('start character ' + start + ' does not match end character ' + this.input[range.end]);
  111. return range;
  112. },
  113. /**
  114. * Stashed token.
  115. */
  116. stashed: function() {
  117. return this.stash.length
  118. && this.stash.shift();
  119. },
  120. /**
  121. * Deferred token.
  122. */
  123. deferred: function() {
  124. return this.deferredTokens.length
  125. && this.deferredTokens.shift();
  126. },
  127. /**
  128. * end-of-source.
  129. */
  130. eos: function() {
  131. if (this.input.length) return;
  132. if (this.indentStack.length) {
  133. this.indentStack.shift();
  134. return this.tok('outdent');
  135. } else {
  136. return this.tok('eos');
  137. }
  138. },
  139. /**
  140. * Blank line.
  141. */
  142. blank: function() {
  143. var captures;
  144. if (captures = /^\n *\n/.exec(this.input)) {
  145. this.consume(captures[0].length - 1);
  146. ++this.lineno;
  147. if (this.pipeless) return this.tok('text', '');
  148. return this.next();
  149. }
  150. },
  151. /**
  152. * Comment.
  153. */
  154. comment: function() {
  155. var captures;
  156. if (captures = /^\/\/(-)?([^\n]*)/.exec(this.input)) {
  157. this.consume(captures[0].length);
  158. var tok = this.tok('comment', captures[2]);
  159. tok.buffer = '-' != captures[1];
  160. return tok;
  161. }
  162. },
  163. /**
  164. * Interpolated tag.
  165. */
  166. interpolation: function() {
  167. if (/^#\{/.test(this.input)) {
  168. var match;
  169. try {
  170. match = this.bracketExpression(1);
  171. } catch (ex) {
  172. return;//not an interpolation expression, just an unmatched open interpolation
  173. }
  174. this.consume(match.end + 1);
  175. return this.tok('interpolation', match.src);
  176. }
  177. },
  178. /**
  179. * Tag.
  180. */
  181. tag: function() {
  182. var captures;
  183. if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) {
  184. this.consume(captures[0].length);
  185. var tok, name = captures[1];
  186. if (':' == name[name.length - 1]) {
  187. name = name.slice(0, -1);
  188. tok = this.tok('tag', name);
  189. this.defer(this.tok(':'));
  190. while (' ' == this.input[0]) this.input = this.input.substr(1);
  191. } else {
  192. tok = this.tok('tag', name);
  193. }
  194. tok.selfClosing = !! captures[2];
  195. return tok;
  196. }
  197. },
  198. /**
  199. * Filter.
  200. */
  201. filter: function() {
  202. return this.scan(/^:([\w\-]+)/, 'filter');
  203. },
  204. /**
  205. * Doctype.
  206. */
  207. doctype: function() {
  208. if (this.scan(/^!!! *([^\n]+)?/, 'doctype')) {
  209. throw new Error('`!!!` is deprecated, you must now use `doctype`');
  210. }
  211. var node = this.scan(/^(?:doctype) *([^\n]+)?/, 'doctype');
  212. if (node && node.val && node.val.trim() === '5') {
  213. throw new Error('`doctype 5` is deprecated, you must now use `doctype html`');
  214. }
  215. return node;
  216. },
  217. /**
  218. * Id.
  219. */
  220. id: function() {
  221. return this.scan(/^#([\w-]+)/, 'id');
  222. },
  223. /**
  224. * Class.
  225. */
  226. className: function() {
  227. return this.scan(/^\.([\w-]+)/, 'class');
  228. },
  229. /**
  230. * Text.
  231. */
  232. text: function() {
  233. return this.scan(/^(?:\| ?| )([^\n]+)/, 'text') || this.scan(/^(<[^\n]*)/, 'text');
  234. },
  235. textFail: function () {
  236. var tok;
  237. if (tok = this.scan(/^([^\.\n][^\n]+)/, 'text')) {
  238. console.warn('Warning: missing space before text for line ' + this.lineno +
  239. ' of jade file "' + this.filename + '"');
  240. return tok;
  241. }
  242. },
  243. /**
  244. * Dot.
  245. */
  246. dot: function() {
  247. return this.scan(/^\./, 'dot');
  248. },
  249. /**
  250. * Extends.
  251. */
  252. "extends": function() {
  253. return this.scan(/^extends? +([^\n]+)/, 'extends');
  254. },
  255. /**
  256. * Block prepend.
  257. */
  258. prepend: function() {
  259. var captures;
  260. if (captures = /^prepend +([^\n]+)/.exec(this.input)) {
  261. this.consume(captures[0].length);
  262. var mode = 'prepend'
  263. , name = captures[1]
  264. , tok = this.tok('block', name);
  265. tok.mode = mode;
  266. return tok;
  267. }
  268. },
  269. /**
  270. * Block append.
  271. */
  272. append: function() {
  273. var captures;
  274. if (captures = /^append +([^\n]+)/.exec(this.input)) {
  275. this.consume(captures[0].length);
  276. var mode = 'append'
  277. , name = captures[1]
  278. , tok = this.tok('block', name);
  279. tok.mode = mode;
  280. return tok;
  281. }
  282. },
  283. /**
  284. * Block.
  285. */
  286. block: function() {
  287. var captures;
  288. if (captures = /^block\b *(?:(prepend|append) +)?([^\n]+)/.exec(this.input)) {
  289. this.consume(captures[0].length);
  290. var mode = captures[1] || 'replace'
  291. , name = captures[2]
  292. , tok = this.tok('block', name);
  293. tok.mode = mode;
  294. return tok;
  295. }
  296. },
  297. /**
  298. * Mixin Block.
  299. */
  300. mixinBlock: function() {
  301. var captures;
  302. if (captures = /^block\s*(\n|$)/.exec(this.input)) {
  303. this.consume(captures[0].length - 1);
  304. return this.tok('mixin-block');
  305. }
  306. },
  307. /**
  308. * Yield.
  309. */
  310. yield: function() {
  311. return this.scan(/^yield */, 'yield');
  312. },
  313. /**
  314. * Include.
  315. */
  316. include: function() {
  317. return this.scan(/^include +([^\n]+)/, 'include');
  318. },
  319. /**
  320. * Include with filter
  321. */
  322. includeFiltered: function() {
  323. var captures;
  324. if (captures = /^include:([\w\-]+) +([^\n]+)/.exec(this.input)) {
  325. this.consume(captures[0].length);
  326. var filter = captures[1];
  327. var path = captures[2];
  328. var tok = this.tok('include', path);
  329. tok.filter = filter;
  330. return tok;
  331. }
  332. },
  333. /**
  334. * Case.
  335. */
  336. "case": function() {
  337. return this.scan(/^case +([^\n]+)/, 'case');
  338. },
  339. /**
  340. * When.
  341. */
  342. when: function() {
  343. return this.scan(/^when +([^:\n]+)/, 'when');
  344. },
  345. /**
  346. * Default.
  347. */
  348. "default": function() {
  349. return this.scan(/^default */, 'default');
  350. },
  351. /**
  352. * Call mixin.
  353. */
  354. call: function(){
  355. var tok, captures;
  356. if (captures = /^\+(([-\w]+)|(#\{))/.exec(this.input)) {
  357. // try to consume simple or interpolated call
  358. if (captures[2]) {
  359. // simple call
  360. this.consume(captures[0].length);
  361. tok = this.tok('call', captures[2]);
  362. } else {
  363. // interpolated call
  364. var match;
  365. try {
  366. match = this.bracketExpression(2);
  367. } catch (ex) {
  368. return;//not an interpolation expression, just an unmatched open interpolation
  369. }
  370. this.consume(match.end + 1);
  371. assertExpression(match.src);
  372. tok = this.tok('call', '#{'+match.src+'}');
  373. }
  374. // Check for args (not attributes)
  375. if (captures = /^ *\(/.exec(this.input)) {
  376. try {
  377. var range = this.bracketExpression(captures[0].length - 1);
  378. if (!/^ *[-\w]+ *=/.test(range.src)) { // not attributes
  379. this.consume(range.end + 1);
  380. tok.args = range.src;
  381. }
  382. } catch (ex) {
  383. //not a bracket expcetion, just unmatched open parens
  384. }
  385. }
  386. return tok;
  387. }
  388. },
  389. /**
  390. * Mixin.
  391. */
  392. mixin: function(){
  393. var captures;
  394. if (captures = /^mixin +([-\w]+)(?: *\((.*)\))? */.exec(this.input)) {
  395. this.consume(captures[0].length);
  396. var tok = this.tok('mixin', captures[1]);
  397. tok.args = captures[2];
  398. return tok;
  399. }
  400. },
  401. /**
  402. * Conditional.
  403. */
  404. conditional: function() {
  405. var captures;
  406. if (captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input)) {
  407. this.consume(captures[0].length);
  408. var type = captures[1]
  409. , js = captures[2];
  410. switch (type) {
  411. case 'if':
  412. assertExpression(js)
  413. js = 'if (' + js + ')';
  414. break;
  415. case 'unless':
  416. assertExpression(js)
  417. js = 'if (!(' + js + '))';
  418. break;
  419. case 'else if':
  420. assertExpression(js)
  421. js = 'else if (' + js + ')';
  422. break;
  423. case 'else':
  424. if (js && js.trim()) {
  425. throw new Error('`else` cannot have a condition, perhaps you meant `else if`');
  426. }
  427. js = 'else';
  428. break;
  429. }
  430. return this.tok('code', js);
  431. }
  432. },
  433. /**
  434. * While.
  435. */
  436. "while": function() {
  437. var captures;
  438. if (captures = /^while +([^\n]+)/.exec(this.input)) {
  439. this.consume(captures[0].length);
  440. assertExpression(captures[1])
  441. return this.tok('code', 'while (' + captures[1] + ')');
  442. }
  443. },
  444. /**
  445. * Each.
  446. */
  447. each: function() {
  448. var captures;
  449. if (captures = /^(?:- *)?(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? * in *([^\n]+)/.exec(this.input)) {
  450. this.consume(captures[0].length);
  451. var tok = this.tok('each', captures[1]);
  452. tok.key = captures[2] || '$index';
  453. assertExpression(captures[3])
  454. tok.code = captures[3];
  455. return tok;
  456. }
  457. },
  458. /**
  459. * Code.
  460. */
  461. code: function() {
  462. var captures;
  463. if (captures = /^(!?=|-)[ \t]*([^\n]+)/.exec(this.input)) {
  464. this.consume(captures[0].length);
  465. var flags = captures[1];
  466. captures[1] = captures[2];
  467. var tok = this.tok('code', captures[1]);
  468. tok.escape = flags.charAt(0) === '=';
  469. tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '=';
  470. if (tok.buffer) assertExpression(captures[1])
  471. return tok;
  472. }
  473. },
  474. /**
  475. * Attributes.
  476. */
  477. attrs: function() {
  478. if ('(' == this.input.charAt(0)) {
  479. var index = this.bracketExpression().end
  480. , str = this.input.substr(1, index-1)
  481. , tok = this.tok('attrs');
  482. assertNestingCorrect(str);
  483. var quote = '';
  484. var interpolate = function (attr) {
  485. return attr.replace(/(\\)?#\{(.+)/g, function(_, escape, expr){
  486. if (escape) return _;
  487. try {
  488. var range = characterParser.parseMax(expr);
  489. if (expr[range.end] !== '}') return _.substr(0, 2) + interpolate(_.substr(2));
  490. assertExpression(range.src)
  491. return quote + " + (" + range.src + ") + " + quote + interpolate(expr.substr(range.end + 1));
  492. } catch (ex) {
  493. return _.substr(0, 2) + interpolate(_.substr(2));
  494. }
  495. });
  496. }
  497. this.consume(index + 1);
  498. tok.attrs = [];
  499. var escapedAttr = true
  500. var key = '';
  501. var val = '';
  502. var interpolatable = '';
  503. var state = characterParser.defaultState();
  504. var loc = 'key';
  505. var isEndOfAttribute = function (i) {
  506. if (key.trim() === '') return false;
  507. if (i === str.length) return true;
  508. if (loc === 'key') {
  509. if (str[i] === ' ' || str[i] === '\n') {
  510. for (var x = i; x < str.length; x++) {
  511. if (str[x] != ' ' && str[x] != '\n') {
  512. if (str[x] === '=' || str[x] === '!' || str[x] === ',') return false;
  513. else return true;
  514. }
  515. }
  516. }
  517. return str[i] === ','
  518. } else if (loc === 'value' && !state.isNesting()) {
  519. try {
  520. Function('', 'return (' + val + ');');
  521. if (str[i] === ' ' || str[i] === '\n') {
  522. for (var x = i; x < str.length; x++) {
  523. if (str[x] != ' ' && str[x] != '\n') {
  524. if (characterParser.isPunctuator(str[x]) && str[x] != '"' && str[x] != "'") return false;
  525. else return true;
  526. }
  527. }
  528. }
  529. return str[i] === ',';
  530. } catch (ex) {
  531. return false;
  532. }
  533. }
  534. }
  535. this.lineno += str.split("\n").length - 1;
  536. for (var i = 0; i <= str.length; i++) {
  537. if (isEndOfAttribute(i)) {
  538. val = val.trim();
  539. if (val) assertExpression(val)
  540. key = key.trim();
  541. key = key.replace(/^['"]|['"]$/g, '');
  542. tok.attrs.push({
  543. name: key,
  544. val: '' == val ? true : val,
  545. escaped: escapedAttr
  546. });
  547. key = val = '';
  548. loc = 'key';
  549. escapedAttr = false;
  550. } else {
  551. switch (loc) {
  552. case 'key-char':
  553. if (str[i] === quote) {
  554. loc = 'key';
  555. if (i + 1 < str.length && [' ', ',', '!', '=', '\n'].indexOf(str[i + 1]) === -1)
  556. throw new Error('Unexpected character ' + str[i + 1] + ' expected ` `, `\\n`, `,`, `!` or `=`');
  557. } else if (loc === 'key-char') {
  558. key += str[i];
  559. }
  560. break;
  561. case 'key':
  562. if (key === '' && (str[i] === '"' || str[i] === "'")) {
  563. loc = 'key-char';
  564. quote = str[i];
  565. } else if (str[i] === '!' || str[i] === '=') {
  566. escapedAttr = str[i] !== '!';
  567. if (str[i] === '!') i++;
  568. if (str[i] !== '=') throw new Error('Unexpected character ' + str[i] + ' expected `=`');
  569. loc = 'value';
  570. state = characterParser.defaultState();
  571. } else {
  572. key += str[i]
  573. }
  574. break;
  575. case 'value':
  576. state = characterParser.parseChar(str[i], state);
  577. if (state.isString()) {
  578. loc = 'string';
  579. quote = str[i];
  580. interpolatable = str[i];
  581. } else {
  582. val += str[i];
  583. }
  584. break;
  585. case 'string':
  586. state = characterParser.parseChar(str[i], state);
  587. interpolatable += str[i];
  588. if (!state.isString()) {
  589. loc = 'value';
  590. val += interpolate(interpolatable);
  591. }
  592. break;
  593. }
  594. }
  595. }
  596. if ('/' == this.input.charAt(0)) {
  597. this.consume(1);
  598. tok.selfClosing = true;
  599. }
  600. return tok;
  601. }
  602. },
  603. /**
  604. * &attributes block
  605. */
  606. attributesBlock: function () {
  607. var captures;
  608. if (/^&attributes\b/.test(this.input)) {
  609. this.consume(11);
  610. var args = this.bracketExpression();
  611. this.consume(args.end + 1);
  612. return this.tok('&attributes', args.src);
  613. }
  614. },
  615. /**
  616. * Indent | Outdent | Newline.
  617. */
  618. indent: function() {
  619. var captures, re;
  620. // established regexp
  621. if (this.indentRe) {
  622. captures = this.indentRe.exec(this.input);
  623. // determine regexp
  624. } else {
  625. // tabs
  626. re = /^\n(\t*) */;
  627. captures = re.exec(this.input);
  628. // spaces
  629. if (captures && !captures[1].length) {
  630. re = /^\n( *)/;
  631. captures = re.exec(this.input);
  632. }
  633. // established
  634. if (captures && captures[1].length) this.indentRe = re;
  635. }
  636. if (captures) {
  637. var tok
  638. , indents = captures[1].length;
  639. ++this.lineno;
  640. this.consume(indents + 1);
  641. if (' ' == this.input[0] || '\t' == this.input[0]) {
  642. throw new Error('Invalid indentation, you can use tabs or spaces but not both');
  643. }
  644. // blank line
  645. if ('\n' == this.input[0]) return this.tok('newline');
  646. // outdent
  647. if (this.indentStack.length && indents < this.indentStack[0]) {
  648. while (this.indentStack.length && this.indentStack[0] > indents) {
  649. this.stash.push(this.tok('outdent'));
  650. this.indentStack.shift();
  651. }
  652. tok = this.stash.pop();
  653. // indent
  654. } else if (indents && indents != this.indentStack[0]) {
  655. this.indentStack.unshift(indents);
  656. tok = this.tok('indent', indents);
  657. // newline
  658. } else {
  659. tok = this.tok('newline');
  660. }
  661. return tok;
  662. }
  663. },
  664. /**
  665. * Pipe-less text consumed only when
  666. * pipeless is true;
  667. */
  668. pipelessText: function() {
  669. if (this.pipeless) {
  670. if ('\n' == this.input[0]) return;
  671. var i = this.input.indexOf('\n');
  672. if (-1 == i) i = this.input.length;
  673. var str = this.input.substr(0, i);
  674. this.consume(str.length);
  675. return this.tok('text', str);
  676. }
  677. },
  678. /**
  679. * ':'
  680. */
  681. colon: function() {
  682. return this.scan(/^: */, ':');
  683. },
  684. fail: function () {
  685. if (/^ ($|\n)/.test(this.input)) {
  686. this.consume(1);
  687. return this.next();
  688. }
  689. throw new Error('unexpected text ' + this.input.substr(0, 5));
  690. },
  691. /**
  692. * Return the next token object, or those
  693. * previously stashed by lookahead.
  694. *
  695. * @return {Object}
  696. * @api private
  697. */
  698. advance: function(){
  699. return this.stashed()
  700. || this.next();
  701. },
  702. /**
  703. * Return the next token object.
  704. *
  705. * @return {Object}
  706. * @api private
  707. */
  708. next: function() {
  709. return this.deferred()
  710. || this.blank()
  711. || this.eos()
  712. || this.pipelessText()
  713. || this.yield()
  714. || this.doctype()
  715. || this.interpolation()
  716. || this["case"]()
  717. || this.when()
  718. || this["default"]()
  719. || this["extends"]()
  720. || this.append()
  721. || this.prepend()
  722. || this.block()
  723. || this.mixinBlock()
  724. || this.include()
  725. || this.includeFiltered()
  726. || this.mixin()
  727. || this.call()
  728. || this.conditional()
  729. || this.each()
  730. || this["while"]()
  731. || this.tag()
  732. || this.filter()
  733. || this.code()
  734. || this.id()
  735. || this.className()
  736. || this.attrs()
  737. || this.attributesBlock()
  738. || this.indent()
  739. || this.text()
  740. || this.comment()
  741. || this.colon()
  742. || this.dot()
  743. || this.textFail()
  744. || this.fail();
  745. }
  746. };