index.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516
  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. if((!characterParser.isPunctuator(str[x]) || quoteRe.test(str[x]) || str[x] === ':') && this.assertExpression(val, true)){
  1082. done = true;
  1083. }
  1084. break;
  1085. }
  1086. }
  1087. // if everything else is whitespace, return now so last attribute
  1088. // does not include trailing whitespace
  1089. if(done || x === str.length){
  1090. break;
  1091. }
  1092. }
  1093. // if there's no whitespace and the character is not ',', the
  1094. // attribute did not end.
  1095. if(str[i] === ',' && this.assertExpression(val, true)){
  1096. break;
  1097. }
  1098. }
  1099. state = characterParser.parseChar(str[i], state);
  1100. val += str[i];
  1101. if (str[i] === '\n') {
  1102. line++;
  1103. col = 1;
  1104. } else {
  1105. col++;
  1106. }
  1107. }
  1108. this.assertExpression(val);
  1109. this.lineno = line;
  1110. this.colno = col;
  1111. return { val: val, mustEscape: escapeAttr, remainingSource: str.substr(i) };
  1112. },
  1113. /**
  1114. * Attributes.
  1115. */
  1116. attrs: function() {
  1117. var tok;
  1118. if ('(' == this.input.charAt(0)) {
  1119. tok = this.tok('start-attributes');
  1120. var index = this.bracketExpression().end;
  1121. var str = this.input.substr(1, index-1);
  1122. this.incrementColumn(1);
  1123. this.tokens.push(this.tokEnd(tok));
  1124. this.assertNestingCorrect(str);
  1125. this.consume(index + 1);
  1126. while(str){
  1127. str = this.attribute(str);
  1128. }
  1129. tok = this.tok('end-attributes');
  1130. this.incrementColumn(1);
  1131. this.tokens.push(this.tokEnd(tok));
  1132. return true;
  1133. }
  1134. },
  1135. /**
  1136. * &attributes block
  1137. */
  1138. attributesBlock: function () {
  1139. if (/^&attributes\b/.test(this.input)) {
  1140. var consumed = 11;
  1141. this.consume(consumed);
  1142. var tok = this.tok('&attributes');
  1143. this.incrementColumn(consumed);
  1144. var args = this.bracketExpression();
  1145. consumed = args.end + 1;
  1146. this.consume(consumed);
  1147. tok.val = args.src;
  1148. this.incrementColumn(consumed);
  1149. this.tokens.push(this.tokEnd(tok));
  1150. return true;
  1151. }
  1152. },
  1153. /**
  1154. * Indent | Outdent | Newline.
  1155. */
  1156. indent: function() {
  1157. var captures = this.scanIndentation();
  1158. var tok;
  1159. if (captures) {
  1160. var indents = captures[1].length;
  1161. this.incrementLine(1);
  1162. this.consume(indents + 1);
  1163. if (' ' == this.input[0] || '\t' == this.input[0]) {
  1164. this.error('INVALID_INDENTATION', 'Invalid indentation, you can use tabs or spaces but not both');
  1165. }
  1166. // blank line
  1167. if ('\n' == this.input[0]) {
  1168. this.interpolationAllowed = true;
  1169. return this.tokEnd(this.tok('newline'));
  1170. }
  1171. // outdent
  1172. if (indents < this.indentStack[0]) {
  1173. var outdent_count = 0;
  1174. while (this.indentStack[0] > indents) {
  1175. if (this.indentStack[1] < indents) {
  1176. this.error('INCONSISTENT_INDENTATION', 'Inconsistent indentation. Expecting either ' + this.indentStack[1] + ' or ' + this.indentStack[0] + ' spaces/tabs.');
  1177. }
  1178. outdent_count++;
  1179. this.indentStack.shift();
  1180. }
  1181. while(outdent_count--){
  1182. this.colno = 1;
  1183. tok = this.tok('outdent');
  1184. this.colno = this.indentStack[0] + 1;
  1185. this.tokens.push(this.tokEnd(tok));
  1186. }
  1187. // indent
  1188. } else if (indents && indents != this.indentStack[0]) {
  1189. tok = this.tok('indent', indents);
  1190. this.colno = 1 + indents;
  1191. this.tokens.push(this.tokEnd(tok));
  1192. this.indentStack.unshift(indents);
  1193. // newline
  1194. } else {
  1195. tok = this.tok('newline');
  1196. this.colno = 1 + Math.min(this.indentStack[0] || 0, indents);
  1197. this.tokens.push(this.tokEnd(tok));
  1198. }
  1199. this.interpolationAllowed = true;
  1200. return true;
  1201. }
  1202. },
  1203. pipelessText: function pipelessText(indents) {
  1204. while (this.callLexerFunction('blank'));
  1205. var captures = this.scanIndentation();
  1206. indents = indents || captures && captures[1].length;
  1207. if (indents > this.indentStack[0]) {
  1208. this.tokens.push(this.tokEnd(this.tok('start-pipeless-text')));
  1209. var tokens = [];
  1210. var token_indent = [];
  1211. var isMatch;
  1212. // Index in this.input. Can't use this.consume because we might need to
  1213. // retry lexing the block.
  1214. var stringPtr = 0;
  1215. do {
  1216. // text has `\n` as a prefix
  1217. var i = this.input.substr(stringPtr + 1).indexOf('\n');
  1218. if (-1 == i) i = this.input.length - stringPtr - 1;
  1219. var str = this.input.substr(stringPtr + 1, i);
  1220. var lineCaptures = this.indentRe.exec('\n' + str);
  1221. var lineIndents = lineCaptures && lineCaptures[1].length;
  1222. isMatch = lineIndents >= indents;
  1223. token_indent.push(isMatch);
  1224. isMatch = isMatch || !str.trim();
  1225. if (isMatch) {
  1226. // consume test along with `\n` prefix if match
  1227. stringPtr += str.length + 1;
  1228. tokens.push(str.substr(indents));
  1229. } else if (lineIndents > this.indentStack[0]) {
  1230. // line is indented less than the first line but is still indented
  1231. // need to retry lexing the text block
  1232. this.tokens.pop();
  1233. return pipelessText.call(this, lineCaptures[1].length);
  1234. }
  1235. } while((this.input.length - stringPtr) && isMatch);
  1236. this.consume(stringPtr);
  1237. while (this.input.length === 0 && tokens[tokens.length - 1] === '') tokens.pop();
  1238. tokens.forEach(function (token, i) {
  1239. var tok;
  1240. this.incrementLine(1);
  1241. if (i !== 0) tok = this.tok('newline');
  1242. if (token_indent[i]) this.incrementColumn(indents);
  1243. if (tok) this.tokens.push(this.tokEnd(tok));
  1244. this.addText('text', token);
  1245. }.bind(this));
  1246. this.tokens.push(this.tokEnd(this.tok('end-pipeless-text')));
  1247. return true;
  1248. }
  1249. },
  1250. /**
  1251. * Slash.
  1252. */
  1253. slash: function() {
  1254. var tok = this.scan(/^\//, 'slash');
  1255. if (tok) {
  1256. this.tokens.push(this.tokEnd(tok));
  1257. return true;
  1258. }
  1259. },
  1260. /**
  1261. * ':'
  1262. */
  1263. colon: function() {
  1264. var tok = this.scan(/^: +/, ':');
  1265. if (tok) {
  1266. this.tokens.push(this.tokEnd(tok));
  1267. return true;
  1268. }
  1269. },
  1270. fail: function () {
  1271. this.error('UNEXPECTED_TEXT', 'unexpected text "' + this.input.substr(0, 5) + '"');
  1272. },
  1273. callLexerFunction: function (func) {
  1274. var rest = [];
  1275. for (var i = 1; i < arguments.length; i++) {
  1276. rest.push(arguments[i]);
  1277. }
  1278. var pluginArgs = [this].concat(rest);
  1279. for (var i = 0; i < this.plugins.length; i++) {
  1280. var plugin = this.plugins[i];
  1281. if (plugin[func] && plugin[func].apply(plugin, pluginArgs)) {
  1282. return true;
  1283. }
  1284. }
  1285. return this[func].apply(this, rest);
  1286. },
  1287. /**
  1288. * Move to the next token
  1289. *
  1290. * @api private
  1291. */
  1292. advance: function() {
  1293. return this.callLexerFunction('blank')
  1294. || this.callLexerFunction('eos')
  1295. || this.callLexerFunction('endInterpolation')
  1296. || this.callLexerFunction('yield')
  1297. || this.callLexerFunction('doctype')
  1298. || this.callLexerFunction('interpolation')
  1299. || this.callLexerFunction('case')
  1300. || this.callLexerFunction('when')
  1301. || this.callLexerFunction('default')
  1302. || this.callLexerFunction('extends')
  1303. || this.callLexerFunction('append')
  1304. || this.callLexerFunction('prepend')
  1305. || this.callLexerFunction('block')
  1306. || this.callLexerFunction('mixinBlock')
  1307. || this.callLexerFunction('include')
  1308. || this.callLexerFunction('mixin')
  1309. || this.callLexerFunction('call')
  1310. || this.callLexerFunction('conditional')
  1311. || this.callLexerFunction('each')
  1312. || this.callLexerFunction('while')
  1313. || this.callLexerFunction('tag')
  1314. || this.callLexerFunction('filter')
  1315. || this.callLexerFunction('blockCode')
  1316. || this.callLexerFunction('code')
  1317. || this.callLexerFunction('id')
  1318. || this.callLexerFunction('dot')
  1319. || this.callLexerFunction('className')
  1320. || this.callLexerFunction('attrs')
  1321. || this.callLexerFunction('attributesBlock')
  1322. || this.callLexerFunction('indent')
  1323. || this.callLexerFunction('text')
  1324. || this.callLexerFunction('textHtml')
  1325. || this.callLexerFunction('comment')
  1326. || this.callLexerFunction('slash')
  1327. || this.callLexerFunction('colon')
  1328. || this.fail();
  1329. },
  1330. /**
  1331. * Return an array of tokens for the current file
  1332. *
  1333. * @returns {Array.<Token>}
  1334. * @api public
  1335. */
  1336. getTokens: function () {
  1337. while (!this.ended) {
  1338. this.callLexerFunction('advance');
  1339. }
  1340. return this.tokens;
  1341. }
  1342. };