source: Dev/branches/rest-dojo-ui/client/util/less/parser.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

  • Property svn:executable set to *
File size: 40.9 KB
Line 
1var less, tree;
2
3if (typeof(window) === 'undefined') {
4    less = exports,
5    tree = require('less/tree');
6} else {
7    if (typeof(window.less) === 'undefined') { window.less = {} }
8    less = window.less,
9    tree = window.less.tree = {};
10}
11//
12// less.js - parser
13//
14//    A relatively straight-forward predictive parser.
15//    There is no tokenization/lexing stage, the input is parsed
16//    in one sweep.
17//
18//    To make the parser fast enough to run in the browser, several
19//    optimization had to be made:
20//
21//    - Matching and slicing on a huge input is often cause of slowdowns.
22//      The solution is to chunkify the input into smaller strings.
23//      The chunks are stored in the `chunks` var,
24//      `j` holds the current chunk index, and `current` holds
25//      the index of the current chunk in relation to `input`.
26//      This gives us an almost 4x speed-up.
27//
28//    - In many cases, we don't need to match individual tokens;
29//      for example, if a value doesn't hold any variables, operations
30//      or dynamic references, the parser can effectively 'skip' it,
31//      treating it as a literal.
32//      An example would be '1px solid #000' - which evaluates to itself,
33//      we don't need to know what the individual components are.
34//      The drawback, of course is that you don't get the benefits of
35//      syntax-checking on the CSS. This gives us a 50% speed-up in the parser,
36//      and a smaller speed-up in the code-gen.
37//
38//
39//    Token matching is done with the `$` function, which either takes
40//    a terminal string or regexp, or a non-terminal function to call.
41//    It also takes care of moving all the indices forwards.
42//
43//
44less.Parser = function Parser(env) {
45    var input,       // LeSS input string
46        i,           // current index in `input`
47        j,           // current chunk
48        temp,        // temporarily holds a chunk's state, for backtracking
49        memo,        // temporarily holds `i`, when backtracking
50        furthest,    // furthest index the parser has gone to
51        chunks,      // chunkified input
52        current,     // index of current chunk, in `input`
53        parser;
54
55    var that = this;
56
57    // This function is called after all files
58    // have been imported through `@import`.
59    var finish = function () {};
60
61    var imports = this.imports = {
62        paths: env && env.paths || [],  // Search paths, when importing
63        queue: [],                      // Files which haven't been imported yet
64        files: {},                      // Holds the imported parse trees
65        mime:  env && env.mime,         // MIME type of .less files
66        push: function (path, callback) {
67            var that = this;
68            this.queue.push(path);
69
70            //
71            // Import a file asynchronously
72            //
73            less.Parser.importer(path, this.paths, function (root) {
74                that.queue.splice(that.queue.indexOf(path), 1); // Remove the path from the queue
75                that.files[path] = root;                        // Store the root
76
77                callback(root);
78
79                if (that.queue.length === 0) { finish() }       // Call `finish` if we're done importing
80            }, env);
81        }
82    };
83
84    function save()    { temp = chunks[j], memo = i, current = i }
85    function restore() { chunks[j] = temp, i = memo, current = i }
86
87    function sync() {
88        if (i > current) {
89            chunks[j] = chunks[j].slice(i - current);
90            current = i;
91        }
92    }
93    //
94    // Parse from a token, regexp or string, and move forward if match
95    //
96    function $(tok) {
97        var match, args, length, c, index, endIndex, k, mem;
98
99        //
100        // Non-terminal
101        //
102        if (tok instanceof Function) {
103            return tok.call(parser.parsers);
104        //
105        // Terminal
106        //
107        //     Either match a single character in the input,
108        //     or match a regexp in the current chunk (chunk[j]).
109        //
110        } else if (typeof(tok) === 'string') {
111            match = input.charAt(i) === tok ? tok : null;
112            length = 1;
113            sync ();
114        } else {
115            sync ();
116
117            if (match = tok.exec(chunks[j])) {
118                length = match[0].length;
119            } else {
120                return null;
121            }
122        }
123
124        // The match is confirmed, add the match length to `i`,
125        // and consume any extra white-space characters (' ' || '\n')
126        // which come after that. The reason for this is that LeSS's
127        // grammar is mostly white-space insensitive.
128        //
129        if (match) {
130            mem = i += length;
131            endIndex = i + chunks[j].length - length;
132
133            while (i < endIndex) {
134                c = input.charCodeAt(i);
135                if (! (c === 32 || c === 10 || c === 9)) { break }
136                i++;
137            }
138            chunks[j] = chunks[j].slice(length + (i - mem));
139            current = i;
140
141            if (chunks[j].length === 0 && j < chunks.length - 1) { j++ }
142
143            if(typeof(match) === 'string') {
144                return match;
145            } else {
146                return match.length === 1 ? match[0] : match;
147            }
148        }
149    }
150
151    // Same as $(), but don't change the state of the parser,
152    // just return the match.
153    function peek(tok) {
154        if (typeof(tok) === 'string') {
155            return input.charAt(i) === tok;
156        } else {
157            if (tok.test(chunks[j])) {
158                return true;
159            } else {
160                return false;
161            }
162        }
163    }
164
165    this.env = env = env || {};
166
167    // The optimization level dictates the thoroughness of the parser,
168    // the lower the number, the less nodes it will create in the tree.
169    // This could matter for debugging, or if you want to access
170    // the individual nodes in the tree.
171    this.optimization = ('optimization' in this.env) ? this.env.optimization : 1;
172
173    this.env.filename = this.env.filename || null;
174
175    //
176    // The Parser
177    //
178    return parser = {
179
180        imports: imports,
181        //
182        // Parse an input string into an abstract syntax tree,
183        // call `callback` when done.
184        //
185        parse: function (str, callback) {
186            var root, start, end, zone, line, lines, buff = [], c, error = null;
187
188            i = j = current = furthest = 0;
189            chunks = [];
190            input = str.replace(/\r\n/g, '\n');
191
192            // Split the input into chunks.
193            chunks = (function (chunks) {
194                var j = 0,
195                    skip = /[^"'`\{\}\/\(\)]+/g,
196                    comment = /\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g,
197                    level = 0,
198                    match,
199                    chunk = chunks[0],
200                    inParam,
201                    inString;
202
203                for (var i = 0, c, cc; i < input.length; i++) {
204                    skip.lastIndex = i;
205                    if (match = skip.exec(input)) {
206                        if (match.index === i) {
207                            i += match[0].length;
208                            chunk.push(match[0]);
209                        }
210                    }
211                    c = input.charAt(i);
212                    comment.lastIndex = i;
213
214                    if (!inString && !inParam && c === '/') {
215                        cc = input.charAt(i + 1);
216                        if (cc === '/' || cc === '*') {
217                            if (match = comment.exec(input)) {
218                                if (match.index === i) {
219                                    i += match[0].length;
220                                    chunk.push(match[0]);
221                                    c = input.charAt(i);
222                                }
223                            }
224                        }
225                    }
226
227                    if        (c === '{' && !inString && !inParam) { level ++;
228                        chunk.push(c);
229                    } else if (c === '}' && !inString && !inParam) { level --;
230                        chunk.push(c);
231                        chunks[++j] = chunk = [];
232                    } else if (c === '(' && !inString && !inParam) {
233                        chunk.push(c);
234                        inParam = true;
235                    } else if (c === ')' && !inString && inParam) {
236                        chunk.push(c);
237                        inParam = false;
238                    } else {
239                        if (c === '"' || c === "'" || c === '`') {
240                            if (! inString) {
241                                inString = c;
242                            } else {
243                                inString = inString === c ? false : inString;
244                            }
245                        }
246                        chunk.push(c);
247                    }
248                }
249                if (level > 0) {
250                    throw {
251                        type: 'Syntax',
252                        message: "Missing closing `}`",
253                        filename: env.filename
254                    };
255                }
256
257                return chunks.map(function (c) { return c.join('') });;
258            })([[]]);
259
260            // Start with the primary rule.
261            // The whole syntax tree is held under a Ruleset node,
262            // with the `root` property set to true, so no `{}` are
263            // output. The callback is called when the input is parsed.
264            root = new(tree.Ruleset)([], $(this.parsers.primary));
265            root.root = true;
266
267            root.toCSS = (function (evaluate) {
268                var line, lines, column;
269
270                return function (options, variables) {
271                    var frames = [];
272
273                    options = options || {};
274                    //
275                    // Allows setting variables with a hash, so:
276                    //
277                    //   `{ color: new(tree.Color)('#f01') }` will become:
278                    //
279                    //   new(tree.Rule)('@color',
280                    //     new(tree.Value)([
281                    //       new(tree.Expression)([
282                    //         new(tree.Color)('#f01')
283                    //       ])
284                    //     ])
285                    //   )
286                    //
287                    if (typeof(variables) === 'object' && !Array.isArray(variables)) {
288                        variables = Object.keys(variables).map(function (k) {
289                            var value = variables[k];
290
291                            if (! (value instanceof tree.Value)) {
292                                if (! (value instanceof tree.Expression)) {
293                                    value = new(tree.Expression)([value]);
294                                }
295                                value = new(tree.Value)([value]);
296                            }
297                            return new(tree.Rule)('@' + k, value, false, 0);
298                        });
299                        frames = [new(tree.Ruleset)(null, variables)];
300                    }
301
302                    try {
303                        var css = evaluate.call(this, { frames: frames })
304                                          .toCSS([], { compress: options.compress || false });
305                    } catch (e) {
306                        lines = input.split('\n');
307                        line = getLine(e.index);
308
309                        for (var n = e.index, column = -1;
310                                 n >= 0 && input.charAt(n) !== '\n';
311                                 n--) { column++ }
312
313                        throw {
314                            type: e.type,
315                            message: e.message,
316                            filename: env.filename,
317                            index: e.index,
318                            line: typeof(line) === 'number' ? line + 1 : null,
319                            callLine: e.call && (getLine(e.call) + 1),
320                            callExtract: lines[getLine(e.call)],
321                            stack: e.stack,
322                            column: column,
323                            extract: [
324                                lines[line - 1],
325                                lines[line],
326                                lines[line + 1]
327                            ]
328                        };
329                    }
330                    if (options.compress) {
331                        return css.replace(/(\s)+/g, "$1");
332                    } else {
333                        return css;
334                    }
335
336                    function getLine(index) {
337                        return index ? (input.slice(0, index).match(/\n/g) || "").length : null;
338                    }
339                };
340            })(root.eval);
341
342            // If `i` is smaller than the `input.length - 1`,
343            // it means the parser wasn't able to parse the whole
344            // string, so we've got a parsing error.
345            //
346            // We try to extract a \n delimited string,
347            // showing the line where the parse error occured.
348            // We split it up into two parts (the part which parsed,
349            // and the part which didn't), so we can color them differently.
350            if (i < input.length - 1) {
351                i = furthest;
352                lines = input.split('\n');
353                line = (input.slice(0, i).match(/\n/g) || "").length + 1;
354
355                for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\n'; n--) { column++ }
356
357                error = {
358                    name: "ParseError",
359                    message: "Syntax Error on line " + line,
360                    index: i,
361                    filename: env.filename,
362                    line: line,
363                    column: column,
364                    extract: [
365                        lines[line - 2],
366                        lines[line - 1],
367                        lines[line]
368                    ]
369                };
370            }
371
372            if (this.imports.queue.length > 0) {
373                finish = function () { callback(error, root) };
374            } else {
375                callback(error, root);
376            }
377        },
378
379        //
380        // Here in, the parsing rules/functions
381        //
382        // The basic structure of the syntax tree generated is as follows:
383        //
384        //   Ruleset ->  Rule -> Value -> Expression -> Entity
385        //
386        // Here's some LESS code:
387        //
388        //    .class {
389        //      color: #fff;
390        //      border: 1px solid #000;
391        //      width: @w + 4px;
392        //      > .child {...}
393        //    }
394        //
395        // And here's what the parse tree might look like:
396        //
397        //     Ruleset (Selector '.class', [
398        //         Rule ("color",  Value ([Expression [Color #fff]]))
399        //         Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
400        //         Rule ("width",  Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
401        //         Ruleset (Selector [Element '>', '.child'], [...])
402        //     ])
403        //
404        //  In general, most rules will try to parse a token with the `$()` function, and if the return
405        //  value is truly, will return a new node, of the relevant type. Sometimes, we need to check
406        //  first, before parsing, that's when we use `peek()`.
407        //
408        parsers: {
409            //
410            // The `primary` rule is the *entry* and *exit* point of the parser.
411            // The rules here can appear at any level of the parse tree.
412            //
413            // The recursive nature of the grammar is an interplay between the `block`
414            // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
415            // as represented by this simplified grammar:
416            //
417            //     primary  →  (ruleset | rule)+
418            //     ruleset  →  selector+ block
419            //     block    →  '{' primary '}'
420            //
421            // Only at one point is the primary rule not called from the
422            // block rule: at the root level.
423            //
424            primary: function () {
425                var node, root = [];
426
427                while ((node = $(this.mixin.definition) || $(this.rule)    ||  $(this.ruleset) ||
428                               $(this.mixin.call)       || $(this.comment) ||  $(this.directive))
429                               || $(/^[\s\n]+/)) {
430                    node && root.push(node);
431                }
432                return root;
433            },
434
435            // We create a Comment node for CSS comments `/* */`,
436            // but keep the LeSS comments `//` silent, by just skipping
437            // over them.
438            comment: function () {
439                var comment;
440
441                if (input.charAt(i) !== '/') return;
442
443                if (input.charAt(i + 1) === '/') {
444                    return new(tree.Comment)($(/^\/\/.*/), true);
445                } else if (comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/)) {
446                    return new(tree.Comment)(comment);
447                }
448            },
449
450            //
451            // Entities are tokens which can be found inside an Expression
452            //
453            entities: {
454                //
455                // A string, which supports escaping " and '
456                //
457                //     "milky way" 'he\'s the one!'
458                //
459                quoted: function () {
460                    var str, j = i, e;
461
462                    if (input.charAt(j) === '~') { j++, e = true } // Escaped strings
463                    if (input.charAt(j) !== '"' && input.charAt(j) !== "'") return;
464
465                    e && $('~');
466
467                    if (str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/)) {
468                        return new(tree.Quoted)(str[0], str[1] || str[2], e);
469                    }
470                },
471
472                //
473                // A catch-all word, such as:
474                //
475                //     black border-collapse
476                //
477                keyword: function () {
478                    var k;
479                    if (k = $(/^[A-Za-z-]+/)) { return new(tree.Keyword)(k) }
480                },
481
482                //
483                // A function call
484                //
485                //     rgb(255, 0, 255)
486                //
487                // We also try to catch IE's `alpha()`, but let the `alpha` parser
488                // deal with the details.
489                //
490                // The arguments are parsed with the `entities.arguments` parser.
491                //
492                call: function () {
493                    var name, args, index = i;
494
495                    if (! (name = /^([\w-]+|%)\(/.exec(chunks[j]))) return;
496
497                    name = name[1].toLowerCase();
498
499                    if (name === 'url') { return null }
500                    else                { i += name.length }
501
502                    if (name === 'alpha') { return $(this.alpha) }
503
504                    $('('); // Parse the '(' and consume whitespace.
505
506                    args = $(this.entities.arguments);
507
508                    if (! $(')')) return;
509
510                    if (name) { return new(tree.Call)(name, args, index) }
511                },
512                arguments: function () {
513                    var args = [], arg;
514
515                    while (arg = $(this.expression)) {
516                        args.push(arg);
517                        if (! $(',')) { break }
518                    }
519                    return args;
520                },
521                literal: function () {
522                    return $(this.entities.dimension) ||
523                           $(this.entities.color) ||
524                           $(this.entities.quoted);
525                },
526
527                //
528                // Parse url() tokens
529                //
530                // We use a specific rule for urls, because they don't really behave like
531                // standard function calls. The difference is that the argument doesn't have
532                // to be enclosed within a string, so it can't be parsed as an Expression.
533                //
534                url: function () {
535                    var value;
536
537                    if (input.charAt(i) !== 'u' || !$(/^url\(/)) return;
538                    value = $(this.entities.quoted)  || $(this.entities.variable) ||
539                            $(this.entities.dataURI) || $(/^[-\w%@$\/.&=:;#+?~]+/) || "";
540                    if (! $(')')) throw new(Error)("missing closing ) for url()");
541
542                    return new(tree.URL)((value.value || value.data || value instanceof tree.Variable)
543                                        ? value : new(tree.Anonymous)(value), imports.paths);
544                },
545
546                dataURI: function () {
547                    var obj;
548
549                    if ($(/^data:/)) {
550                        obj         = {};
551                        obj.mime    = $(/^[^\/]+\/[^,;)]+/)     || '';
552                        obj.charset = $(/^;\s*charset=[^,;)]+/) || '';
553                        obj.base64  = $(/^;\s*base64/)          || '';
554                        obj.data    = $(/^,\s*[^)]+/);
555
556                        if (obj.data) { return obj }
557                    }
558                },
559
560                //
561                // A Variable entity, such as `@fink`, in
562                //
563                //     width: @fink + 2px
564                //
565                // We use a different parser for variable definitions,
566                // see `parsers.variable`.
567                //
568                variable: function () {
569                    var name, index = i;
570
571                    if (input.charAt(i) === '@' && (name = $(/^@@?[\w-]+/))) {
572                        return new(tree.Variable)(name, index);
573                    }
574                },
575
576                //
577                // A Hexadecimal color
578                //
579                //     #4F3C2F
580                //
581                // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
582                //
583                color: function () {
584                    var rgb;
585
586                    if (input.charAt(i) === '#' && (rgb = $(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/))) {
587                        return new(tree.Color)(rgb[1]);
588                    }
589                },
590
591                //
592                // A Dimension, that is, a number and a unit
593                //
594                //     0.5em 95%
595                //
596                dimension: function () {
597                    var value, c = input.charCodeAt(i);
598                    if ((c > 57 || c < 45) || c === 47) return;
599
600                    if (value = $(/^(-?\d*\.?\d+)(px|%|em|pc|ex|in|deg|s|ms|pt|cm|mm|rad|grad|turn)?/)) {
601                        return new(tree.Dimension)(value[1], value[2]);
602                    }
603                },
604
605                //
606                // JavaScript code to be evaluated
607                //
608                //     `window.location.href`
609                //
610                javascript: function () {
611                    var str, j = i, e;
612
613                    if (input.charAt(j) === '~') { j++, e = true } // Escaped strings
614                    if (input.charAt(j) !== '`') { return }
615
616                    e && $('~');
617
618                    if (str = $(/^`([^`]*)`/)) {
619                        return new(tree.JavaScript)(str[1], i, e);
620                    }
621                }
622            },
623
624            //
625            // The variable part of a variable definition. Used in the `rule` parser
626            //
627            //     @fink:
628            //
629            variable: function () {
630                var name;
631
632                if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1] }
633            },
634
635            //
636            // A font size/line-height shorthand
637            //
638            //     small/12px
639            //
640            // We need to peek first, or we'll match on keywords and dimensions
641            //
642            shorthand: function () {
643                var a, b;
644
645                if (! peek(/^[@\w.%-]+\/[@\w.-]+/)) return;
646
647                if ((a = $(this.entity)) && $('/') && (b = $(this.entity))) {
648                    return new(tree.Shorthand)(a, b);
649                }
650            },
651
652            //
653            // Mixins
654            //
655            mixin: {
656                //
657                // A Mixin call, with an optional argument list
658                //
659                //     #mixins > .square(#fff);
660                //     .rounded(4px, black);
661                //     .button;
662                //
663                // The `while` loop is there because mixins can be
664                // namespaced, but we only support the child and descendant
665                // selector for now.
666                //
667                call: function () {
668                    var elements = [], e, c, args, index = i, s = input.charAt(i);
669
670                    if (s !== '.' && s !== '#') { return }
671
672                    while (e = $(/^[#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/)) {
673                        elements.push(new(tree.Element)(c, e));
674                        c = $('>');
675                    }
676                    $('(') && (args = $(this.entities.arguments)) && $(')');
677
678                    if (elements.length > 0 && ($(';') || peek('}'))) {
679                        return new(tree.mixin.Call)(elements, args, index);
680                    }
681                },
682
683                //
684                // A Mixin definition, with a list of parameters
685                //
686                //     .rounded (@radius: 2px, @color) {
687                //        ...
688                //     }
689                //
690                // Until we have a finer grained state-machine, we have to
691                // do a look-ahead, to make sure we don't have a mixin call.
692                // See the `rule` function for more information.
693                //
694                // We start by matching `.rounded (`, and then proceed on to
695                // the argument list, which has optional default values.
696                // We store the parameters in `params`, with a `value` key,
697                // if there is a value, such as in the case of `@radius`.
698                //
699                // Once we've got our params list, and a closing `)`, we parse
700                // the `{...}` block.
701                //
702                definition: function () {
703                    var name, params = [], match, ruleset, param, value;
704
705                    if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') ||
706                        peek(/^[^{]*(;|})/)) return;
707
708                    if (match = $(/^([#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+)\s*\(/)) {
709                        name = match[1];
710
711                        while (param = $(this.entities.variable) || $(this.entities.literal)
712                                                                 || $(this.entities.keyword)) {
713                            // Variable
714                            if (param instanceof tree.Variable) {
715                                if ($(':')) {
716                                    if (value = $(this.expression)) {
717                                        params.push({ name: param.name, value: value });
718                                    } else {
719                                        throw new(Error)("Expected value");
720                                    }
721                                } else {
722                                    params.push({ name: param.name });
723                                }
724                            } else {
725                                params.push({ value: param });
726                            }
727                            if (! $(',')) { break }
728                        }
729                        if (! $(')')) throw new(Error)("Expected )");
730
731                        ruleset = $(this.block);
732
733                        if (ruleset) {
734                            return new(tree.mixin.Definition)(name, params, ruleset);
735                        }
736                    }
737                }
738            },
739
740            //
741            // Entities are the smallest recognized token,
742            // and can be found inside a rule's value.
743            //
744            entity: function () {
745                return $(this.entities.literal) || $(this.entities.variable) || $(this.entities.url) ||
746                       $(this.entities.call)    || $(this.entities.keyword)  || $(this.entities.javascript) ||
747                       $(this.comment);
748            },
749
750            //
751            // A Rule terminator. Note that we use `peek()` to check for '}',
752            // because the `block` rule will be expecting it, but we still need to make sure
753            // it's there, if ';' was ommitted.
754            //
755            end: function () {
756                return $(';') || peek('}');
757            },
758
759            //
760            // IE's alpha function
761            //
762            //     alpha(opacity=88)
763            //
764            alpha: function () {
765                var value;
766
767                if (! $(/^\(opacity=/i)) return;
768                if (value = $(/^\d+/) || $(this.entities.variable)) {
769                    if (! $(')')) throw new(Error)("missing closing ) for alpha()");
770                    return new(tree.Alpha)(value);
771                }
772            },
773
774            //
775            // A Selector Element
776            //
777            //     div
778            //     + h1
779            //     #socks
780            //     input[type="text"]
781            //
782            // Elements are the building blocks for Selectors,
783            // they are made out of a `Combinator` (see combinator rule),
784            // and an element name, such as a tag a class, or `*`.
785            //
786            element: function () {
787                var e, t, c;
788
789                c = $(this.combinator);
790                e = $(/^(?:[.#]?|:*)(?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/) || $('*') || $(this.attribute) || $(/^\([^)@]+\)/) || $(/^(?:\d*\.)?\d+%/);
791
792                if (e) { return new(tree.Element)(c, e) }
793
794                if (c.value && c.value[0] === '&') {
795                  return new(tree.Element)(c, null);
796                }
797            },
798
799            //
800            // Combinators combine elements together, in a Selector.
801            //
802            // Because our parser isn't white-space sensitive, special care
803            // has to be taken, when parsing the descendant combinator, ` `,
804            // as it's an empty space. We have to check the previous character
805            // in the input, to see if it's a ` ` character. More info on how
806            // we deal with this in *combinator.js*.
807            //
808            combinator: function () {
809                var match, c = input.charAt(i);
810
811                if (c === '>' || c === '+' || c === '~') {
812                    i++;
813                    while (input.charAt(i) === ' ') { i++ }
814                    return new(tree.Combinator)(c);
815                } else if (c === '&') {
816                    match = '&';
817                    i++;
818                    if(input.charAt(i) === ' ') {
819                        match = '& ';
820                    }
821                    while (input.charAt(i) === ' ') { i++ }
822                    return new(tree.Combinator)(match);
823                } else if (c === ':' && input.charAt(i + 1) === ':') {
824                    i += 2;
825                    while (input.charAt(i) === ' ') { i++ }
826                    return new(tree.Combinator)('::');
827                } else if (input.charAt(i - 1) === ' ') {
828                    return new(tree.Combinator)(" ");
829                } else {
830                    return new(tree.Combinator)(null);
831                }
832            },
833
834            //
835            // A CSS Selector
836            //
837            //     .class > div + h1
838            //     li a:hover
839            //
840            // Selectors are made out of one or more Elements, see above.
841            //
842            selector: function () {
843                var sel, e, elements = [], c, match;
844
845                while (e = $(this.element)) {
846                    c = input.charAt(i);
847                    elements.push(e)
848                    if (c === '{' || c === '}' || c === ';' || c === ',') { break }
849                }
850
851                if (elements.length > 0) { return new(tree.Selector)(elements) }
852            },
853            tag: function () {
854                return $(/^[a-zA-Z][a-zA-Z-]*[0-9]?/) || $('*');
855            },
856            attribute: function () {
857                var attr = '', key, val, op;
858
859                if (! $('[')) return;
860
861                if (key = $(/^[a-zA-Z-]+/) || $(this.entities.quoted)) {
862                    if ((op = $(/^[|~*$^]?=/)) &&
863                        (val = $(this.entities.quoted) || $(/^[\w-]+/))) {
864                        attr = [key, op, val.toCSS ? val.toCSS() : val].join('');
865                    } else { attr = key }
866                }
867
868                if (! $(']')) return;
869
870                if (attr) { return "[" + attr + "]" }
871            },
872
873            //
874            // The `block` rule is used by `ruleset` and `mixin.definition`.
875            // It's a wrapper around the `primary` rule, with added `{}`.
876            //
877            block: function () {
878                var content;
879
880                if ($('{') && (content = $(this.primary)) && $('}')) {
881                    return content;
882                }
883            },
884
885            //
886            // div, .class, body > p {...}
887            //
888            ruleset: function () {
889                var selectors = [], s, rules, match;
890                save();
891
892                if (match = /^([.#:% \w-]+)[\s\n]*\{/.exec(chunks[j])) {
893                    i += match[0].length - 1;
894                    selectors = [new(tree.Selector)([new(tree.Element)(null, match[1])])];
895                } else {
896                    while (s = $(this.selector)) {
897                        selectors.push(s);
898                        $(this.comment);
899                        if (! $(',')) { break }
900                        $(this.comment);
901                    }
902                }
903
904                if (selectors.length > 0 && (rules = $(this.block))) {
905                    return new(tree.Ruleset)(selectors, rules);
906                } else {
907                    // Backtrack
908                    furthest = i;
909                    restore();
910                }
911            },
912            rule: function () {
913                var name, value, c = input.charAt(i), important, match;
914                save();
915
916                if (c === '.' || c === '#' || c === '&') { return }
917
918                if (name = $(this.variable) || $(this.property)) {
919                    if ((name.charAt(0) != '@') && (match = /^([^@+\/'"*`(;{}-]*);/.exec(chunks[j]))) {
920                        i += match[0].length - 1;
921                        value = new(tree.Anonymous)(match[1]);
922                    } else if (name === "font") {
923                        value = $(this.font);
924                    } else {
925                        value = $(this.value);
926                    }
927                    important = $(this.important);
928
929                    if (value && $(this.end)) {
930                        return new(tree.Rule)(name, value, important, memo);
931                    } else {
932                        furthest = i;
933                        restore();
934                    }
935                }
936            },
937
938            //
939            // An @import directive
940            //
941            //     @import "lib";
942            //
943            // Depending on our environemnt, importing is done differently:
944            // In the browser, it's an XHR request, in Node, it would be a
945            // file-system operation. The function used for importing is
946            // stored in `import`, which we pass to the Import constructor.
947            //
948            "import": function () {
949                var path;
950                if ($(/^@import\s+/) &&
951                    (path = $(this.entities.quoted) || $(this.entities.url)) &&
952                    $(';')) {
953                    return new(tree.Import)(path, imports);
954                }
955            },
956
957            //
958            // A CSS Directive
959            //
960            //     @charset "utf-8";
961            //
962            directive: function () {
963                var name, value, rules, types;
964
965                if (input.charAt(i) !== '@') return;
966
967                if (value = $(this['import'])) {
968                    return value;
969                } else if (name = $(/^@media|@page/) || $(/^@(?:-webkit-)?keyframes/)) {
970                    types = ($(/^[^{]+/) || '').trim();
971                    if (rules = $(this.block)) {
972                        return new(tree.Directive)(name + " " + types, rules);
973                    }
974                } else if (name = $(/^@[-a-z]+/)) {
975                    if (name === '@font-face') {
976                        if (rules = $(this.block)) {
977                            return new(tree.Directive)(name, rules);
978                        }
979                    } else if ((value = $(this.entity)) && $(';')) {
980                        return new(tree.Directive)(name, value);
981                    }
982                }
983            },
984            font: function () {
985                var value = [], expression = [], weight, shorthand, font, e;
986
987                while (e = $(this.shorthand) || $(this.entity)) {
988                    expression.push(e);
989                }
990                value.push(new(tree.Expression)(expression));
991
992                if ($(',')) {
993                    while (e = $(this.expression)) {
994                        value.push(e);
995                        if (! $(',')) { break }
996                    }
997                }
998                return new(tree.Value)(value);
999            },
1000
1001            //
1002            // A Value is a comma-delimited list of Expressions
1003            //
1004            //     font-family: Baskerville, Georgia, serif;
1005            //
1006            // In a Rule, a Value represents everything after the `:`,
1007            // and before the `;`.
1008            //
1009            value: function () {
1010                var e, expressions = [], important;
1011
1012                while (e = $(this.expression)) {
1013                    expressions.push(e);
1014                    if (! $(',')) { break }
1015                }
1016
1017                if (expressions.length > 0) {
1018                    return new(tree.Value)(expressions);
1019                }
1020            },
1021            important: function () {
1022                if (input.charAt(i) === '!') {
1023                    return $(/^! *important/);
1024                }
1025            },
1026            sub: function () {
1027                var e;
1028
1029                if ($('(') && (e = $(this.expression)) && $(')')) {
1030                    return e;
1031                }
1032            },
1033            multiplication: function () {
1034                var m, a, op, operation;
1035                if (m = $(this.operand)) {
1036                    while ((op = ($('/') || $('*'))) && (a = $(this.operand))) {
1037                        operation = new(tree.Operation)(op, [operation || m, a]);
1038                    }
1039                    return operation || m;
1040                }
1041            },
1042            addition: function () {
1043                var m, a, op, operation;
1044                if (m = $(this.multiplication)) {
1045                    while ((op = $(/^[-+]\s+/) || (input.charAt(i - 1) != ' ' && ($('+') || $('-')))) &&
1046                           (a = $(this.multiplication))) {
1047                        operation = new(tree.Operation)(op, [operation || m, a]);
1048                    }
1049                    return operation || m;
1050                }
1051            },
1052
1053            //
1054            // An operand is anything that can be part of an operation,
1055            // such as a Color, or a Variable
1056            //
1057            operand: function () {
1058                var negate, p = input.charAt(i + 1);
1059
1060                if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-') }
1061                var o = $(this.sub) || $(this.entities.dimension) ||
1062                        $(this.entities.color) || $(this.entities.variable) ||
1063                        $(this.entities.call);
1064                return negate ? new(tree.Operation)('*', [new(tree.Dimension)(-1), o])
1065                              : o;
1066            },
1067
1068            //
1069            // Expressions either represent mathematical operations,
1070            // or white-space delimited Entities.
1071            //
1072            //     1px solid black
1073            //     @var * 2
1074            //
1075            expression: function () {
1076                var e, delim, entities = [], d;
1077
1078                while (e = $(this.addition) || $(this.entity)) {
1079                    entities.push(e);
1080                }
1081                if (entities.length > 0) {
1082                    return new(tree.Expression)(entities);
1083                }
1084            },
1085            property: function () {
1086                var name;
1087
1088                if (name = $(/^(\*?-?[-a-z_0-9]+)\s*:/)) {
1089                    return name[1];
1090                }
1091            }
1092        }
1093    };
1094};
1095
1096if (typeof(window) !== 'undefined') {
1097    //
1098    // Used by `@import` directives
1099    //
1100    less.Parser.importer = function (path, paths, callback, env) {
1101        if (path.charAt(0) !== '/' && paths.length > 0) {
1102            path = paths[0] + path;
1103        }
1104        // We pass `true` as 3rd argument, to force the reload of the import.
1105        // This is so we can get the syntax tree as opposed to just the CSS output,
1106        // as we need this to evaluate the current stylesheet.
1107        loadStyleSheet({ href: path, title: path, type: env.mime }, callback, true);
1108    };
1109}
1110
Note: See TracBrowser for help on using the repository browser.