lexer.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949
  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. this.pipeless = true;
  161. return tok;
  162. }
  163. },
  164. /**
  165. * Interpolated tag.
  166. */
  167. interpolation: function() {
  168. if (/^#\{/.test(this.input)) {
  169. var match = this.bracketExpression(1);
  170. this.consume(match.end + 1);
  171. return this.tok('interpolation', match.src);
  172. }
  173. },
  174. /**
  175. * Tag.
  176. */
  177. tag: function() {
  178. var captures;
  179. if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) {
  180. this.consume(captures[0].length);
  181. var tok, name = captures[1];
  182. if (':' == name[name.length - 1]) {
  183. name = name.slice(0, -1);
  184. tok = this.tok('tag', name);
  185. this.defer(this.tok(':'));
  186. if (this.input[0] !== ' ') {
  187. console.warn('Warning: space required after `:` on line ' + this.lineno +
  188. ' of jade file "' + this.filename + '"');
  189. }
  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. var tok = this.scan(/^:([\w\-]+)/, 'filter');
  203. if (tok) {
  204. this.pipeless = true;
  205. return tok;
  206. }
  207. },
  208. /**
  209. * Doctype.
  210. */
  211. doctype: function() {
  212. if (this.scan(/^!!! *([^\n]+)?/, 'doctype')) {
  213. throw new Error('`!!!` is deprecated, you must now use `doctype`');
  214. }
  215. var node = this.scan(/^(?:doctype) *([^\n]+)?/, 'doctype');
  216. if (node && node.val && node.val.trim() === '5') {
  217. throw new Error('`doctype 5` is deprecated, you must now use `doctype html`');
  218. }
  219. return node;
  220. },
  221. /**
  222. * Id.
  223. */
  224. id: function() {
  225. return this.scan(/^#([\w-]+)/, 'id');
  226. },
  227. /**
  228. * Class.
  229. */
  230. className: function() {
  231. return this.scan(/^\.([\w-]+)/, 'class');
  232. },
  233. /**
  234. * Text.
  235. */
  236. text: function() {
  237. return this.scan(/^(?:\| ?| )([^\n]+)/, 'text') ||
  238. this.scan(/^\|?( )/, 'text') ||
  239. this.scan(/^(<[^\n]*)/, 'text');
  240. },
  241. textFail: function () {
  242. var tok;
  243. if (tok = this.scan(/^([^\.\n][^\n]+)/, 'text')) {
  244. console.warn('Warning: missing space before text for line ' + this.lineno +
  245. ' of jade file "' + this.filename + '"');
  246. return tok;
  247. }
  248. },
  249. /**
  250. * Dot.
  251. */
  252. dot: function() {
  253. var match;
  254. if (match = this.scan(/^\./, 'dot')) {
  255. this.pipeless = true;
  256. return match;
  257. }
  258. },
  259. /**
  260. * Extends.
  261. */
  262. "extends": function() {
  263. return this.scan(/^extends? +([^\n]+)/, 'extends');
  264. },
  265. /**
  266. * Block prepend.
  267. */
  268. prepend: function() {
  269. var captures;
  270. if (captures = /^prepend +([^\n]+)/.exec(this.input)) {
  271. this.consume(captures[0].length);
  272. var mode = 'prepend'
  273. , name = captures[1]
  274. , tok = this.tok('block', name);
  275. tok.mode = mode;
  276. return tok;
  277. }
  278. },
  279. /**
  280. * Block append.
  281. */
  282. append: function() {
  283. var captures;
  284. if (captures = /^append +([^\n]+)/.exec(this.input)) {
  285. this.consume(captures[0].length);
  286. var mode = 'append'
  287. , name = captures[1]
  288. , tok = this.tok('block', name);
  289. tok.mode = mode;
  290. return tok;
  291. }
  292. },
  293. /**
  294. * Block.
  295. */
  296. block: function() {
  297. var captures;
  298. if (captures = /^block\b *(?:(prepend|append) +)?([^\n]+)/.exec(this.input)) {
  299. this.consume(captures[0].length);
  300. var mode = captures[1] || 'replace'
  301. , name = captures[2]
  302. , tok = this.tok('block', name);
  303. tok.mode = mode;
  304. return tok;
  305. }
  306. },
  307. /**
  308. * Mixin Block.
  309. */
  310. mixinBlock: function() {
  311. var captures;
  312. if (captures = /^block[ \t]*(\n|$)/.exec(this.input)) {
  313. this.consume(captures[0].length - captures[1].length);
  314. return this.tok('mixin-block');
  315. }
  316. },
  317. /**
  318. * Yield.
  319. */
  320. 'yield': function() {
  321. return this.scan(/^yield */, 'yield');
  322. },
  323. /**
  324. * Include.
  325. */
  326. include: function() {
  327. return this.scan(/^include +([^\n]+)/, 'include');
  328. },
  329. /**
  330. * Include with filter
  331. */
  332. includeFiltered: function() {
  333. var captures;
  334. if (captures = /^include:([\w\-]+)([\( ])/.exec(this.input)) {
  335. this.consume(captures[0].length - 1);
  336. var filter = captures[1];
  337. var attrs = captures[2] === '(' ? this.attrs() : null;
  338. if (!(captures[2] === ' ' || this.input[0] === ' ')) {
  339. throw new Error('expected space after include:filter but got ' + utils.stringify(this.input[0]));
  340. }
  341. captures = /^ *([^\n]+)/.exec(this.input);
  342. if (!captures || captures[1].trim() === '') {
  343. throw new Error('missing path for include:filter');
  344. }
  345. this.consume(captures[0].length);
  346. var path = captures[1];
  347. var tok = this.tok('include', path);
  348. tok.filter = filter;
  349. tok.attrs = attrs;
  350. return tok;
  351. }
  352. },
  353. /**
  354. * Case.
  355. */
  356. "case": function() {
  357. return this.scan(/^case +([^\n]+)/, 'case');
  358. },
  359. /**
  360. * When.
  361. */
  362. when: function() {
  363. return this.scan(/^when +([^:\n]+)/, 'when');
  364. },
  365. /**
  366. * Default.
  367. */
  368. "default": function() {
  369. return this.scan(/^default */, 'default');
  370. },
  371. /**
  372. * Call mixin.
  373. */
  374. call: function(){
  375. var tok, captures;
  376. if (captures = /^\+(\s*)(([-\w]+)|(#\{))/.exec(this.input)) {
  377. // try to consume simple or interpolated call
  378. if (captures[3]) {
  379. // simple call
  380. this.consume(captures[0].length);
  381. tok = this.tok('call', captures[3]);
  382. } else {
  383. // interpolated call
  384. var match = this.bracketExpression(2 + captures[1].length);
  385. this.consume(match.end + 1);
  386. assertExpression(match.src);
  387. tok = this.tok('call', '#{'+match.src+'}');
  388. }
  389. // Check for args (not attributes)
  390. if (captures = /^ *\(/.exec(this.input)) {
  391. var range = this.bracketExpression(captures[0].length - 1);
  392. if (!/^\s*[-\w]+ *=/.test(range.src)) { // not attributes
  393. this.consume(range.end + 1);
  394. tok.args = range.src;
  395. }
  396. if (tok.args) {
  397. assertExpression('[' + tok.args + ']');
  398. }
  399. }
  400. return tok;
  401. }
  402. },
  403. /**
  404. * Mixin.
  405. */
  406. mixin: function(){
  407. var captures;
  408. if (captures = /^mixin +([-\w]+)(?: *\((.*)\))? */.exec(this.input)) {
  409. this.consume(captures[0].length);
  410. var tok = this.tok('mixin', captures[1]);
  411. tok.args = captures[2];
  412. return tok;
  413. }
  414. },
  415. /**
  416. * Conditional.
  417. */
  418. conditional: function() {
  419. var captures;
  420. if (captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input)) {
  421. this.consume(captures[0].length);
  422. var type = captures[1]
  423. var js = captures[2];
  424. var isIf = false;
  425. var isElse = false;
  426. switch (type) {
  427. case 'if':
  428. assertExpression(js)
  429. js = 'if (' + js + ')';
  430. isIf = true;
  431. break;
  432. case 'unless':
  433. assertExpression(js)
  434. js = 'if (!(' + js + '))';
  435. isIf = true;
  436. break;
  437. case 'else if':
  438. assertExpression(js)
  439. js = 'else if (' + js + ')';
  440. isIf = true;
  441. isElse = true;
  442. break;
  443. case 'else':
  444. if (js && js.trim()) {
  445. throw new Error('`else` cannot have a condition, perhaps you meant `else if`');
  446. }
  447. js = 'else';
  448. isElse = true;
  449. break;
  450. }
  451. var tok = this.tok('code', js);
  452. tok.isElse = isElse;
  453. tok.isIf = isIf;
  454. tok.requiresBlock = true;
  455. return tok;
  456. }
  457. },
  458. /**
  459. * While.
  460. */
  461. "while": function() {
  462. var captures;
  463. if (captures = /^while +([^\n]+)/.exec(this.input)) {
  464. this.consume(captures[0].length);
  465. assertExpression(captures[1])
  466. var tok = this.tok('code', 'while (' + captures[1] + ')');
  467. tok.requiresBlock = true;
  468. return tok;
  469. }
  470. },
  471. /**
  472. * Each.
  473. */
  474. each: function() {
  475. var captures;
  476. if (captures = /^(?:- *)?(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? * in *([^\n]+)/.exec(this.input)) {
  477. this.consume(captures[0].length);
  478. var tok = this.tok('each', captures[1]);
  479. tok.key = captures[2] || '$index';
  480. assertExpression(captures[3])
  481. tok.code = captures[3];
  482. return tok;
  483. }
  484. },
  485. /**
  486. * Code.
  487. */
  488. code: function() {
  489. var captures;
  490. if (captures = /^(!?=|-)[ \t]*([^\n]+)/.exec(this.input)) {
  491. this.consume(captures[0].length);
  492. var flags = captures[1];
  493. captures[1] = captures[2];
  494. var tok = this.tok('code', captures[1]);
  495. tok.escape = flags.charAt(0) === '=';
  496. tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '=';
  497. if (tok.buffer) assertExpression(captures[1])
  498. return tok;
  499. }
  500. },
  501. /**
  502. * Block code.
  503. */
  504. blockCode: function() {
  505. var captures;
  506. if (captures = /^-\n/.exec(this.input)) {
  507. this.consume(captures[0].length - 1);
  508. var tok = this.tok('blockCode');
  509. this.pipeless = true;
  510. return tok;
  511. }
  512. },
  513. /**
  514. * Attributes.
  515. */
  516. attrs: function() {
  517. if ('(' == this.input.charAt(0)) {
  518. var index = this.bracketExpression().end
  519. , str = this.input.substr(1, index-1)
  520. , tok = this.tok('attrs');
  521. assertNestingCorrect(str);
  522. var quote = '';
  523. var interpolate = function (attr) {
  524. return attr.replace(/(\\)?#\{(.+)/g, function(_, escape, expr){
  525. if (escape) return _;
  526. try {
  527. var range = characterParser.parseMax(expr);
  528. if (expr[range.end] !== '}') return _.substr(0, 2) + interpolate(_.substr(2));
  529. assertExpression(range.src)
  530. return quote + " + (" + range.src + ") + " + quote + interpolate(expr.substr(range.end + 1));
  531. } catch (ex) {
  532. return _.substr(0, 2) + interpolate(_.substr(2));
  533. }
  534. });
  535. }
  536. this.consume(index + 1);
  537. tok.attrs = [];
  538. var escapedAttr = true
  539. var key = '';
  540. var val = '';
  541. var interpolatable = '';
  542. var state = characterParser.defaultState();
  543. var loc = 'key';
  544. var isEndOfAttribute = function (i) {
  545. if (key.trim() === '') return false;
  546. if (i === str.length) return true;
  547. if (loc === 'key') {
  548. if (str[i] === ' ' || str[i] === '\n') {
  549. for (var x = i; x < str.length; x++) {
  550. if (str[x] != ' ' && str[x] != '\n') {
  551. if (str[x] === '=' || str[x] === '!' || str[x] === ',') return false;
  552. else return true;
  553. }
  554. }
  555. }
  556. return str[i] === ','
  557. } else if (loc === 'value' && !state.isNesting()) {
  558. try {
  559. assertExpression(val);
  560. if (str[i] === ' ' || str[i] === '\n') {
  561. for (var x = i; x < str.length; x++) {
  562. if (str[x] != ' ' && str[x] != '\n') {
  563. if (characterParser.isPunctuator(str[x]) && str[x] != '"' && str[x] != "'") return false;
  564. else return true;
  565. }
  566. }
  567. }
  568. return str[i] === ',';
  569. } catch (ex) {
  570. return false;
  571. }
  572. }
  573. }
  574. this.lineno += str.split("\n").length - 1;
  575. for (var i = 0; i <= str.length; i++) {
  576. if (isEndOfAttribute(i)) {
  577. val = val.trim();
  578. if (val) assertExpression(val)
  579. key = key.trim();
  580. key = key.replace(/^['"]|['"]$/g, '');
  581. tok.attrs.push({
  582. name: key,
  583. val: '' == val ? true : val,
  584. escaped: escapedAttr
  585. });
  586. key = val = '';
  587. loc = 'key';
  588. escapedAttr = false;
  589. } else {
  590. switch (loc) {
  591. case 'key-char':
  592. if (str[i] === quote) {
  593. loc = 'key';
  594. if (i + 1 < str.length && [' ', ',', '!', '=', '\n'].indexOf(str[i + 1]) === -1)
  595. throw new Error('Unexpected character ' + str[i + 1] + ' expected ` `, `\\n`, `,`, `!` or `=`');
  596. } else {
  597. key += str[i];
  598. }
  599. break;
  600. case 'key':
  601. if (key === '' && (str[i] === '"' || str[i] === "'")) {
  602. loc = 'key-char';
  603. quote = str[i];
  604. } else if (str[i] === '!' || str[i] === '=') {
  605. escapedAttr = str[i] !== '!';
  606. if (str[i] === '!') i++;
  607. if (str[i] !== '=') throw new Error('Unexpected character ' + str[i] + ' expected `=`');
  608. loc = 'value';
  609. state = characterParser.defaultState();
  610. } else {
  611. key += str[i]
  612. }
  613. break;
  614. case 'value':
  615. state = characterParser.parseChar(str[i], state);
  616. if (state.isString()) {
  617. loc = 'string';
  618. quote = str[i];
  619. interpolatable = str[i];
  620. } else {
  621. val += str[i];
  622. }
  623. break;
  624. case 'string':
  625. state = characterParser.parseChar(str[i], state);
  626. interpolatable += str[i];
  627. if (!state.isString()) {
  628. loc = 'value';
  629. val += interpolate(interpolatable);
  630. }
  631. break;
  632. }
  633. }
  634. }
  635. if ('/' == this.input.charAt(0)) {
  636. this.consume(1);
  637. tok.selfClosing = true;
  638. }
  639. return tok;
  640. }
  641. },
  642. /**
  643. * &attributes block
  644. */
  645. attributesBlock: function () {
  646. var captures;
  647. if (/^&attributes\b/.test(this.input)) {
  648. this.consume(11);
  649. var args = this.bracketExpression();
  650. this.consume(args.end + 1);
  651. return this.tok('&attributes', args.src);
  652. }
  653. },
  654. /**
  655. * Indent | Outdent | Newline.
  656. */
  657. indent: function() {
  658. var captures, re;
  659. // established regexp
  660. if (this.indentRe) {
  661. captures = this.indentRe.exec(this.input);
  662. // determine regexp
  663. } else {
  664. // tabs
  665. re = /^\n(\t*) */;
  666. captures = re.exec(this.input);
  667. // spaces
  668. if (captures && !captures[1].length) {
  669. re = /^\n( *)/;
  670. captures = re.exec(this.input);
  671. }
  672. // established
  673. if (captures && captures[1].length) this.indentRe = re;
  674. }
  675. if (captures) {
  676. var tok
  677. , indents = captures[1].length;
  678. ++this.lineno;
  679. this.consume(indents + 1);
  680. if (' ' == this.input[0] || '\t' == this.input[0]) {
  681. throw new Error('Invalid indentation, you can use tabs or spaces but not both');
  682. }
  683. // blank line
  684. if ('\n' == this.input[0]) {
  685. this.pipeless = false;
  686. return this.tok('newline');
  687. }
  688. // outdent
  689. if (this.indentStack.length && indents < this.indentStack[0]) {
  690. while (this.indentStack.length && this.indentStack[0] > indents) {
  691. this.stash.push(this.tok('outdent'));
  692. this.indentStack.shift();
  693. }
  694. tok = this.stash.pop();
  695. // indent
  696. } else if (indents && indents != this.indentStack[0]) {
  697. this.indentStack.unshift(indents);
  698. tok = this.tok('indent', indents);
  699. // newline
  700. } else {
  701. tok = this.tok('newline');
  702. }
  703. this.pipeless = false;
  704. return tok;
  705. }
  706. },
  707. /**
  708. * Pipe-less text consumed only when
  709. * pipeless is true;
  710. */
  711. pipelessText: function() {
  712. if (!this.pipeless) return;
  713. var captures, re;
  714. // established regexp
  715. if (this.indentRe) {
  716. captures = this.indentRe.exec(this.input);
  717. // determine regexp
  718. } else {
  719. // tabs
  720. re = /^\n(\t*) */;
  721. captures = re.exec(this.input);
  722. // spaces
  723. if (captures && !captures[1].length) {
  724. re = /^\n( *)/;
  725. captures = re.exec(this.input);
  726. }
  727. // established
  728. if (captures && captures[1].length) this.indentRe = re;
  729. }
  730. var indents = captures && captures[1].length;
  731. if (indents && (this.indentStack.length === 0 || indents > this.indentStack[0])) {
  732. var indent = captures[1];
  733. var line;
  734. var tokens = [];
  735. var isMatch;
  736. do {
  737. // text has `\n` as a prefix
  738. var i = this.input.substr(1).indexOf('\n');
  739. if (-1 == i) i = this.input.length - 1;
  740. var str = this.input.substr(1, i);
  741. isMatch = str.substr(0, indent.length) === indent || !str.trim();
  742. if (isMatch) {
  743. // consume test along with `\n` prefix if match
  744. this.consume(str.length + 1);
  745. ++this.lineno;
  746. tokens.push(str.substr(indent.length));
  747. }
  748. } while(this.input.length && isMatch);
  749. while (this.input.length === 0 && tokens[tokens.length - 1] === '') tokens.pop();
  750. return this.tok('pipeless-text', tokens);
  751. }
  752. },
  753. /**
  754. * ':'
  755. */
  756. colon: function() {
  757. var good = /^: +/.test(this.input);
  758. var res = this.scan(/^: */, ':');
  759. if (res && !good) {
  760. console.warn('Warning: space required after `:` on line ' + this.lineno +
  761. ' of jade file "' + this.filename + '"');
  762. }
  763. return res;
  764. },
  765. fail: function () {
  766. throw new Error('unexpected text ' + this.input.substr(0, 5));
  767. },
  768. /**
  769. * Return the next token object, or those
  770. * previously stashed by lookahead.
  771. *
  772. * @return {Object}
  773. * @api private
  774. */
  775. advance: function(){
  776. return this.stashed()
  777. || this.next();
  778. },
  779. /**
  780. * Return the next token object.
  781. *
  782. * @return {Object}
  783. * @api private
  784. */
  785. next: function() {
  786. return this.deferred()
  787. || this.blank()
  788. || this.eos()
  789. || this.pipelessText()
  790. || this.yield()
  791. || this.doctype()
  792. || this.interpolation()
  793. || this["case"]()
  794. || this.when()
  795. || this["default"]()
  796. || this["extends"]()
  797. || this.append()
  798. || this.prepend()
  799. || this.block()
  800. || this.mixinBlock()
  801. || this.include()
  802. || this.includeFiltered()
  803. || this.mixin()
  804. || this.call()
  805. || this.conditional()
  806. || this.each()
  807. || this["while"]()
  808. || this.tag()
  809. || this.filter()
  810. || this.blockCode()
  811. || this.code()
  812. || this.id()
  813. || this.className()
  814. || this.attrs()
  815. || this.attributesBlock()
  816. || this.indent()
  817. || this.text()
  818. || this.comment()
  819. || this.colon()
  820. || this.dot()
  821. || this.textFail()
  822. || this.fail();
  823. }
  824. };