index.js 41 KB

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