1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | var 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 |
|
35 |
|
36 |
|
37 | Lexer.prototype = {
|
38 |
|
39 | |
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | tok: function(type, val){
|
49 | return {
|
50 | type: type
|
51 | , line: this.lineno
|
52 | , val: val
|
53 | }
|
54 | },
|
55 |
|
56 | |
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | consume: function(len){
|
64 | this.input = this.input.substr(len);
|
65 | },
|
66 |
|
67 | |
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
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 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | defer: function(tok){
|
92 | this.deferredTokens.push(tok);
|
93 | },
|
94 |
|
95 | |
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
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 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
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 |
|
138 |
|
139 |
|
140 | stashed: function() {
|
141 | return this.stash.length
|
142 | && this.stash.shift();
|
143 | },
|
144 |
|
145 | |
146 |
|
147 |
|
148 |
|
149 | deferred: function() {
|
150 | return this.deferredTokens.length
|
151 | && this.deferredTokens.shift();
|
152 | },
|
153 |
|
154 | |
155 |
|
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 |
|
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 |
|
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 |
|
205 |
|
206 |
|
207 | filter: function() {
|
208 | return this.scan(/^:(\w+)/, 'filter');
|
209 | },
|
210 |
|
211 | |
212 |
|
213 |
|
214 |
|
215 | doctype: function() {
|
216 | return this.scan(/^(?:!!!|doctype) *(\w+)?/, 'doctype');
|
217 | },
|
218 |
|
219 | |
220 |
|
221 |
|
222 |
|
223 | id: function() {
|
224 | return this.scan(/^#([\w-]+)/, 'id');
|
225 | },
|
226 |
|
227 | |
228 |
|
229 |
|
230 |
|
231 | className: function() {
|
232 | return this.scan(/^\.([\w-]+)/, 'class');
|
233 | },
|
234 |
|
235 | |
236 |
|
237 |
|
238 |
|
239 | text: function() {
|
240 | return this.scan(/^(?:\| ?)?([^\n]+)/, 'text');
|
241 | },
|
242 |
|
243 | |
244 |
|
245 |
|
246 |
|
247 | include: function() {
|
248 | return this.scan(/^include +([^\n]+)/, 'include');
|
249 | },
|
250 |
|
251 | |
252 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
437 |
|
438 |
|
439 | indent: function() {
|
440 | var captures, re;
|
441 |
|
442 |
|
443 | if (this.indentRe) {
|
444 | captures = this.indentRe.exec(this.input);
|
445 |
|
446 | } else {
|
447 |
|
448 | re = /^\n(\t*) */;
|
449 | captures = re.exec(this.input);
|
450 |
|
451 |
|
452 | if (captures && !captures[1].length) {
|
453 | re = /^\n( *)/;
|
454 | captures = re.exec(this.input);
|
455 | }
|
456 |
|
457 |
|
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 |
|
473 | if ('\n' == this.input[0]) return this.tok('newline');
|
474 |
|
475 |
|
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 |
|
483 | } else if (indents && indents != this.indentStack[0]) {
|
484 | this.indentStack.unshift(indents);
|
485 | tok = this.tok('indent', indents);
|
486 |
|
487 | } else {
|
488 | tok = this.tok('newline');
|
489 | }
|
490 |
|
491 | return tok;
|
492 | }
|
493 | },
|
494 |
|
495 | |
496 |
|
497 |
|
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 |
|
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 |
|
527 | advance: function(){
|
528 | return this.stashed()
|
529 | || this.next();
|
530 | },
|
531 |
|
532 | |
533 |
|
534 |
|
535 |
|
536 |
|
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 | };
|