index.js 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711
  1. 'use strict';
  2. var assert = require('assert');
  3. var isExpression = require('is-expression');
  4. var characterParser = require('character-parser');
  5. var error = require('pug-error');
  6. module.exports = lex;
  7. module.exports.Lexer = Lexer;
  8. function lex(str, options) {
  9. var lexer = new Lexer(str, options);
  10. return JSON.parse(JSON.stringify(lexer.getTokens()));
  11. }
  12. /**
  13. * Initialize `Lexer` with the given `str`.
  14. *
  15. * @param {String} str
  16. * @param {String} filename
  17. * @api private
  18. */
  19. function Lexer(str, options) {
  20. options = options || {};
  21. if (typeof str !== 'string') {
  22. throw new Error(
  23. 'Expected source code to be a string but got "' + typeof str + '"'
  24. );
  25. }
  26. if (typeof options !== 'object') {
  27. throw new Error(
  28. 'Expected "options" to be an object but got "' + typeof options + '"'
  29. );
  30. }
  31. //Strip any UTF-8 BOM off of the start of `str`, if it exists.
  32. str = str.replace(/^\uFEFF/, '');
  33. this.input = str.replace(/\r\n|\r/g, '\n');
  34. this.originalInput = this.input;
  35. this.filename = options.filename;
  36. this.interpolated = options.interpolated || false;
  37. this.lineno = options.startingLine || 1;
  38. this.colno = options.startingColumn || 1;
  39. this.plugins = options.plugins || [];
  40. this.indentStack = [0];
  41. this.indentRe = null;
  42. // If #{}, !{} or #[] syntax is allowed when adding text
  43. this.interpolationAllowed = true;
  44. this.whitespaceRe = /[ \n\t]/;
  45. this.tokens = [];
  46. this.ended = false;
  47. }
  48. /**
  49. * Lexer prototype.
  50. */
  51. Lexer.prototype = {
  52. constructor: Lexer,
  53. error: function(code, message) {
  54. var err = error(code, message, {
  55. line: this.lineno,
  56. column: this.colno,
  57. filename: this.filename,
  58. src: this.originalInput,
  59. });
  60. throw err;
  61. },
  62. assert: function(value, message) {
  63. if (!value) this.error('ASSERT_FAILED', message);
  64. },
  65. isExpression: function(exp) {
  66. return isExpression(exp, {
  67. throw: true,
  68. });
  69. },
  70. assertExpression: function(exp, noThrow) {
  71. //this verifies that a JavaScript expression is valid
  72. try {
  73. this.callLexerFunction('isExpression', exp);
  74. return true;
  75. } catch (ex) {
  76. if (noThrow) return false;
  77. // not coming from acorn
  78. if (!ex.loc) throw ex;
  79. this.incrementLine(ex.loc.line - 1);
  80. this.incrementColumn(ex.loc.column);
  81. var msg =
  82. 'Syntax Error: ' + ex.message.replace(/ \([0-9]+:[0-9]+\)$/, '');
  83. this.error('SYNTAX_ERROR', msg);
  84. }
  85. },
  86. assertNestingCorrect: function(exp) {
  87. //this verifies that code is properly nested, but allows
  88. //invalid JavaScript such as the contents of `attributes`
  89. var res = characterParser(exp);
  90. if (res.isNesting()) {
  91. this.error(
  92. 'INCORRECT_NESTING',
  93. 'Nesting must match on expression `' + exp + '`'
  94. );
  95. }
  96. },
  97. /**
  98. * Construct a token with the given `type` and `val`.
  99. *
  100. * @param {String} type
  101. * @param {String} val
  102. * @return {Object}
  103. * @api private
  104. */
  105. tok: function(type, val) {
  106. var res = {
  107. type: type,
  108. loc: {
  109. start: {
  110. line: this.lineno,
  111. column: this.colno,
  112. },
  113. filename: this.filename,
  114. },
  115. };
  116. if (val !== undefined) res.val = val;
  117. return res;
  118. },
  119. /**
  120. * Set the token's `loc.end` value.
  121. *
  122. * @param {Object} tok
  123. * @returns {Object}
  124. * @api private
  125. */
  126. tokEnd: function(tok) {
  127. tok.loc.end = {
  128. line: this.lineno,
  129. column: this.colno,
  130. };
  131. return tok;
  132. },
  133. /**
  134. * Increment `this.lineno` and reset `this.colno`.
  135. *
  136. * @param {Number} increment
  137. * @api private
  138. */
  139. incrementLine: function(increment) {
  140. this.lineno += increment;
  141. if (increment) this.colno = 1;
  142. },
  143. /**
  144. * Increment `this.colno`.
  145. *
  146. * @param {Number} increment
  147. * @api private
  148. */
  149. incrementColumn: function(increment) {
  150. this.colno += increment;
  151. },
  152. /**
  153. * Consume the given `len` of input.
  154. *
  155. * @param {Number} len
  156. * @api private
  157. */
  158. consume: function(len) {
  159. this.input = this.input.substr(len);
  160. },
  161. /**
  162. * Scan for `type` with the given `regexp`.
  163. *
  164. * @param {String} type
  165. * @param {RegExp} regexp
  166. * @return {Object}
  167. * @api private
  168. */
  169. scan: function(regexp, type) {
  170. var captures;
  171. if ((captures = regexp.exec(this.input))) {
  172. var len = captures[0].length;
  173. var val = captures[1];
  174. var diff = len - (val ? val.length : 0);
  175. var tok = this.tok(type, val);
  176. this.consume(len);
  177. this.incrementColumn(diff);
  178. return tok;
  179. }
  180. },
  181. scanEndOfLine: function(regexp, type) {
  182. var captures;
  183. if ((captures = regexp.exec(this.input))) {
  184. var whitespaceLength = 0;
  185. var whitespace;
  186. var tok;
  187. if ((whitespace = /^([ ]+)([^ ]*)/.exec(captures[0]))) {
  188. whitespaceLength = whitespace[1].length;
  189. this.incrementColumn(whitespaceLength);
  190. }
  191. var newInput = this.input.substr(captures[0].length);
  192. if (newInput[0] === ':') {
  193. this.input = newInput;
  194. tok = this.tok(type, captures[1]);
  195. this.incrementColumn(captures[0].length - whitespaceLength);
  196. return tok;
  197. }
  198. if (/^[ \t]*(\n|$)/.test(newInput)) {
  199. this.input = newInput.substr(/^[ \t]*/.exec(newInput)[0].length);
  200. tok = this.tok(type, captures[1]);
  201. this.incrementColumn(captures[0].length - whitespaceLength);
  202. return tok;
  203. }
  204. }
  205. },
  206. /**
  207. * Return the indexOf `(` or `{` or `[` / `)` or `}` or `]` delimiters.
  208. *
  209. * Make sure that when calling this function, colno is at the character
  210. * immediately before the beginning.
  211. *
  212. * @return {Number}
  213. * @api private
  214. */
  215. bracketExpression: function(skip) {
  216. skip = skip || 0;
  217. var start = this.input[skip];
  218. assert(
  219. start === '(' || start === '{' || start === '[',
  220. 'The start character should be "(", "{" or "["'
  221. );
  222. var end = characterParser.BRACKETS[start];
  223. var range;
  224. try {
  225. range = characterParser.parseUntil(this.input, end, {start: skip + 1});
  226. } catch (ex) {
  227. if (ex.index !== undefined) {
  228. var idx = ex.index;
  229. // starting from this.input[skip]
  230. var tmp = this.input.substr(skip).indexOf('\n');
  231. // starting from this.input[0]
  232. var nextNewline = tmp + skip;
  233. var ptr = 0;
  234. while (idx > nextNewline && tmp !== -1) {
  235. this.incrementLine(1);
  236. idx -= nextNewline + 1;
  237. ptr += nextNewline + 1;
  238. tmp = nextNewline = this.input.substr(ptr).indexOf('\n');
  239. }
  240. this.incrementColumn(idx);
  241. }
  242. if (ex.code === 'CHARACTER_PARSER:END_OF_STRING_REACHED') {
  243. this.error(
  244. 'NO_END_BRACKET',
  245. 'The end of the string reached with no closing bracket ' +
  246. end +
  247. ' found.'
  248. );
  249. } else if (ex.code === 'CHARACTER_PARSER:MISMATCHED_BRACKET') {
  250. this.error('BRACKET_MISMATCH', ex.message);
  251. }
  252. throw ex;
  253. }
  254. return range;
  255. },
  256. scanIndentation: function() {
  257. var captures, re;
  258. // established regexp
  259. if (this.indentRe) {
  260. captures = this.indentRe.exec(this.input);
  261. // determine regexp
  262. } else {
  263. // tabs
  264. re = /^\n(\t*) */;
  265. captures = re.exec(this.input);
  266. // spaces
  267. if (captures && !captures[1].length) {
  268. re = /^\n( *)/;
  269. captures = re.exec(this.input);
  270. }
  271. // established
  272. if (captures && captures[1].length) this.indentRe = re;
  273. }
  274. return captures;
  275. },
  276. /**
  277. * end-of-source.
  278. */
  279. eos: function() {
  280. if (this.input.length) return;
  281. if (this.interpolated) {
  282. this.error(
  283. 'NO_END_BRACKET',
  284. 'End of line was reached with no closing bracket for interpolation.'
  285. );
  286. }
  287. for (var i = 0; this.indentStack[i]; i++) {
  288. this.tokens.push(this.tokEnd(this.tok('outdent')));
  289. }
  290. this.tokens.push(this.tokEnd(this.tok('eos')));
  291. this.ended = true;
  292. return true;
  293. },
  294. /**
  295. * Blank line.
  296. */
  297. blank: function() {
  298. var captures;
  299. if ((captures = /^\n[ \t]*\n/.exec(this.input))) {
  300. this.consume(captures[0].length - 1);
  301. this.incrementLine(1);
  302. return true;
  303. }
  304. },
  305. /**
  306. * Comment.
  307. */
  308. comment: function() {
  309. var captures;
  310. if ((captures = /^\/\/(-)?([^\n]*)/.exec(this.input))) {
  311. this.consume(captures[0].length);
  312. var tok = this.tok('comment', captures[2]);
  313. tok.buffer = '-' != captures[1];
  314. this.interpolationAllowed = tok.buffer;
  315. this.tokens.push(tok);
  316. this.incrementColumn(captures[0].length);
  317. this.tokEnd(tok);
  318. this.callLexerFunction('pipelessText');
  319. return true;
  320. }
  321. },
  322. /**
  323. * Interpolated tag.
  324. */
  325. interpolation: function() {
  326. if (/^#\{/.test(this.input)) {
  327. var match = this.bracketExpression(1);
  328. this.consume(match.end + 1);
  329. var tok = this.tok('interpolation', match.src);
  330. this.tokens.push(tok);
  331. this.incrementColumn(2); // '#{'
  332. this.assertExpression(match.src);
  333. var splitted = match.src.split('\n');
  334. var lines = splitted.length - 1;
  335. this.incrementLine(lines);
  336. this.incrementColumn(splitted[lines].length + 1); // + 1 → '}'
  337. this.tokEnd(tok);
  338. return true;
  339. }
  340. },
  341. /**
  342. * Tag.
  343. */
  344. tag: function() {
  345. var captures;
  346. if ((captures = /^(\w(?:[-:\w]*\w)?)/.exec(this.input))) {
  347. var tok,
  348. name = captures[1],
  349. len = captures[0].length;
  350. this.consume(len);
  351. tok = this.tok('tag', name);
  352. this.tokens.push(tok);
  353. this.incrementColumn(len);
  354. this.tokEnd(tok);
  355. return true;
  356. }
  357. },
  358. /**
  359. * Filter.
  360. */
  361. filter: function(opts) {
  362. var tok = this.scan(/^:([\w\-]+)/, 'filter');
  363. var inInclude = opts && opts.inInclude;
  364. if (tok) {
  365. this.tokens.push(tok);
  366. this.incrementColumn(tok.val.length);
  367. this.tokEnd(tok);
  368. this.callLexerFunction('attrs');
  369. if (!inInclude) {
  370. this.interpolationAllowed = false;
  371. this.callLexerFunction('pipelessText');
  372. }
  373. return true;
  374. }
  375. },
  376. /**
  377. * Doctype.
  378. */
  379. doctype: function() {
  380. var node = this.scanEndOfLine(/^doctype *([^\n]*)/, 'doctype');
  381. if (node) {
  382. this.tokens.push(this.tokEnd(node));
  383. return true;
  384. }
  385. },
  386. /**
  387. * Id.
  388. */
  389. id: function() {
  390. var tok = this.scan(/^#([\w-]+)/, 'id');
  391. if (tok) {
  392. this.tokens.push(tok);
  393. this.incrementColumn(tok.val.length);
  394. this.tokEnd(tok);
  395. return true;
  396. }
  397. if (/^#/.test(this.input)) {
  398. this.error(
  399. 'INVALID_ID',
  400. '"' +
  401. /.[^ \t\(\#\.\:]*/.exec(this.input.substr(1))[0] +
  402. '" is not a valid ID.'
  403. );
  404. }
  405. },
  406. /**
  407. * Class.
  408. */
  409. className: function() {
  410. var tok = this.scan(/^\.([_a-z0-9\-]*[_a-z][_a-z0-9\-]*)/i, 'class');
  411. if (tok) {
  412. this.tokens.push(tok);
  413. this.incrementColumn(tok.val.length);
  414. this.tokEnd(tok);
  415. return true;
  416. }
  417. if (/^\.[_a-z0-9\-]+/i.test(this.input)) {
  418. this.error(
  419. 'INVALID_CLASS_NAME',
  420. 'Class names must contain at least one letter or underscore.'
  421. );
  422. }
  423. if (/^\./.test(this.input)) {
  424. this.error(
  425. 'INVALID_CLASS_NAME',
  426. '"' +
  427. /.[^ \t\(\#\.\:]*/.exec(this.input.substr(1))[0] +
  428. '" is not a valid class name. Class names can only contain "_", "-", a-z and 0-9, and must contain at least one of "_", or a-z'
  429. );
  430. }
  431. },
  432. /**
  433. * Text.
  434. */
  435. endInterpolation: function() {
  436. if (this.interpolated && this.input[0] === ']') {
  437. this.input = this.input.substr(1);
  438. this.ended = true;
  439. return true;
  440. }
  441. },
  442. addText: function(type, value, prefix, escaped) {
  443. var tok;
  444. if (value + prefix === '') return;
  445. prefix = prefix || '';
  446. escaped = escaped || 0;
  447. var indexOfEnd = this.interpolated ? value.indexOf(']') : -1;
  448. var indexOfStart = this.interpolationAllowed ? value.indexOf('#[') : -1;
  449. var indexOfEscaped = this.interpolationAllowed ? value.indexOf('\\#[') : -1;
  450. var matchOfStringInterp = /(\\)?([#!]){((?:.|\n)*)$/.exec(value);
  451. var indexOfStringInterp =
  452. this.interpolationAllowed && matchOfStringInterp
  453. ? matchOfStringInterp.index
  454. : Infinity;
  455. if (indexOfEnd === -1) indexOfEnd = Infinity;
  456. if (indexOfStart === -1) indexOfStart = Infinity;
  457. if (indexOfEscaped === -1) indexOfEscaped = Infinity;
  458. if (
  459. indexOfEscaped !== Infinity &&
  460. indexOfEscaped < indexOfEnd &&
  461. indexOfEscaped < indexOfStart &&
  462. indexOfEscaped < indexOfStringInterp
  463. ) {
  464. prefix = prefix + value.substring(0, indexOfEscaped) + '#[';
  465. return this.addText(
  466. type,
  467. value.substring(indexOfEscaped + 3),
  468. prefix,
  469. escaped + 1
  470. );
  471. }
  472. if (
  473. indexOfStart !== Infinity &&
  474. indexOfStart < indexOfEnd &&
  475. indexOfStart < indexOfEscaped &&
  476. indexOfStart < indexOfStringInterp
  477. ) {
  478. tok = this.tok(type, prefix + value.substring(0, indexOfStart));
  479. this.incrementColumn(prefix.length + indexOfStart + escaped);
  480. this.tokens.push(this.tokEnd(tok));
  481. tok = this.tok('start-pug-interpolation');
  482. this.incrementColumn(2);
  483. this.tokens.push(this.tokEnd(tok));
  484. var child = new this.constructor(value.substr(indexOfStart + 2), {
  485. filename: this.filename,
  486. interpolated: true,
  487. startingLine: this.lineno,
  488. startingColumn: this.colno,
  489. plugins: this.plugins,
  490. });
  491. var interpolated;
  492. try {
  493. interpolated = child.getTokens();
  494. } catch (ex) {
  495. if (ex.code && /^PUG:/.test(ex.code)) {
  496. this.colno = ex.column;
  497. this.error(ex.code.substr(4), ex.msg);
  498. }
  499. throw ex;
  500. }
  501. this.colno = child.colno;
  502. this.tokens = this.tokens.concat(interpolated);
  503. tok = this.tok('end-pug-interpolation');
  504. this.incrementColumn(1);
  505. this.tokens.push(this.tokEnd(tok));
  506. this.addText(type, child.input);
  507. return;
  508. }
  509. if (
  510. indexOfEnd !== Infinity &&
  511. indexOfEnd < indexOfStart &&
  512. indexOfEnd < indexOfEscaped &&
  513. indexOfEnd < indexOfStringInterp
  514. ) {
  515. if (prefix + value.substring(0, indexOfEnd)) {
  516. this.addText(type, value.substring(0, indexOfEnd), prefix);
  517. }
  518. this.ended = true;
  519. this.input = value.substr(value.indexOf(']') + 1) + this.input;
  520. return;
  521. }
  522. if (indexOfStringInterp !== Infinity) {
  523. if (matchOfStringInterp[1]) {
  524. prefix =
  525. prefix +
  526. value.substring(0, indexOfStringInterp) +
  527. matchOfStringInterp[2] +
  528. '{';
  529. return this.addText(
  530. type,
  531. value.substring(indexOfStringInterp + 3),
  532. prefix,
  533. escaped + 1
  534. );
  535. }
  536. var before = value.substr(0, indexOfStringInterp);
  537. if (prefix || before) {
  538. before = prefix + before;
  539. tok = this.tok(type, before);
  540. this.incrementColumn(before.length + escaped);
  541. this.tokens.push(this.tokEnd(tok));
  542. }
  543. var rest = matchOfStringInterp[3];
  544. var range;
  545. tok = this.tok('interpolated-code');
  546. this.incrementColumn(2);
  547. try {
  548. range = characterParser.parseUntil(rest, '}');
  549. } catch (ex) {
  550. if (ex.index !== undefined) {
  551. this.incrementColumn(ex.index);
  552. }
  553. if (ex.code === 'CHARACTER_PARSER:END_OF_STRING_REACHED') {
  554. this.error(
  555. 'NO_END_BRACKET',
  556. 'End of line was reached with no closing bracket for interpolation.'
  557. );
  558. } else if (ex.code === 'CHARACTER_PARSER:MISMATCHED_BRACKET') {
  559. this.error('BRACKET_MISMATCH', ex.message);
  560. } else {
  561. throw ex;
  562. }
  563. }
  564. tok.mustEscape = matchOfStringInterp[2] === '#';
  565. tok.buffer = true;
  566. tok.val = range.src;
  567. this.assertExpression(range.src);
  568. if (range.end + 1 < rest.length) {
  569. rest = rest.substr(range.end + 1);
  570. this.incrementColumn(range.end + 1);
  571. this.tokens.push(this.tokEnd(tok));
  572. this.addText(type, rest);
  573. } else {
  574. this.incrementColumn(rest.length);
  575. this.tokens.push(this.tokEnd(tok));
  576. }
  577. return;
  578. }
  579. value = prefix + value;
  580. tok = this.tok(type, value);
  581. this.incrementColumn(value.length + escaped);
  582. this.tokens.push(this.tokEnd(tok));
  583. },
  584. text: function() {
  585. var tok =
  586. this.scan(/^(?:\| ?| )([^\n]+)/, 'text') ||
  587. this.scan(/^( )/, 'text') ||
  588. this.scan(/^\|( ?)/, 'text');
  589. if (tok) {
  590. this.addText('text', tok.val);
  591. return true;
  592. }
  593. },
  594. textHtml: function() {
  595. var tok = this.scan(/^(<[^\n]*)/, 'text-html');
  596. if (tok) {
  597. this.addText('text-html', tok.val);
  598. return true;
  599. }
  600. },
  601. /**
  602. * Dot.
  603. */
  604. dot: function() {
  605. var tok;
  606. if ((tok = this.scanEndOfLine(/^\./, 'dot'))) {
  607. this.tokens.push(this.tokEnd(tok));
  608. this.callLexerFunction('pipelessText');
  609. return true;
  610. }
  611. },
  612. /**
  613. * Extends.
  614. */
  615. extends: function() {
  616. var tok = this.scan(/^extends?(?= |$|\n)/, 'extends');
  617. if (tok) {
  618. this.tokens.push(this.tokEnd(tok));
  619. if (!this.callLexerFunction('path')) {
  620. this.error('NO_EXTENDS_PATH', 'missing path for extends');
  621. }
  622. return true;
  623. }
  624. if (this.scan(/^extends?\b/)) {
  625. this.error('MALFORMED_EXTENDS', 'malformed extends');
  626. }
  627. },
  628. /**
  629. * Block prepend.
  630. */
  631. prepend: function() {
  632. var captures;
  633. if ((captures = /^(?:block +)?prepend +([^\n]+)/.exec(this.input))) {
  634. var name = captures[1].trim();
  635. var comment = '';
  636. if (name.indexOf('//') !== -1) {
  637. comment =
  638. '//' +
  639. name
  640. .split('//')
  641. .slice(1)
  642. .join('//');
  643. name = name.split('//')[0].trim();
  644. }
  645. if (!name) return;
  646. var tok = this.tok('block', name);
  647. var len = captures[0].length - comment.length;
  648. while (this.whitespaceRe.test(this.input.charAt(len - 1))) len--;
  649. this.incrementColumn(len);
  650. tok.mode = 'prepend';
  651. this.tokens.push(this.tokEnd(tok));
  652. this.consume(captures[0].length - comment.length);
  653. this.incrementColumn(captures[0].length - comment.length - len);
  654. return true;
  655. }
  656. },
  657. /**
  658. * Block append.
  659. */
  660. append: function() {
  661. var captures;
  662. if ((captures = /^(?:block +)?append +([^\n]+)/.exec(this.input))) {
  663. var name = captures[1].trim();
  664. var comment = '';
  665. if (name.indexOf('//') !== -1) {
  666. comment =
  667. '//' +
  668. name
  669. .split('//')
  670. .slice(1)
  671. .join('//');
  672. name = name.split('//')[0].trim();
  673. }
  674. if (!name) return;
  675. var tok = this.tok('block', name);
  676. var len = captures[0].length - comment.length;
  677. while (this.whitespaceRe.test(this.input.charAt(len - 1))) len--;
  678. this.incrementColumn(len);
  679. tok.mode = 'append';
  680. this.tokens.push(this.tokEnd(tok));
  681. this.consume(captures[0].length - comment.length);
  682. this.incrementColumn(captures[0].length - comment.length - len);
  683. return true;
  684. }
  685. },
  686. /**
  687. * Block.
  688. */
  689. block: function() {
  690. var captures;
  691. if ((captures = /^block +([^\n]+)/.exec(this.input))) {
  692. var name = captures[1].trim();
  693. var comment = '';
  694. if (name.indexOf('//') !== -1) {
  695. comment =
  696. '//' +
  697. name
  698. .split('//')
  699. .slice(1)
  700. .join('//');
  701. name = name.split('//')[0].trim();
  702. }
  703. if (!name) return;
  704. var tok = this.tok('block', name);
  705. var len = captures[0].length - comment.length;
  706. while (this.whitespaceRe.test(this.input.charAt(len - 1))) len--;
  707. this.incrementColumn(len);
  708. tok.mode = 'replace';
  709. this.tokens.push(this.tokEnd(tok));
  710. this.consume(captures[0].length - comment.length);
  711. this.incrementColumn(captures[0].length - comment.length - len);
  712. return true;
  713. }
  714. },
  715. /**
  716. * Mixin Block.
  717. */
  718. mixinBlock: function() {
  719. var tok;
  720. if ((tok = this.scanEndOfLine(/^block/, 'mixin-block'))) {
  721. this.tokens.push(this.tokEnd(tok));
  722. return true;
  723. }
  724. },
  725. /**
  726. * Yield.
  727. */
  728. yield: function() {
  729. var tok = this.scanEndOfLine(/^yield/, 'yield');
  730. if (tok) {
  731. this.tokens.push(this.tokEnd(tok));
  732. return true;
  733. }
  734. },
  735. /**
  736. * Include.
  737. */
  738. include: function() {
  739. var tok = this.scan(/^include(?=:| |$|\n)/, 'include');
  740. if (tok) {
  741. this.tokens.push(this.tokEnd(tok));
  742. while (this.callLexerFunction('filter', {inInclude: true}));
  743. if (!this.callLexerFunction('path')) {
  744. if (/^[^ \n]+/.test(this.input)) {
  745. // if there is more text
  746. this.fail();
  747. } else {
  748. // if not
  749. this.error('NO_INCLUDE_PATH', 'missing path for include');
  750. }
  751. }
  752. return true;
  753. }
  754. if (this.scan(/^include\b/)) {
  755. this.error('MALFORMED_INCLUDE', 'malformed include');
  756. }
  757. },
  758. /**
  759. * Path
  760. */
  761. path: function() {
  762. var tok = this.scanEndOfLine(/^ ([^\n]+)/, 'path');
  763. if (tok && (tok.val = tok.val.trim())) {
  764. this.tokens.push(this.tokEnd(tok));
  765. return true;
  766. }
  767. },
  768. /**
  769. * Case.
  770. */
  771. case: function() {
  772. var tok = this.scanEndOfLine(/^case +([^\n]+)/, 'case');
  773. if (tok) {
  774. this.incrementColumn(-tok.val.length);
  775. this.assertExpression(tok.val);
  776. this.incrementColumn(tok.val.length);
  777. this.tokens.push(this.tokEnd(tok));
  778. return true;
  779. }
  780. if (this.scan(/^case\b/)) {
  781. this.error('NO_CASE_EXPRESSION', 'missing expression for case');
  782. }
  783. },
  784. /**
  785. * When.
  786. */
  787. when: function() {
  788. var tok = this.scanEndOfLine(/^when +([^:\n]+)/, 'when');
  789. if (tok) {
  790. var parser = characterParser(tok.val);
  791. while (parser.isNesting() || parser.isString()) {
  792. var rest = /:([^:\n]+)/.exec(this.input);
  793. if (!rest) break;
  794. tok.val += rest[0];
  795. this.consume(rest[0].length);
  796. this.incrementColumn(rest[0].length);
  797. parser = characterParser(tok.val);
  798. }
  799. this.incrementColumn(-tok.val.length);
  800. this.assertExpression(tok.val);
  801. this.incrementColumn(tok.val.length);
  802. this.tokens.push(this.tokEnd(tok));
  803. return true;
  804. }
  805. if (this.scan(/^when\b/)) {
  806. this.error('NO_WHEN_EXPRESSION', 'missing expression for when');
  807. }
  808. },
  809. /**
  810. * Default.
  811. */
  812. default: function() {
  813. var tok = this.scanEndOfLine(/^default/, 'default');
  814. if (tok) {
  815. this.tokens.push(this.tokEnd(tok));
  816. return true;
  817. }
  818. if (this.scan(/^default\b/)) {
  819. this.error(
  820. 'DEFAULT_WITH_EXPRESSION',
  821. 'default should not have an expression'
  822. );
  823. }
  824. },
  825. /**
  826. * Call mixin.
  827. */
  828. call: function() {
  829. var tok, captures, increment;
  830. if ((captures = /^\+(\s*)(([-\w]+)|(#\{))/.exec(this.input))) {
  831. // try to consume simple or interpolated call
  832. if (captures[3]) {
  833. // simple call
  834. increment = captures[0].length;
  835. this.consume(increment);
  836. tok = this.tok('call', captures[3]);
  837. } else {
  838. // interpolated call
  839. var match = this.bracketExpression(2 + captures[1].length);
  840. increment = match.end + 1;
  841. this.consume(increment);
  842. this.assertExpression(match.src);
  843. tok = this.tok('call', '#{' + match.src + '}');
  844. }
  845. this.incrementColumn(increment);
  846. tok.args = null;
  847. // Check for args (not attributes)
  848. if ((captures = /^ *\(/.exec(this.input))) {
  849. var range = this.bracketExpression(captures[0].length - 1);
  850. if (!/^\s*[-\w]+ *=/.test(range.src)) {
  851. // not attributes
  852. this.incrementColumn(1);
  853. this.consume(range.end + 1);
  854. tok.args = range.src;
  855. this.assertExpression('[' + tok.args + ']');
  856. for (var i = 0; i <= tok.args.length; i++) {
  857. if (tok.args[i] === '\n') {
  858. this.incrementLine(1);
  859. } else {
  860. this.incrementColumn(1);
  861. }
  862. }
  863. }
  864. }
  865. this.tokens.push(this.tokEnd(tok));
  866. return true;
  867. }
  868. },
  869. /**
  870. * Mixin.
  871. */
  872. mixin: function() {
  873. var captures;
  874. if ((captures = /^mixin +([-\w]+)(?: *\((.*)\))? */.exec(this.input))) {
  875. this.consume(captures[0].length);
  876. var tok = this.tok('mixin', captures[1]);
  877. tok.args = captures[2] || null;
  878. this.incrementColumn(captures[0].length);
  879. this.tokens.push(this.tokEnd(tok));
  880. return true;
  881. }
  882. },
  883. /**
  884. * Conditional.
  885. */
  886. conditional: function() {
  887. var captures;
  888. if ((captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input))) {
  889. this.consume(captures[0].length);
  890. var type = captures[1].replace(/ /g, '-');
  891. var js = captures[2] && captures[2].trim();
  892. // type can be "if", "else-if" and "else"
  893. var tok = this.tok(type, js);
  894. this.incrementColumn(captures[0].length - js.length);
  895. switch (type) {
  896. case 'if':
  897. case 'else-if':
  898. this.assertExpression(js);
  899. break;
  900. case 'unless':
  901. this.assertExpression(js);
  902. tok.val = '!(' + js + ')';
  903. tok.type = 'if';
  904. break;
  905. case 'else':
  906. if (js) {
  907. this.error(
  908. 'ELSE_CONDITION',
  909. '`else` cannot have a condition, perhaps you meant `else if`'
  910. );
  911. }
  912. break;
  913. }
  914. this.incrementColumn(js.length);
  915. this.tokens.push(this.tokEnd(tok));
  916. return true;
  917. }
  918. },
  919. /**
  920. * While.
  921. */
  922. while: function() {
  923. var captures, tok;
  924. if ((captures = /^while +([^\n]+)/.exec(this.input))) {
  925. this.consume(captures[0].length);
  926. this.assertExpression(captures[1]);
  927. tok = this.tok('while', captures[1]);
  928. this.incrementColumn(captures[0].length);
  929. this.tokens.push(this.tokEnd(tok));
  930. return true;
  931. }
  932. if (this.scan(/^while\b/)) {
  933. this.error('NO_WHILE_EXPRESSION', 'missing expression for while');
  934. }
  935. },
  936. /**
  937. * Each.
  938. */
  939. each: function() {
  940. var captures;
  941. if (
  942. (captures = /^(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? * in *([^\n]+)/.exec(
  943. this.input
  944. ))
  945. ) {
  946. this.consume(captures[0].length);
  947. var tok = this.tok('each', captures[1]);
  948. tok.key = captures[2] || null;
  949. this.incrementColumn(captures[0].length - captures[3].length);
  950. this.assertExpression(captures[3]);
  951. tok.code = captures[3];
  952. this.incrementColumn(captures[3].length);
  953. this.tokens.push(this.tokEnd(tok));
  954. return true;
  955. }
  956. const name = /^each\b/.exec(this.input) ? 'each' : 'for';
  957. if (this.scan(/^(?:each|for)\b/)) {
  958. this.error(
  959. 'MALFORMED_EACH',
  960. 'This `' +
  961. name +
  962. '` has a syntax error. `' +
  963. name +
  964. '` statements should be of the form: `' +
  965. name +
  966. ' VARIABLE_NAME of JS_EXPRESSION`'
  967. );
  968. }
  969. if (
  970. (captures = /^- *(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? +in +([^\n]+)/.exec(
  971. this.input
  972. ))
  973. ) {
  974. this.error(
  975. 'MALFORMED_EACH',
  976. 'Pug each and for should no longer be prefixed with a dash ("-"). They are pug keywords and not part of JavaScript.'
  977. );
  978. }
  979. },
  980. /**
  981. * EachOf.
  982. */
  983. eachOf: function() {
  984. var captures;
  985. if ((captures = /^(?:each|for) (.*?) of *([^\n]+)/.exec(this.input))) {
  986. this.consume(captures[0].length);
  987. var tok = this.tok('eachOf', captures[1]);
  988. tok.value = captures[1];
  989. this.incrementColumn(captures[0].length - captures[2].length);
  990. this.assertExpression(captures[2]);
  991. tok.code = captures[2];
  992. this.incrementColumn(captures[2].length);
  993. this.tokens.push(this.tokEnd(tok));
  994. if (
  995. !(
  996. /^[a-zA-Z_$][\w$]*$/.test(tok.value.trim()) ||
  997. /^\[ *[a-zA-Z_$][\w$]* *\, *[a-zA-Z_$][\w$]* *\]$/.test(
  998. tok.value.trim()
  999. )
  1000. )
  1001. ) {
  1002. this.error(
  1003. 'MALFORMED_EACH_OF_LVAL',
  1004. 'The value variable for each must either be a valid identifier (e.g. `item`) or a pair of identifiers in square brackets (e.g. `[key, value]`).'
  1005. );
  1006. }
  1007. return true;
  1008. }
  1009. if (
  1010. (captures = /^- *(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? +of +([^\n]+)/.exec(
  1011. this.input
  1012. ))
  1013. ) {
  1014. this.error(
  1015. 'MALFORMED_EACH',
  1016. 'Pug each and for should not be prefixed with a dash ("-"). They are pug keywords and not part of JavaScript.'
  1017. );
  1018. }
  1019. },
  1020. /**
  1021. * Code.
  1022. */
  1023. code: function() {
  1024. var captures;
  1025. if ((captures = /^(!?=|-)[ \t]*([^\n]+)/.exec(this.input))) {
  1026. var flags = captures[1];
  1027. var code = captures[2];
  1028. var shortened = 0;
  1029. if (this.interpolated) {
  1030. var parsed;
  1031. try {
  1032. parsed = characterParser.parseUntil(code, ']');
  1033. } catch (err) {
  1034. if (err.index !== undefined) {
  1035. this.incrementColumn(captures[0].length - code.length + err.index);
  1036. }
  1037. if (err.code === 'CHARACTER_PARSER:END_OF_STRING_REACHED') {
  1038. this.error(
  1039. 'NO_END_BRACKET',
  1040. 'End of line was reached with no closing bracket for interpolation.'
  1041. );
  1042. } else if (err.code === 'CHARACTER_PARSER:MISMATCHED_BRACKET') {
  1043. this.error('BRACKET_MISMATCH', err.message);
  1044. } else {
  1045. throw err;
  1046. }
  1047. }
  1048. shortened = code.length - parsed.end;
  1049. code = parsed.src;
  1050. }
  1051. var consumed = captures[0].length - shortened;
  1052. this.consume(consumed);
  1053. var tok = this.tok('code', code);
  1054. tok.mustEscape = flags.charAt(0) === '=';
  1055. tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '=';
  1056. // p #[!= abc] hey
  1057. // ^ original colno
  1058. // -------------- captures[0]
  1059. // -------- captures[2]
  1060. // ------ captures[0] - captures[2]
  1061. // ^ after colno
  1062. // = abc
  1063. // ^ original colno
  1064. // ------- captures[0]
  1065. // --- captures[2]
  1066. // ---- captures[0] - captures[2]
  1067. // ^ after colno
  1068. this.incrementColumn(captures[0].length - captures[2].length);
  1069. if (tok.buffer) this.assertExpression(code);
  1070. this.tokens.push(tok);
  1071. // p #[!= abc] hey
  1072. // ^ original colno
  1073. // ----- shortened
  1074. // --- code
  1075. // ^ after colno
  1076. // = abc
  1077. // ^ original colno
  1078. // shortened
  1079. // --- code
  1080. // ^ after colno
  1081. this.incrementColumn(code.length);
  1082. this.tokEnd(tok);
  1083. return true;
  1084. }
  1085. },
  1086. /**
  1087. * Block code.
  1088. */
  1089. blockCode: function() {
  1090. var tok;
  1091. if ((tok = this.scanEndOfLine(/^-/, 'blockcode'))) {
  1092. this.tokens.push(this.tokEnd(tok));
  1093. this.interpolationAllowed = false;
  1094. this.callLexerFunction('pipelessText');
  1095. return true;
  1096. }
  1097. },
  1098. /**
  1099. * Attribute Name.
  1100. */
  1101. attribute: function(str) {
  1102. var quote = '';
  1103. var quoteRe = /['"]/;
  1104. var key = '';
  1105. var i;
  1106. // consume all whitespace before the key
  1107. for (i = 0; i < str.length; i++) {
  1108. if (!this.whitespaceRe.test(str[i])) break;
  1109. if (str[i] === '\n') {
  1110. this.incrementLine(1);
  1111. } else {
  1112. this.incrementColumn(1);
  1113. }
  1114. }
  1115. if (i === str.length) {
  1116. return '';
  1117. }
  1118. var tok = this.tok('attribute');
  1119. // quote?
  1120. if (quoteRe.test(str[i])) {
  1121. quote = str[i];
  1122. this.incrementColumn(1);
  1123. i++;
  1124. }
  1125. // start looping through the key
  1126. for (; i < str.length; i++) {
  1127. if (quote) {
  1128. if (str[i] === quote) {
  1129. this.incrementColumn(1);
  1130. i++;
  1131. break;
  1132. }
  1133. } else {
  1134. if (
  1135. this.whitespaceRe.test(str[i]) ||
  1136. str[i] === '!' ||
  1137. str[i] === '=' ||
  1138. str[i] === ','
  1139. ) {
  1140. break;
  1141. }
  1142. }
  1143. key += str[i];
  1144. if (str[i] === '\n') {
  1145. this.incrementLine(1);
  1146. } else {
  1147. this.incrementColumn(1);
  1148. }
  1149. }
  1150. tok.name = key;
  1151. var valueResponse = this.attributeValue(str.substr(i));
  1152. if (valueResponse.val) {
  1153. tok.val = valueResponse.val;
  1154. tok.mustEscape = valueResponse.mustEscape;
  1155. } else {
  1156. // was a boolean attribute (ex: `input(disabled)`)
  1157. tok.val = true;
  1158. tok.mustEscape = true;
  1159. }
  1160. str = valueResponse.remainingSource;
  1161. this.tokens.push(this.tokEnd(tok));
  1162. for (i = 0; i < str.length; i++) {
  1163. if (!this.whitespaceRe.test(str[i])) {
  1164. break;
  1165. }
  1166. if (str[i] === '\n') {
  1167. this.incrementLine(1);
  1168. } else {
  1169. this.incrementColumn(1);
  1170. }
  1171. }
  1172. if (str[i] === ',') {
  1173. this.incrementColumn(1);
  1174. i++;
  1175. }
  1176. return str.substr(i);
  1177. },
  1178. /**
  1179. * Attribute Value.
  1180. */
  1181. attributeValue: function(str) {
  1182. var quoteRe = /['"]/;
  1183. var val = '';
  1184. var done, i, x;
  1185. var escapeAttr = true;
  1186. var state = characterParser.defaultState();
  1187. var col = this.colno;
  1188. var line = this.lineno;
  1189. // consume all whitespace before the equals sign
  1190. for (i = 0; i < str.length; i++) {
  1191. if (!this.whitespaceRe.test(str[i])) break;
  1192. if (str[i] === '\n') {
  1193. line++;
  1194. col = 1;
  1195. } else {
  1196. col++;
  1197. }
  1198. }
  1199. if (i === str.length) {
  1200. return {remainingSource: str};
  1201. }
  1202. if (str[i] === '!') {
  1203. escapeAttr = false;
  1204. col++;
  1205. i++;
  1206. if (str[i] !== '=')
  1207. this.error(
  1208. 'INVALID_KEY_CHARACTER',
  1209. 'Unexpected character ' + str[i] + ' expected `=`'
  1210. );
  1211. }
  1212. if (str[i] !== '=') {
  1213. // check for anti-pattern `div("foo"bar)`
  1214. if (i === 0 && str && !this.whitespaceRe.test(str[0]) && str[0] !== ',') {
  1215. this.error(
  1216. 'INVALID_KEY_CHARACTER',
  1217. 'Unexpected character ' + str[0] + ' expected `=`'
  1218. );
  1219. } else {
  1220. return {remainingSource: str};
  1221. }
  1222. }
  1223. this.lineno = line;
  1224. this.colno = col + 1;
  1225. i++;
  1226. // consume all whitespace before the value
  1227. for (; i < str.length; i++) {
  1228. if (!this.whitespaceRe.test(str[i])) break;
  1229. if (str[i] === '\n') {
  1230. this.incrementLine(1);
  1231. } else {
  1232. this.incrementColumn(1);
  1233. }
  1234. }
  1235. line = this.lineno;
  1236. col = this.colno;
  1237. // start looping through the value
  1238. for (; i < str.length; i++) {
  1239. // if the character is in a string or in parentheses/brackets/braces
  1240. if (!(state.isNesting() || state.isString())) {
  1241. if (this.whitespaceRe.test(str[i])) {
  1242. done = false;
  1243. // find the first non-whitespace character
  1244. for (x = i; x < str.length; x++) {
  1245. if (!this.whitespaceRe.test(str[x])) {
  1246. // if it is a JavaScript punctuator, then assume that it is
  1247. // a part of the value
  1248. const isNotPunctuator = !characterParser.isPunctuator(str[x]);
  1249. const isQuote = quoteRe.test(str[x]);
  1250. const isColon = str[x] === ':';
  1251. const isSpreadOperator =
  1252. str[x] + str[x + 1] + str[x + 2] === '...';
  1253. if (
  1254. (isNotPunctuator || isQuote || isColon || isSpreadOperator) &&
  1255. this.assertExpression(val, true)
  1256. ) {
  1257. done = true;
  1258. }
  1259. break;
  1260. }
  1261. }
  1262. // if everything else is whitespace, return now so last attribute
  1263. // does not include trailing whitespace
  1264. if (done || x === str.length) {
  1265. break;
  1266. }
  1267. }
  1268. // if there's no whitespace and the character is not ',', the
  1269. // attribute did not end.
  1270. if (str[i] === ',' && this.assertExpression(val, true)) {
  1271. break;
  1272. }
  1273. }
  1274. state = characterParser.parseChar(str[i], state);
  1275. val += str[i];
  1276. if (str[i] === '\n') {
  1277. line++;
  1278. col = 1;
  1279. } else {
  1280. col++;
  1281. }
  1282. }
  1283. this.assertExpression(val);
  1284. this.lineno = line;
  1285. this.colno = col;
  1286. return {val: val, mustEscape: escapeAttr, remainingSource: str.substr(i)};
  1287. },
  1288. /**
  1289. * Attributes.
  1290. */
  1291. attrs: function() {
  1292. var tok;
  1293. if ('(' == this.input.charAt(0)) {
  1294. tok = this.tok('start-attributes');
  1295. var index = this.bracketExpression().end;
  1296. var str = this.input.substr(1, index - 1);
  1297. this.incrementColumn(1);
  1298. this.tokens.push(this.tokEnd(tok));
  1299. this.assertNestingCorrect(str);
  1300. this.consume(index + 1);
  1301. while (str) {
  1302. str = this.attribute(str);
  1303. }
  1304. tok = this.tok('end-attributes');
  1305. this.incrementColumn(1);
  1306. this.tokens.push(this.tokEnd(tok));
  1307. return true;
  1308. }
  1309. },
  1310. /**
  1311. * &attributes block
  1312. */
  1313. attributesBlock: function() {
  1314. if (/^&attributes\b/.test(this.input)) {
  1315. var consumed = 11;
  1316. this.consume(consumed);
  1317. var tok = this.tok('&attributes');
  1318. this.incrementColumn(consumed);
  1319. var args = this.bracketExpression();
  1320. consumed = args.end + 1;
  1321. this.consume(consumed);
  1322. tok.val = args.src;
  1323. this.incrementColumn(consumed);
  1324. this.tokens.push(this.tokEnd(tok));
  1325. return true;
  1326. }
  1327. },
  1328. /**
  1329. * Indent | Outdent | Newline.
  1330. */
  1331. indent: function() {
  1332. var captures = this.scanIndentation();
  1333. var tok;
  1334. if (captures) {
  1335. var indents = captures[1].length;
  1336. this.incrementLine(1);
  1337. this.consume(indents + 1);
  1338. if (' ' == this.input[0] || '\t' == this.input[0]) {
  1339. this.error(
  1340. 'INVALID_INDENTATION',
  1341. 'Invalid indentation, you can use tabs or spaces but not both'
  1342. );
  1343. }
  1344. // blank line
  1345. if ('\n' == this.input[0]) {
  1346. this.interpolationAllowed = true;
  1347. return this.tokEnd(this.tok('newline'));
  1348. }
  1349. // outdent
  1350. if (indents < this.indentStack[0]) {
  1351. var outdent_count = 0;
  1352. while (this.indentStack[0] > indents) {
  1353. if (this.indentStack[1] < indents) {
  1354. this.error(
  1355. 'INCONSISTENT_INDENTATION',
  1356. 'Inconsistent indentation. Expecting either ' +
  1357. this.indentStack[1] +
  1358. ' or ' +
  1359. this.indentStack[0] +
  1360. ' spaces/tabs.'
  1361. );
  1362. }
  1363. outdent_count++;
  1364. this.indentStack.shift();
  1365. }
  1366. while (outdent_count--) {
  1367. this.colno = 1;
  1368. tok = this.tok('outdent');
  1369. this.colno = this.indentStack[0] + 1;
  1370. this.tokens.push(this.tokEnd(tok));
  1371. }
  1372. // indent
  1373. } else if (indents && indents != this.indentStack[0]) {
  1374. tok = this.tok('indent', indents);
  1375. this.colno = 1 + indents;
  1376. this.tokens.push(this.tokEnd(tok));
  1377. this.indentStack.unshift(indents);
  1378. // newline
  1379. } else {
  1380. tok = this.tok('newline');
  1381. this.colno = 1 + Math.min(this.indentStack[0] || 0, indents);
  1382. this.tokens.push(this.tokEnd(tok));
  1383. }
  1384. this.interpolationAllowed = true;
  1385. return true;
  1386. }
  1387. },
  1388. pipelessText: function pipelessText(indents) {
  1389. while (this.callLexerFunction('blank'));
  1390. var captures = this.scanIndentation();
  1391. indents = indents || (captures && captures[1].length);
  1392. if (indents > this.indentStack[0]) {
  1393. this.tokens.push(this.tokEnd(this.tok('start-pipeless-text')));
  1394. var tokens = [];
  1395. var token_indent = [];
  1396. var isMatch;
  1397. // Index in this.input. Can't use this.consume because we might need to
  1398. // retry lexing the block.
  1399. var stringPtr = 0;
  1400. do {
  1401. // text has `\n` as a prefix
  1402. var i = this.input.substr(stringPtr + 1).indexOf('\n');
  1403. if (-1 == i) i = this.input.length - stringPtr - 1;
  1404. var str = this.input.substr(stringPtr + 1, i);
  1405. var lineCaptures = this.indentRe.exec('\n' + str);
  1406. var lineIndents = lineCaptures && lineCaptures[1].length;
  1407. isMatch = lineIndents >= indents;
  1408. token_indent.push(isMatch);
  1409. isMatch = isMatch || !str.trim();
  1410. if (isMatch) {
  1411. // consume test along with `\n` prefix if match
  1412. stringPtr += str.length + 1;
  1413. tokens.push(str.substr(indents));
  1414. } else if (lineIndents > this.indentStack[0]) {
  1415. // line is indented less than the first line but is still indented
  1416. // need to retry lexing the text block
  1417. this.tokens.pop();
  1418. return pipelessText.call(this, lineCaptures[1].length);
  1419. }
  1420. } while (this.input.length - stringPtr && isMatch);
  1421. this.consume(stringPtr);
  1422. while (this.input.length === 0 && tokens[tokens.length - 1] === '')
  1423. tokens.pop();
  1424. tokens.forEach(
  1425. function(token, i) {
  1426. var tok;
  1427. this.incrementLine(1);
  1428. if (i !== 0) tok = this.tok('newline');
  1429. if (token_indent[i]) this.incrementColumn(indents);
  1430. if (tok) this.tokens.push(this.tokEnd(tok));
  1431. this.addText('text', token);
  1432. }.bind(this)
  1433. );
  1434. this.tokens.push(this.tokEnd(this.tok('end-pipeless-text')));
  1435. return true;
  1436. }
  1437. },
  1438. /**
  1439. * Slash.
  1440. */
  1441. slash: function() {
  1442. var tok = this.scan(/^\//, 'slash');
  1443. if (tok) {
  1444. this.tokens.push(this.tokEnd(tok));
  1445. return true;
  1446. }
  1447. },
  1448. /**
  1449. * ':'
  1450. */
  1451. colon: function() {
  1452. var tok = this.scan(/^: +/, ':');
  1453. if (tok) {
  1454. this.tokens.push(this.tokEnd(tok));
  1455. return true;
  1456. }
  1457. },
  1458. fail: function() {
  1459. this.error(
  1460. 'UNEXPECTED_TEXT',
  1461. 'unexpected text "' + this.input.substr(0, 5) + '"'
  1462. );
  1463. },
  1464. callLexerFunction: function(func) {
  1465. var rest = [];
  1466. for (var i = 1; i < arguments.length; i++) {
  1467. rest.push(arguments[i]);
  1468. }
  1469. var pluginArgs = [this].concat(rest);
  1470. for (var i = 0; i < this.plugins.length; i++) {
  1471. var plugin = this.plugins[i];
  1472. if (plugin[func] && plugin[func].apply(plugin, pluginArgs)) {
  1473. return true;
  1474. }
  1475. }
  1476. return this[func].apply(this, rest);
  1477. },
  1478. /**
  1479. * Move to the next token
  1480. *
  1481. * @api private
  1482. */
  1483. advance: function() {
  1484. return (
  1485. this.callLexerFunction('blank') ||
  1486. this.callLexerFunction('eos') ||
  1487. this.callLexerFunction('endInterpolation') ||
  1488. this.callLexerFunction('yield') ||
  1489. this.callLexerFunction('doctype') ||
  1490. this.callLexerFunction('interpolation') ||
  1491. this.callLexerFunction('case') ||
  1492. this.callLexerFunction('when') ||
  1493. this.callLexerFunction('default') ||
  1494. this.callLexerFunction('extends') ||
  1495. this.callLexerFunction('append') ||
  1496. this.callLexerFunction('prepend') ||
  1497. this.callLexerFunction('block') ||
  1498. this.callLexerFunction('mixinBlock') ||
  1499. this.callLexerFunction('include') ||
  1500. this.callLexerFunction('mixin') ||
  1501. this.callLexerFunction('call') ||
  1502. this.callLexerFunction('conditional') ||
  1503. this.callLexerFunction('eachOf') ||
  1504. this.callLexerFunction('each') ||
  1505. this.callLexerFunction('while') ||
  1506. this.callLexerFunction('tag') ||
  1507. this.callLexerFunction('filter') ||
  1508. this.callLexerFunction('blockCode') ||
  1509. this.callLexerFunction('code') ||
  1510. this.callLexerFunction('id') ||
  1511. this.callLexerFunction('dot') ||
  1512. this.callLexerFunction('className') ||
  1513. this.callLexerFunction('attrs') ||
  1514. this.callLexerFunction('attributesBlock') ||
  1515. this.callLexerFunction('indent') ||
  1516. this.callLexerFunction('text') ||
  1517. this.callLexerFunction('textHtml') ||
  1518. this.callLexerFunction('comment') ||
  1519. this.callLexerFunction('slash') ||
  1520. this.callLexerFunction('colon') ||
  1521. this.fail()
  1522. );
  1523. },
  1524. /**
  1525. * Return an array of tokens for the current file
  1526. *
  1527. * @returns {Array.<Token>}
  1528. * @api public
  1529. */
  1530. getTokens: function() {
  1531. while (!this.ended) {
  1532. this.callLexerFunction('advance');
  1533. }
  1534. return this.tokens;
  1535. },
  1536. };