1 | 'use strict';
|
2 |
|
3 | var utils = require('./utils');
|
4 | var characterParser = require('character-parser');
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | var Lexer = module.exports = function Lexer(str, filename) {
|
16 | this.input = str.replace(/\r\n|\r/g, '\n');
|
17 | this.filename = filename;
|
18 | this.deferredTokens = [];
|
19 | this.lastIndents = 0;
|
20 | this.lineno = 1;
|
21 | this.stash = [];
|
22 | this.indentStack = [];
|
23 | this.indentRe = null;
|
24 | this.pipeless = false;
|
25 | };
|
26 |
|
27 |
|
28 | function assertExpression(exp) {
|
29 |
|
30 | Function('', 'return (' + exp + ')');
|
31 | }
|
32 | function assertNestingCorrect(exp) {
|
33 |
|
34 |
|
35 | var res = characterParser(exp)
|
36 | if (res.isNesting()) {
|
37 | throw new Error('Nesting must match on expression `' + exp + '`')
|
38 | }
|
39 | }
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 | Lexer.prototype = {
|
46 |
|
47 | |
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | tok: function(type, val){
|
57 | return {
|
58 | type: type
|
59 | , line: this.lineno
|
60 | , val: val
|
61 | }
|
62 | },
|
63 |
|
64 | |
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | consume: function(len){
|
72 | this.input = this.input.substr(len);
|
73 | },
|
74 |
|
75 | |
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 | scan: function(regexp, type){
|
85 | var captures;
|
86 | if (captures = regexp.exec(this.input)) {
|
87 | this.consume(captures[0].length);
|
88 | return this.tok(type, captures[1]);
|
89 | }
|
90 | },
|
91 |
|
92 | |
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 | defer: function(tok){
|
100 | this.deferredTokens.push(tok);
|
101 | },
|
102 |
|
103 | |
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 | lookahead: function(n){
|
112 | var fetch = n - this.stash.length;
|
113 | while (fetch-- > 0) this.stash.push(this.next());
|
114 | return this.stash[--n];
|
115 | },
|
116 |
|
117 | |
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 | bracketExpression: function(skip){
|
125 | skip = skip || 0;
|
126 | var start = this.input[skip];
|
127 | if (start != '(' && start != '{' && start != '[') throw new Error('unrecognized start character');
|
128 | var end = ({'(': ')', '{': '}', '[': ']'})[start];
|
129 | var range = characterParser.parseMax(this.input, {start: skip + 1});
|
130 | if (this.input[range.end] !== end) throw new Error('start character ' + start + ' does not match end character ' + this.input[range.end]);
|
131 | return range;
|
132 | },
|
133 |
|
134 | |
135 |
|
136 |
|
137 |
|
138 | stashed: function() {
|
139 | return this.stash.length
|
140 | && this.stash.shift();
|
141 | },
|
142 |
|
143 | |
144 |
|
145 |
|
146 |
|
147 | deferred: function() {
|
148 | return this.deferredTokens.length
|
149 | && this.deferredTokens.shift();
|
150 | },
|
151 |
|
152 | |
153 |
|
154 |
|
155 |
|
156 | eos: function() {
|
157 | if (this.input.length) return;
|
158 | if (this.indentStack.length) {
|
159 | this.indentStack.shift();
|
160 | return this.tok('outdent');
|
161 | } else {
|
162 | return this.tok('eos');
|
163 | }
|
164 | },
|
165 |
|
166 | |
167 |
|
168 |
|
169 |
|
170 | blank: function() {
|
171 | var captures;
|
172 | if (captures = /^\n *\n/.exec(this.input)) {
|
173 | this.consume(captures[0].length - 1);
|
174 | ++this.lineno;
|
175 | if (this.pipeless) return this.tok('text', '');
|
176 | return this.next();
|
177 | }
|
178 | },
|
179 |
|
180 | |
181 |
|
182 |
|
183 |
|
184 | comment: function() {
|
185 | var captures;
|
186 | if (captures = /^\/\/(-)?([^\n]*)/.exec(this.input)) {
|
187 | this.consume(captures[0].length);
|
188 | var tok = this.tok('comment', captures[2]);
|
189 | tok.buffer = '-' != captures[1];
|
190 | this.pipeless = true;
|
191 | return tok;
|
192 | }
|
193 | },
|
194 |
|
195 | |
196 |
|
197 |
|
198 |
|
199 | interpolation: function() {
|
200 | if (/^#\{/.test(this.input)) {
|
201 | var match = this.bracketExpression(1);
|
202 |
|
203 | this.consume(match.end + 1);
|
204 | return this.tok('interpolation', match.src);
|
205 | }
|
206 | },
|
207 |
|
208 | |
209 |
|
210 |
|
211 |
|
212 | tag: function() {
|
213 | var captures;
|
214 | if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) {
|
215 | this.consume(captures[0].length);
|
216 | var tok, name = captures[1];
|
217 | if (':' == name[name.length - 1]) {
|
218 | name = name.slice(0, -1);
|
219 | tok = this.tok('tag', name);
|
220 | this.defer(this.tok(':'));
|
221 | if (this.input[0] !== ' ') {
|
222 | console.warn('Warning: space required after `:` on line ' + this.lineno +
|
223 | ' of jade file "' + this.filename + '"');
|
224 | }
|
225 | while (' ' == this.input[0]) this.input = this.input.substr(1);
|
226 | } else {
|
227 | tok = this.tok('tag', name);
|
228 | }
|
229 | tok.selfClosing = !!captures[2];
|
230 | return tok;
|
231 | }
|
232 | },
|
233 |
|
234 | |
235 |
|
236 |
|
237 |
|
238 | filter: function() {
|
239 | var tok = this.scan(/^:([\w\-]+)/, 'filter');
|
240 | if (tok) {
|
241 | this.pipeless = true;
|
242 | return tok;
|
243 | }
|
244 | },
|
245 |
|
246 | |
247 |
|
248 |
|
249 |
|
250 | doctype: function() {
|
251 | if (this.scan(/^!!! *([^\n]+)?/, 'doctype')) {
|
252 | throw new Error('`!!!` is deprecated, you must now use `doctype`');
|
253 | }
|
254 | var node = this.scan(/^(?:doctype) *([^\n]+)?/, 'doctype');
|
255 | if (node && node.val && node.val.trim() === '5') {
|
256 | throw new Error('`doctype 5` is deprecated, you must now use `doctype html`');
|
257 | }
|
258 | return node;
|
259 | },
|
260 |
|
261 | |
262 |
|
263 |
|
264 |
|
265 | id: function() {
|
266 | return this.scan(/^#([\w-]+)/, 'id');
|
267 | },
|
268 |
|
269 | |
270 |
|
271 |
|
272 |
|
273 | className: function() {
|
274 | return this.scan(/^\.([\w-]+)/, 'class');
|
275 | },
|
276 |
|
277 | |
278 |
|
279 |
|
280 |
|
281 | text: function() {
|
282 | return this.scan(/^(?:\| ?| )([^\n]+)/, 'text') ||
|
283 | this.scan(/^\|?( )/, 'text') ||
|
284 | this.scan(/^(<[^\n]*)/, 'text');
|
285 | },
|
286 |
|
287 | textFail: function () {
|
288 | var tok;
|
289 | if (tok = this.scan(/^([^\.\n][^\n]+)/, 'text')) {
|
290 | console.warn('Warning: missing space before text for line ' + this.lineno +
|
291 | ' of jade file "' + this.filename + '"');
|
292 | return tok;
|
293 | }
|
294 | },
|
295 |
|
296 | |
297 |
|
298 |
|
299 |
|
300 | dot: function() {
|
301 | var match;
|
302 | if (match = this.scan(/^\./, 'dot')) {
|
303 | this.pipeless = true;
|
304 | return match;
|
305 | }
|
306 | },
|
307 |
|
308 | |
309 |
|
310 |
|
311 |
|
312 | "extends": function() {
|
313 | return this.scan(/^extends? +([^\n]+)/, 'extends');
|
314 | },
|
315 |
|
316 | |
317 |
|
318 |
|
319 |
|
320 | prepend: function() {
|
321 | var captures;
|
322 | if (captures = /^prepend +([^\n]+)/.exec(this.input)) {
|
323 | this.consume(captures[0].length);
|
324 | var mode = 'prepend'
|
325 | , name = captures[1]
|
326 | , tok = this.tok('block', name);
|
327 | tok.mode = mode;
|
328 | return tok;
|
329 | }
|
330 | },
|
331 |
|
332 | |
333 |
|
334 |
|
335 |
|
336 | append: function() {
|
337 | var captures;
|
338 | if (captures = /^append +([^\n]+)/.exec(this.input)) {
|
339 | this.consume(captures[0].length);
|
340 | var mode = 'append'
|
341 | , name = captures[1]
|
342 | , tok = this.tok('block', name);
|
343 | tok.mode = mode;
|
344 | return tok;
|
345 | }
|
346 | },
|
347 |
|
348 | |
349 |
|
350 |
|
351 |
|
352 | block: function() {
|
353 | var captures;
|
354 | if (captures = /^block\b *(?:(prepend|append) +)?([^\n]+)/.exec(this.input)) {
|
355 | this.consume(captures[0].length);
|
356 | var mode = captures[1] || 'replace'
|
357 | , name = captures[2]
|
358 | , tok = this.tok('block', name);
|
359 |
|
360 | tok.mode = mode;
|
361 | return tok;
|
362 | }
|
363 | },
|
364 |
|
365 | |
366 |
|
367 |
|
368 |
|
369 | mixinBlock: function() {
|
370 | var captures;
|
371 | if (captures = /^block[ \t]*(\n|$)/.exec(this.input)) {
|
372 | this.consume(captures[0].length - captures[1].length);
|
373 | return this.tok('mixin-block');
|
374 | }
|
375 | },
|
376 |
|
377 | |
378 |
|
379 |
|
380 |
|
381 | 'yield': function() {
|
382 | return this.scan(/^yield */, 'yield');
|
383 | },
|
384 |
|
385 | |
386 |
|
387 |
|
388 |
|
389 | include: function() {
|
390 | return this.scan(/^include +([^\n]+)/, 'include');
|
391 | },
|
392 |
|
393 | |
394 |
|
395 |
|
396 |
|
397 | includeFiltered: function() {
|
398 | var captures;
|
399 | if (captures = /^include:([\w\-]+)([\( ])/.exec(this.input)) {
|
400 | this.consume(captures[0].length - 1);
|
401 | var filter = captures[1];
|
402 | var attrs = captures[2] === '(' ? this.attrs() : null;
|
403 | if (!(captures[2] === ' ' || this.input[0] === ' ')) {
|
404 | throw new Error('expected space after include:filter but got ' + utils.stringify(this.input[0]));
|
405 | }
|
406 | captures = /^ *([^\n]+)/.exec(this.input);
|
407 | if (!captures || captures[1].trim() === '') {
|
408 | throw new Error('missing path for include:filter');
|
409 | }
|
410 | this.consume(captures[0].length);
|
411 | var path = captures[1];
|
412 | var tok = this.tok('include', path);
|
413 | tok.filter = filter;
|
414 | tok.attrs = attrs;
|
415 | return tok;
|
416 | }
|
417 | },
|
418 |
|
419 | |
420 |
|
421 |
|
422 |
|
423 | "case": function() {
|
424 | return this.scan(/^case +([^\n]+)/, 'case');
|
425 | },
|
426 |
|
427 | |
428 |
|
429 |
|
430 |
|
431 | when: function() {
|
432 | return this.scan(/^when +([^:\n]+)/, 'when');
|
433 | },
|
434 |
|
435 | |
436 |
|
437 |
|
438 |
|
439 | "default": function() {
|
440 | return this.scan(/^default */, 'default');
|
441 | },
|
442 |
|
443 | |
444 |
|
445 |
|
446 |
|
447 | call: function(){
|
448 |
|
449 | var tok, captures;
|
450 | if (captures = /^\+(\s*)(([-\w]+)|(#\{))/.exec(this.input)) {
|
451 |
|
452 | if (captures[3]) {
|
453 |
|
454 | this.consume(captures[0].length);
|
455 | tok = this.tok('call', captures[3]);
|
456 | } else {
|
457 |
|
458 | var match = this.bracketExpression(2 + captures[1].length);
|
459 | this.consume(match.end + 1);
|
460 | assertExpression(match.src);
|
461 | tok = this.tok('call', '#{'+match.src+'}');
|
462 | }
|
463 |
|
464 |
|
465 | if (captures = /^ *\(/.exec(this.input)) {
|
466 | var range = this.bracketExpression(captures[0].length - 1);
|
467 | if (!/^\s*[-\w]+ *=/.test(range.src)) {
|
468 | this.consume(range.end + 1);
|
469 | tok.args = range.src;
|
470 | }
|
471 | if (tok.args) {
|
472 | assertExpression('[' + tok.args + ']');
|
473 | }
|
474 | }
|
475 |
|
476 | return tok;
|
477 | }
|
478 | },
|
479 |
|
480 | |
481 |
|
482 |
|
483 |
|
484 | mixin: function(){
|
485 | var captures;
|
486 | if (captures = /^mixin +([-\w]+)(?: *\((.*)\))? */.exec(this.input)) {
|
487 | this.consume(captures[0].length);
|
488 | var tok = this.tok('mixin', captures[1]);
|
489 | tok.args = captures[2];
|
490 | return tok;
|
491 | }
|
492 | },
|
493 |
|
494 | |
495 |
|
496 |
|
497 |
|
498 | conditional: function() {
|
499 | var captures;
|
500 | if (captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input)) {
|
501 | this.consume(captures[0].length);
|
502 | var type = captures[1]
|
503 | var js = captures[2];
|
504 | var isIf = false;
|
505 | var isElse = false;
|
506 |
|
507 | switch (type) {
|
508 | case 'if':
|
509 | assertExpression(js)
|
510 | js = 'if (' + js + ')';
|
511 | isIf = true;
|
512 | break;
|
513 | case 'unless':
|
514 | assertExpression(js)
|
515 | js = 'if (!(' + js + '))';
|
516 | isIf = true;
|
517 | break;
|
518 | case 'else if':
|
519 | assertExpression(js)
|
520 | js = 'else if (' + js + ')';
|
521 | isIf = true;
|
522 | isElse = true;
|
523 | break;
|
524 | case 'else':
|
525 | if (js && js.trim()) {
|
526 | throw new Error('`else` cannot have a condition, perhaps you meant `else if`');
|
527 | }
|
528 | js = 'else';
|
529 | isElse = true;
|
530 | break;
|
531 | }
|
532 | var tok = this.tok('code', js);
|
533 | tok.isElse = isElse;
|
534 | tok.isIf = isIf;
|
535 | tok.requiresBlock = true;
|
536 | return tok;
|
537 | }
|
538 | },
|
539 |
|
540 | |
541 |
|
542 |
|
543 |
|
544 | "while": function() {
|
545 | var captures;
|
546 | if (captures = /^while +([^\n]+)/.exec(this.input)) {
|
547 | this.consume(captures[0].length);
|
548 | assertExpression(captures[1])
|
549 | var tok = this.tok('code', 'while (' + captures[1] + ')');
|
550 | tok.requiresBlock = true;
|
551 | return tok;
|
552 | }
|
553 | },
|
554 |
|
555 | |
556 |
|
557 |
|
558 |
|
559 | each: function() {
|
560 | var captures;
|
561 | if (captures = /^(?:- *)?(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? * in *([^\n]+)/.exec(this.input)) {
|
562 | this.consume(captures[0].length);
|
563 | var tok = this.tok('each', captures[1]);
|
564 | tok.key = captures[2] || '$index';
|
565 | assertExpression(captures[3])
|
566 | tok.code = captures[3];
|
567 | return tok;
|
568 | }
|
569 | },
|
570 |
|
571 | |
572 |
|
573 |
|
574 |
|
575 | code: function() {
|
576 | var captures;
|
577 | if (captures = /^(!?=|-)[ \t]*([^\n]+)/.exec(this.input)) {
|
578 | this.consume(captures[0].length);
|
579 | var flags = captures[1];
|
580 | captures[1] = captures[2];
|
581 | var tok = this.tok('code', captures[1]);
|
582 | tok.escape = flags.charAt(0) === '=';
|
583 | tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '=';
|
584 | if (tok.buffer) assertExpression(captures[1])
|
585 | return tok;
|
586 | }
|
587 | },
|
588 |
|
589 |
|
590 | |
591 |
|
592 |
|
593 |
|
594 | blockCode: function() {
|
595 | var captures;
|
596 | if (captures = /^-\n/.exec(this.input)) {
|
597 | this.consume(captures[0].length - 1);
|
598 | var tok = this.tok('blockCode');
|
599 | this.pipeless = true;
|
600 | return tok;
|
601 | }
|
602 | },
|
603 |
|
604 | |
605 |
|
606 |
|
607 |
|
608 | attrs: function() {
|
609 | if ('(' == this.input.charAt(0)) {
|
610 | var index = this.bracketExpression().end
|
611 | , str = this.input.substr(1, index-1)
|
612 | , tok = this.tok('attrs');
|
613 |
|
614 | assertNestingCorrect(str);
|
615 |
|
616 | var quote = '';
|
617 | var interpolate = function (attr) {
|
618 | return attr.replace(/(\\)?#\{(.+)/g, function(_, escape, expr){
|
619 | if (escape) return _;
|
620 | try {
|
621 | var range = characterParser.parseMax(expr);
|
622 | if (expr[range.end] !== '}') return _.substr(0, 2) + interpolate(_.substr(2));
|
623 | assertExpression(range.src)
|
624 | return quote + " + (" + range.src + ") + " + quote + interpolate(expr.substr(range.end + 1));
|
625 | } catch (ex) {
|
626 | return _.substr(0, 2) + interpolate(_.substr(2));
|
627 | }
|
628 | });
|
629 | }
|
630 |
|
631 | this.consume(index + 1);
|
632 | tok.attrs = [];
|
633 |
|
634 | var escapedAttr = true
|
635 | var key = '';
|
636 | var val = '';
|
637 | var interpolatable = '';
|
638 | var state = characterParser.defaultState();
|
639 | var loc = 'key';
|
640 | var isEndOfAttribute = function (i) {
|
641 | if (key.trim() === '') return false;
|
642 | if (i === str.length) return true;
|
643 | if (loc === 'key') {
|
644 | if (str[i] === ' ' || str[i] === '\n') {
|
645 | for (var x = i; x < str.length; x++) {
|
646 | if (str[x] != ' ' && str[x] != '\n') {
|
647 | if (str[x] === '=' || str[x] === '!' || str[x] === ',') return false;
|
648 | else return true;
|
649 | }
|
650 | }
|
651 | }
|
652 | return str[i] === ','
|
653 | } else if (loc === 'value' && !state.isNesting()) {
|
654 | try {
|
655 | assertExpression(val);
|
656 | if (str[i] === ' ' || str[i] === '\n') {
|
657 | for (var x = i; x < str.length; x++) {
|
658 | if (str[x] != ' ' && str[x] != '\n') {
|
659 | if (characterParser.isPunctuator(str[x]) && str[x] != '"' && str[x] != "'") return false;
|
660 | else return true;
|
661 | }
|
662 | }
|
663 | }
|
664 | return str[i] === ',';
|
665 | } catch (ex) {
|
666 | return false;
|
667 | }
|
668 | }
|
669 | }
|
670 |
|
671 | this.lineno += str.split("\n").length - 1;
|
672 |
|
673 | for (var i = 0; i <= str.length; i++) {
|
674 | if (isEndOfAttribute(i)) {
|
675 | val = val.trim();
|
676 | if (val) assertExpression(val)
|
677 | key = key.trim();
|
678 | key = key.replace(/^['"]|['"]$/g, '');
|
679 | tok.attrs.push({
|
680 | name: key,
|
681 | val: '' == val ? true : val,
|
682 | escaped: escapedAttr
|
683 | });
|
684 | key = val = '';
|
685 | loc = 'key';
|
686 | escapedAttr = false;
|
687 | } else {
|
688 | switch (loc) {
|
689 | case 'key-char':
|
690 | if (str[i] === quote) {
|
691 | loc = 'key';
|
692 | if (i + 1 < str.length && [' ', ',', '!', '=', '\n'].indexOf(str[i + 1]) === -1)
|
693 | throw new Error('Unexpected character ' + str[i + 1] + ' expected ` `, `\\n`, `,`, `!` or `=`');
|
694 | } else {
|
695 | key += str[i];
|
696 | }
|
697 | break;
|
698 | case 'key':
|
699 | if (key === '' && (str[i] === '"' || str[i] === "'")) {
|
700 | loc = 'key-char';
|
701 | quote = str[i];
|
702 | } else if (str[i] === '!' || str[i] === '=') {
|
703 | escapedAttr = str[i] !== '!';
|
704 | if (str[i] === '!') i++;
|
705 | if (str[i] !== '=') throw new Error('Unexpected character ' + str[i] + ' expected `=`');
|
706 | loc = 'value';
|
707 | state = characterParser.defaultState();
|
708 | } else {
|
709 | key += str[i]
|
710 | }
|
711 | break;
|
712 | case 'value':
|
713 | state = characterParser.parseChar(str[i], state);
|
714 | if (state.isString()) {
|
715 | loc = 'string';
|
716 | quote = str[i];
|
717 | interpolatable = str[i];
|
718 | } else {
|
719 | val += str[i];
|
720 | }
|
721 | break;
|
722 | case 'string':
|
723 | state = characterParser.parseChar(str[i], state);
|
724 | interpolatable += str[i];
|
725 | if (!state.isString()) {
|
726 | loc = 'value';
|
727 | val += interpolate(interpolatable);
|
728 | }
|
729 | break;
|
730 | }
|
731 | }
|
732 | }
|
733 |
|
734 | if ('/' == this.input.charAt(0)) {
|
735 | this.consume(1);
|
736 | tok.selfClosing = true;
|
737 | }
|
738 |
|
739 | return tok;
|
740 | }
|
741 | },
|
742 |
|
743 | |
744 |
|
745 |
|
746 | attributesBlock: function () {
|
747 | var captures;
|
748 | if (/^&attributes\b/.test(this.input)) {
|
749 | this.consume(11);
|
750 | var args = this.bracketExpression();
|
751 | this.consume(args.end + 1);
|
752 | return this.tok('&attributes', args.src);
|
753 | }
|
754 | },
|
755 |
|
756 | |
757 |
|
758 |
|
759 |
|
760 | indent: function() {
|
761 | var captures, re;
|
762 |
|
763 |
|
764 | if (this.indentRe) {
|
765 | captures = this.indentRe.exec(this.input);
|
766 |
|
767 | } else {
|
768 |
|
769 | re = /^\n(\t*) */;
|
770 | captures = re.exec(this.input);
|
771 |
|
772 |
|
773 | if (captures && !captures[1].length) {
|
774 | re = /^\n( *)/;
|
775 | captures = re.exec(this.input);
|
776 | }
|
777 |
|
778 |
|
779 | if (captures && captures[1].length) this.indentRe = re;
|
780 | }
|
781 |
|
782 | if (captures) {
|
783 | var tok
|
784 | , indents = captures[1].length;
|
785 |
|
786 | ++this.lineno;
|
787 | this.consume(indents + 1);
|
788 |
|
789 | if (' ' == this.input[0] || '\t' == this.input[0]) {
|
790 | throw new Error('Invalid indentation, you can use tabs or spaces but not both');
|
791 | }
|
792 |
|
793 |
|
794 | if ('\n' == this.input[0]) {
|
795 | this.pipeless = false;
|
796 | return this.tok('newline');
|
797 | }
|
798 |
|
799 |
|
800 | if (this.indentStack.length && indents < this.indentStack[0]) {
|
801 | while (this.indentStack.length && this.indentStack[0] > indents) {
|
802 | this.stash.push(this.tok('outdent'));
|
803 | this.indentStack.shift();
|
804 | }
|
805 | tok = this.stash.pop();
|
806 |
|
807 | } else if (indents && indents != this.indentStack[0]) {
|
808 | this.indentStack.unshift(indents);
|
809 | tok = this.tok('indent', indents);
|
810 |
|
811 | } else {
|
812 | tok = this.tok('newline');
|
813 | }
|
814 |
|
815 | this.pipeless = false;
|
816 | return tok;
|
817 | }
|
818 | },
|
819 |
|
820 | |
821 |
|
822 |
|
823 |
|
824 |
|
825 | pipelessText: function() {
|
826 | if (!this.pipeless) return;
|
827 | var captures, re;
|
828 |
|
829 |
|
830 | if (this.indentRe) {
|
831 | captures = this.indentRe.exec(this.input);
|
832 |
|
833 | } else {
|
834 |
|
835 | re = /^\n(\t*) */;
|
836 | captures = re.exec(this.input);
|
837 |
|
838 |
|
839 | if (captures && !captures[1].length) {
|
840 | re = /^\n( *)/;
|
841 | captures = re.exec(this.input);
|
842 | }
|
843 |
|
844 |
|
845 | if (captures && captures[1].length) this.indentRe = re;
|
846 | }
|
847 |
|
848 | var indents = captures && captures[1].length;
|
849 | if (indents && (this.indentStack.length === 0 || indents > this.indentStack[0])) {
|
850 | var indent = captures[1];
|
851 | var line;
|
852 | var tokens = [];
|
853 | var isMatch;
|
854 | do {
|
855 |
|
856 | var i = this.input.substr(1).indexOf('\n');
|
857 | if (-1 == i) i = this.input.length - 1;
|
858 | var str = this.input.substr(1, i);
|
859 | isMatch = str.substr(0, indent.length) === indent || !str.trim();
|
860 | if (isMatch) {
|
861 |
|
862 | this.consume(str.length + 1);
|
863 | ++this.lineno;
|
864 | tokens.push(str.substr(indent.length));
|
865 | }
|
866 | } while(this.input.length && isMatch);
|
867 | while (this.input.length === 0 && tokens[tokens.length - 1] === '') tokens.pop();
|
868 | return this.tok('pipeless-text', tokens);
|
869 | }
|
870 | },
|
871 |
|
872 | |
873 |
|
874 |
|
875 |
|
876 | colon: function() {
|
877 | var good = /^: +/.test(this.input);
|
878 | var res = this.scan(/^: */, ':');
|
879 | if (res && !good) {
|
880 | console.warn('Warning: space required after `:` on line ' + this.lineno +
|
881 | ' of jade file "' + this.filename + '"');
|
882 | }
|
883 | return res;
|
884 | },
|
885 |
|
886 | fail: function () {
|
887 | throw new Error('unexpected text ' + this.input.substr(0, 5));
|
888 | },
|
889 |
|
890 | |
891 |
|
892 |
|
893 |
|
894 |
|
895 |
|
896 |
|
897 |
|
898 | advance: function(){
|
899 | return this.stashed()
|
900 | || this.next();
|
901 | },
|
902 |
|
903 | |
904 |
|
905 |
|
906 |
|
907 |
|
908 |
|
909 |
|
910 | next: function() {
|
911 | return this.deferred()
|
912 | || this.blank()
|
913 | || this.eos()
|
914 | || this.pipelessText()
|
915 | || this.yield()
|
916 | || this.doctype()
|
917 | || this.interpolation()
|
918 | || this["case"]()
|
919 | || this.when()
|
920 | || this["default"]()
|
921 | || this["extends"]()
|
922 | || this.append()
|
923 | || this.prepend()
|
924 | || this.block()
|
925 | || this.mixinBlock()
|
926 | || this.include()
|
927 | || this.includeFiltered()
|
928 | || this.mixin()
|
929 | || this.call()
|
930 | || this.conditional()
|
931 | || this.each()
|
932 | || this["while"]()
|
933 | || this.tag()
|
934 | || this.filter()
|
935 | || this.blockCode()
|
936 | || this.code()
|
937 | || this.id()
|
938 | || this.className()
|
939 | || this.attrs()
|
940 | || this.attributesBlock()
|
941 | || this.indent()
|
942 | || this.text()
|
943 | || this.comment()
|
944 | || this.colon()
|
945 | || this.dot()
|
946 | || this.textFail()
|
947 | || this.fail();
|
948 | }
|
949 | };
|