UNPKG

11.5 kBJavaScriptView Raw
1
2/*!
3 * Jade - Lexer
4 * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
5 * MIT Licensed
6 */
7
8/**
9 * Initialize `Lexer` with the given `str`.
10 *
11 * Options:
12 *
13 * - `colons` allow colons for attr delimiters
14 *
15 * @param {String} str
16 * @param {Object} options
17 * @api private
18 */
19
20var Lexer = module.exports = function Lexer(str, options) {
21 options = options || {};
22 this.input = str.replace(/\r\n|\r/g, '\n');
23 this.colons = options.colons;
24 this.deferredTokens = [];
25 this.lastIndents = 0;
26 this.lineno = 1;
27 this.stash = [];
28 this.indentStack = [];
29 this.indentRe = null;
30 this.pipeless = false;
31};
32
33/**
34 * Lexer prototype.
35 */
36
37Lexer.prototype = {
38
39 /**
40 * Construct a token with the given `type` and `val`.
41 *
42 * @param {String} type
43 * @param {String} val
44 * @return {Object}
45 * @api private
46 */
47
48 tok: function(type, val){
49 return {
50 type: type
51 , line: this.lineno
52 , val: val
53 }
54 },
55
56 /**
57 * Consume the given `len` of input.
58 *
59 * @param {Number} len
60 * @api private
61 */
62
63 consume: function(len){
64 this.input = this.input.substr(len);
65 },
66
67 /**
68 * Scan for `type` with the given `regexp`.
69 *
70 * @param {String} type
71 * @param {RegExp} regexp
72 * @return {Object}
73 * @api private
74 */
75
76 scan: function(regexp, type){
77 var captures;
78 if (captures = regexp.exec(this.input)) {
79 this.consume(captures[0].length);
80 return this.tok(type, captures[1]);
81 }
82 },
83
84 /**
85 * Defer the given `tok`.
86 *
87 * @param {Object} tok
88 * @api private
89 */
90
91 defer: function(tok){
92 this.deferredTokens.push(tok);
93 },
94
95 /**
96 * Lookahead `n` tokens.
97 *
98 * @param {Number} n
99 * @return {Object}
100 * @api private
101 */
102
103 lookahead: function(n){
104 var fetch = n - this.stash.length;
105 while (fetch-- > 0) this.stash.push(this.next());
106 return this.stash[--n];
107 },
108
109 /**
110 * Return the indexOf `start` / `end` delimiters.
111 *
112 * @param {String} start
113 * @param {String} end
114 * @return {Number}
115 * @api private
116 */
117
118 indexOfDelimiters: function(start, end){
119 var str = this.input
120 , nstart = 0
121 , nend = 0
122 , pos = 0;
123 for (var i = 0, len = str.length; i < len; ++i) {
124 if (start == str[i]) {
125 ++nstart;
126 } else if (end == str[i]) {
127 if (++nend == nstart) {
128 pos = i;
129 break;
130 }
131 }
132 }
133 return pos;
134 },
135
136 /**
137 * Stashed token.
138 */
139
140 stashed: function() {
141 return this.stash.length
142 && this.stash.shift();
143 },
144
145 /**
146 * Deferred token.
147 */
148
149 deferred: function() {
150 return this.deferredTokens.length
151 && this.deferredTokens.shift();
152 },
153
154 /**
155 * end-of-source.
156 */
157
158 eos: function() {
159 if (this.input.length) return;
160 if (this.indentStack.length) {
161 this.indentStack.shift();
162 return this.tok('outdent');
163 } else {
164 return this.tok('eos');
165 }
166 },
167
168 /**
169 * Comment.
170 */
171
172 comment: function() {
173 var captures;
174 if (captures = /^ *\/\/(-)?([^\n]*)/.exec(this.input)) {
175 this.consume(captures[0].length);
176 var tok = this.tok('comment', captures[2]);
177 tok.buffer = '-' != captures[1];
178 return tok;
179 }
180 },
181
182 /**
183 * Tag.
184 */
185
186 tag: function() {
187 var captures;
188 if (captures = /^(\w[-:\w]*)/.exec(this.input)) {
189 this.consume(captures[0].length);
190 var tok, name = captures[1];
191 if (':' == name[name.length - 1]) {
192 name = name.slice(0, -1);
193 tok = this.tok('tag', name);
194 this.deferredTokens.push(this.tok(':'));
195 while (' ' == this.input[0]) this.input = this.input.substr(1);
196 } else {
197 tok = this.tok('tag', name);
198 }
199 return tok;
200 }
201 },
202
203 /**
204 * Filter.
205 */
206
207 filter: function() {
208 return this.scan(/^:(\w+)/, 'filter');
209 },
210
211 /**
212 * Doctype.
213 */
214
215 doctype: function() {
216 return this.scan(/^(?:!!!|doctype) *(\w+)?/, 'doctype');
217 },
218
219 /**
220 * Id.
221 */
222
223 id: function() {
224 return this.scan(/^#([\w-]+)/, 'id');
225 },
226
227 /**
228 * Class.
229 */
230
231 className: function() {
232 return this.scan(/^\.([\w-]+)/, 'class');
233 },
234
235 /**
236 * Text.
237 */
238
239 text: function() {
240 return this.scan(/^(?:\| ?)?([^\n]+)/, 'text');
241 },
242
243 /**
244 * Include.
245 */
246
247 include: function() {
248 return this.scan(/^include +([^\n]+)/, 'include');
249 },
250
251 /**
252 * Mixin.
253 */
254
255 mixin: function(){
256 var captures;
257 if (captures = /^mixin +([-\w]+)(?:\(([^\)]+)\))?/.exec(this.input)) {
258 this.consume(captures[0].length);
259 var tok = this.tok('mixin', captures[1]);
260 tok.args = captures[2];
261 return tok;
262 }
263 },
264
265 /**
266 * Each.
267 */
268
269 each: function() {
270 var captures;
271 if (captures = /^- *each *(\w+)(?: *, *(\w+))? * in *([^\n]+)/.exec(this.input)) {
272 this.consume(captures[0].length);
273 var tok = this.tok('each', captures[1]);
274 tok.key = captures[2] || 'index';
275 tok.code = captures[3];
276 return tok;
277 }
278 },
279
280 /**
281 * Code.
282 */
283
284 code: function() {
285 var captures;
286 if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) {
287 this.consume(captures[0].length);
288 var flags = captures[1];
289 captures[1] = captures[2];
290 var tok = this.tok('code', captures[1]);
291 tok.escape = flags[0] === '=';
292 tok.buffer = flags[0] === '=' || flags[1] === '=';
293 return tok;
294 }
295 },
296
297 /**
298 * Attributes.
299 */
300
301 attrs: function() {
302 if ('(' == this.input[0]) {
303 var index = this.indexOfDelimiters('(', ')')
304 , str = this.input.substr(1, index-1)
305 , tok = this.tok('attrs')
306 , len = str.length
307 , colons = this.colons
308 , states = ['key']
309 , key = ''
310 , val = ''
311 , quote
312 , c;
313
314 function state(){
315 return states[states.length - 1];
316 }
317
318 function interpolate(attr) {
319 return attr.replace(/#\{([^}]+)\}/g, function(_, expr){
320 return quote + " + (" + expr + ") + " + quote;
321 });
322 }
323
324 this.consume(index + 1);
325 tok.attrs = {};
326
327 function parse(c) {
328 var real = c;
329 // TODO: remove when people fix ":"
330 if (colons && ':' == c) c = '=';
331 switch (c) {
332 case ',':
333 case '\n':
334 switch (state()) {
335 case 'expr':
336 case 'array':
337 case 'string':
338 case 'object':
339 val += c;
340 break;
341 default:
342 states.push('key');
343 val = val.trim();
344 key = key.trim();
345 if ('' == key) return;
346 tok.attrs[key.replace(/^['"]|['"]$/g, '')] = '' == val
347 ? true
348 : interpolate(val);
349 key = val = '';
350 }
351 break;
352 case '=':
353 switch (state()) {
354 case 'key char':
355 key += real;
356 break;
357 case 'val':
358 case 'expr':
359 case 'array':
360 case 'string':
361 case 'object':
362 val += real;
363 break;
364 default:
365 states.push('val');
366 }
367 break;
368 case '(':
369 if ('val' == state()) states.push('expr');
370 val += c;
371 break;
372 case ')':
373 if ('expr' == state()) states.pop();
374 val += c;
375 break;
376 case '{':
377 if ('val' == state()) states.push('object');
378 val += c;
379 break;
380 case '}':
381 if ('object' == state()) states.pop();
382 val += c;
383 break;
384 case '[':
385 if ('val' == state()) states.push('array');
386 val += c;
387 break;
388 case ']':
389 if ('array' == state()) states.pop();
390 val += c;
391 break;
392 case '"':
393 case "'":
394 switch (state()) {
395 case 'key':
396 states.push('key char');
397 break;
398 case 'key char':
399 states.pop();
400 break;
401 case 'string':
402 if (c == quote) states.pop();
403 val += c;
404 break;
405 default:
406 states.push('string');
407 val += c;
408 quote = c;
409 }
410 break;
411 case '':
412 break;
413 default:
414 switch (state()) {
415 case 'key':
416 case 'key char':
417 key += c;
418 break;
419 default:
420 val += c;
421 }
422 }
423 }
424
425 for (var i = 0; i < len; ++i) {
426 parse(str[i]);
427 }
428
429 parse(',');
430
431 return tok;
432 }
433 },
434
435 /**
436 * Indent | Outdent | Newline.
437 */
438
439 indent: function() {
440 var captures, re;
441
442 // established regexp
443 if (this.indentRe) {
444 captures = this.indentRe.exec(this.input);
445 // determine regexp
446 } else {
447 // tabs
448 re = /^\n(\t*) */;
449 captures = re.exec(this.input);
450
451 // spaces
452 if (captures && !captures[1].length) {
453 re = /^\n( *)/;
454 captures = re.exec(this.input);
455 }
456
457 // established
458 if (captures && captures[1].length) this.indentRe = re;
459 }
460
461 if (captures) {
462 var tok
463 , indents = captures[1].length;
464
465 ++this.lineno;
466 this.consume(indents + 1);
467
468 if (' ' == this.input[0] || '\t' == this.input[0]) {
469 throw new Error('Invalid indentation, you can use tabs or spaces but not both');
470 }
471
472 // blank line
473 if ('\n' == this.input[0]) return this.tok('newline');
474
475 // outdent
476 if (this.indentStack.length && indents < this.indentStack[0]) {
477 while (this.indentStack.length && this.indentStack[0] > indents) {
478 this.stash.push(this.tok('outdent'));
479 this.indentStack.shift();
480 }
481 tok = this.stash.pop();
482 // indent
483 } else if (indents && indents != this.indentStack[0]) {
484 this.indentStack.unshift(indents);
485 tok = this.tok('indent', indents);
486 // newline
487 } else {
488 tok = this.tok('newline');
489 }
490
491 return tok;
492 }
493 },
494
495 /**
496 * Pipe-less text consumed only when
497 * pipeless is true;
498 */
499
500 pipelessText: function() {
501 if (this.pipeless) {
502 if ('\n' == this.input[0]) return;
503 var i = this.input.indexOf('\n');
504 if (-1 == i) i = this.input.length;
505 var str = this.input.substr(0, i);
506 this.consume(str.length);
507 return this.tok('text', str);
508 }
509 },
510
511 /**
512 * ':'
513 */
514
515 colon: function() {
516 return this.scan(/^: */, ':');
517 },
518
519 /**
520 * Return the next token object, or those
521 * previously stashed by lookahead.
522 *
523 * @return {Object}
524 * @api private
525 */
526
527 advance: function(){
528 return this.stashed()
529 || this.next();
530 },
531
532 /**
533 * Return the next token object.
534 *
535 * @return {Object}
536 * @api private
537 */
538
539 next: function() {
540 return this.deferred()
541 || this.eos()
542 || this.pipelessText()
543 || this.doctype()
544 || this.include()
545 || this.mixin()
546 || this.tag()
547 || this.filter()
548 || this.each()
549 || this.code()
550 || this.id()
551 || this.className()
552 || this.attrs()
553 || this.indent()
554 || this.comment()
555 || this.colon()
556 || this.text();
557 }
558};