[Pkg-javascript-commits] [less.js] 73/285: pull out the input processing out of the parser class and a few small improvements - the no js option now applies to all js, including that within quotes. The Javascript node now also returns the right index.

Jonas Smedegaard dr at jones.dk
Mon Oct 26 23:23:40 UTC 2015


This is an automated email from the git hooks/post-receive script.

js pushed a commit to annotated tag v2.0.0
in repository less.js.

commit 41732cc60bbcf5191ebb5bf1794719963f84671d
Author: Luke Page <luke.a.page at gmail.com>
Date:   Mon Aug 25 12:19:04 2014 +0100

    pull out the input processing out of the parser class and a few small improvements - the no js option now applies to all js, including that within quotes. The Javascript node now also returns the right index.
---
 lib/less/contexts.js                          |   4 +-
 lib/less/parser/parser-input.js               | 285 ++++++++++
 lib/less/parser/parser.js                     | 749 +++++++++-----------------
 lib/less/tree/js-eval-node.js                 |   5 +
 test/less/errors/javascript-error.txt         |   2 +-
 test/less/errors/javascript-undefined-var.txt |   2 +-
 6 files changed, 548 insertions(+), 499 deletions(-)

diff --git a/lib/less/contexts.js b/lib/less/contexts.js
index 5df2cf8..269048a 100644
--- a/lib/less/contexts.js
+++ b/lib/less/contexts.js
@@ -24,7 +24,6 @@ var parseCopyProperties = [
     'compress',         // option - whether to compress
     'processImports',   // option - whether to process imports. if false then imports will not be imported
     'syncImport',       // option - whether to import synchronously
-    'javascriptEnabled',// option - whether JavaScript is enabled. if undefined, defaults to true
     'chunkInput',       // option - whether to chunk input. more performant but causes parse issues.
     'mime',             // browser only - mime type for sheet import
     'useFileCache',     // browser only - whether to use the per file session cache
@@ -77,7 +76,8 @@ var evalCopyProperties = [
     'cleancss',       // whether to compress with clean-css
     'sourceMap',      // whether to output a source map
     'importMultiple', // whether we are currently importing multiple copies
-    'urlArgs'         // whether to add args into url tokens
+    'urlArgs',        // whether to add args into url tokens
+    'javascriptEnabled'// option - whether JavaScript is enabled. if undefined, defaults to true
     ];
 
 contexts.evalEnv = function(options, frames) {
diff --git a/lib/less/parser/parser-input.js b/lib/less/parser/parser-input.js
new file mode 100644
index 0000000..7b03e24
--- /dev/null
+++ b/lib/less/parser/parser-input.js
@@ -0,0 +1,285 @@
+var chunker = require('./chunker.js'),
+    LessError = require('../less-error.js');
+module.exports = function() {
+    var input,       // LeSS input string
+        j,           // current chunk
+        saveStack = [],   // holds state for backtracking
+        furthest,    // furthest index the parser has gone to
+        furthestPossibleErrorMessage,// if this is furthest we got to, this is the probably cause
+        chunks,      // chunkified input
+        current,     // current chunk
+        currentPos,  // index of current chunk, in `input`
+        parserInput = {};
+
+    parserInput.save = function() {
+        currentPos = parserInput.i;
+        saveStack.push( { current: current, i: parserInput.i, j: j });
+    };
+    parserInput.restore = function(possibleErrorMessage) {
+        if (parserInput.i > furthest) {
+            furthest = parserInput.i;
+            furthestPossibleErrorMessage = possibleErrorMessage;
+        }
+        var state = saveStack.pop();
+        current = state.current;
+        currentPos = parserInput.i = state.i;
+        j = state.j;
+    };
+    parserInput.forget = function() {
+        saveStack.pop();
+    };
+    function sync() {
+        if (parserInput.i > currentPos) {
+            current = current.slice(parserInput.i - currentPos);
+            currentPos = parserInput.i;
+        }
+    }
+    parserInput.isWhitespace = function (offset) {
+        var pos = parserInput.i + (offset || 0),
+            code = input.charCodeAt(pos);
+        return (code === CHARCODE_SPACE || code === CHARCODE_CR || code === CHARCODE_TAB || code === CHARCODE_LF);
+    };
+    //
+    // Parse from a token, regexp or string, and move forward if match
+    //
+    parserInput.$ = function(tok) {
+        var tokType = typeof tok,
+            match, length;
+
+        // Either match a single character in the input,
+        // or match a regexp in the current chunk (`current`).
+        //
+        if (tokType === "string") {
+            if (input.charAt(parserInput.i) !== tok) {
+                return null;
+            }
+            skipWhitespace(1);
+            return tok;
+        }
+
+        // regexp
+        sync();
+        if (! (match = tok.exec(current))) {
+            return null;
+        }
+
+        length = match[0].length;
+
+        // The match is confirmed, add the match length to `i`,
+        // and consume any extra white-space characters (' ' || '\n')
+        // which come after that. The reason for this is that LeSS's
+        // grammar is mostly white-space insensitive.
+        //
+        skipWhitespace(length);
+
+        if(typeof(match) === 'string') {
+            return match;
+        } else {
+            return match.length === 1 ? match[0] : match;
+        }
+    };
+
+    // Specialization of $(tok)
+    parserInput.$re = function(tok) {
+        if (parserInput.i > currentPos) {
+            current = current.slice(parserInput.i - currentPos);
+            currentPos = parserInput.i;
+        }
+        var m = tok.exec(current);
+        if (!m) {
+            return null;
+        }
+
+        skipWhitespace(m[0].length);
+        if(typeof m === "string") {
+            return m;
+        }
+
+        return m.length === 1 ? m[0] : m;
+    };
+
+    // Specialization of $(tok)
+    parserInput.$char = function(tok) {
+        if (input.charAt(parserInput.i) !== tok) {
+            return null;
+        }
+        skipWhitespace(1);
+        return tok;
+    };
+
+    var CHARCODE_SPACE = 32,
+        CHARCODE_TAB = 9,
+        CHARCODE_LF = 10,
+        CHARCODE_CR = 13,
+        CHARCODE_PLUS = 43,
+        CHARCODE_COMMA = 44,
+        CHARCODE_FORWARD_SLASH = 47,
+        CHARCODE_9 = 57;
+
+    parserInput.autoCommentAbsorb = true;
+    parserInput.commentStore = [];
+    parserInput.finished = false;
+
+    var skipWhitespace = function(length) {
+        var oldi = parserInput.i, oldj = j,
+            curr = parserInput.i - currentPos,
+            endIndex = parserInput.i + current.length - curr,
+            mem = (parserInput.i += length),
+            inp = input,
+            c, nextChar, comment;
+
+        for (; parserInput.i < endIndex; parserInput.i++) {
+            c = inp.charCodeAt(parserInput.i);
+
+            if (parserInput.autoCommentAbsorb && c === CHARCODE_FORWARD_SLASH) {
+                nextChar = inp[parserInput.i + 1];
+                if (nextChar === '/') {
+                    comment = {index: parserInput.i, isLineComment: true};
+                    var nextNewLine = inp.indexOf("\n", parserInput.i + 1);
+                    if (nextNewLine < 0) {
+                        nextNewLine = endIndex;
+                    }
+                    parserInput.i = nextNewLine;
+                    comment.text = inp.substr(comment.i, parserInput.i - comment.i);
+                    parserInput.commentStore.push(comment);
+                    continue;
+                } else if (nextChar === '*') {
+                    var haystack = inp.substr(parserInput.i);
+                    var comment_search_result = haystack.match(/^\/\*(?:[^*]|\*+[^\/*])*\*+\//);
+                    if (comment_search_result) {
+                        comment = {
+                            index: parserInput.i,
+                            text: comment_search_result[0],
+                            isLineComment: false
+                        };
+                        parserInput.i += comment.text.length - 1;
+                        parserInput.commentStore.push(comment);
+                        continue;
+                    }
+                }
+                break;
+            }
+
+            if ((c !== CHARCODE_SPACE) && (c !== CHARCODE_LF) && (c !== CHARCODE_TAB) && (c !== CHARCODE_CR)) {
+                break;
+            }
+        }
+
+        current = current.slice(length + parserInput.i - mem + curr);
+        currentPos = parserInput.i;
+
+        if (!current.length) {
+            if (j < chunks.length - 1)
+            {
+                current = chunks[++j];
+                skipWhitespace(0); // skip space at the beginning of a chunk
+                return true; // things changed
+            }
+            parserInput.finished = true;
+        }
+
+        return oldi !== parserInput.i || oldj !== j;
+    };
+
+    // Same as $(), but don't change the state of the parser,
+    // just return the match.
+    parserInput.peek = function(tok) {
+        if (typeof(tok) === 'string') {
+            return input.charAt(parserInput.i) === tok;
+        } else {
+            return tok.test(current);
+        }
+    };
+
+    // Specialization of peek()
+    // TODO remove or change some currentChar calls to peekChar
+    parserInput.peekChar = function(tok) {
+        return input.charAt(parserInput.i) === tok;
+    };
+
+    parserInput.currentChar = function() {
+        return input.charAt(parserInput.i);
+    };
+
+    parserInput.getInput = function() {
+        return input;
+    };
+
+    parserInput.peekNotNumeric = function() {
+        var c = input.charCodeAt(parserInput.i);
+        //Is the first char of the dimension 0-9, '.', '+' or '-'
+        return (c > CHARCODE_9 || c < CHARCODE_PLUS) || c === CHARCODE_FORWARD_SLASH || c === CHARCODE_COMMA;
+    };
+
+    parserInput.getLocation = function(index, inputStream) {
+        inputStream = inputStream == null ? input : inputStream;
+
+        var n = index + 1,
+            line = null,
+            column = -1;
+
+        while (--n >= 0 && inputStream.charAt(n) !== '\n') {
+            column++;
+        }
+
+        if (typeof index === 'number') {
+            line = (inputStream.slice(0, index).match(/\n/g) || "").length;
+        }
+
+        return {
+            line: line,
+            column: column
+        };
+    };
+
+    parserInput.start = function(str, chunkInput, parser, env) {
+        input = str;
+        parserInput.i = j = currentPos = furthest = 0;
+
+        // chunking apparantly makes things quicker (but my tests indicate
+        // it might actually make things slower in node at least)
+        // and it is a non-perfect parse - it can't recognise
+        // unquoted urls, meaning it can't distinguish comments
+        // meaning comments with quotes or {}() in them get 'counted'
+        // and then lead to parse errors.
+        // In addition if the chunking chunks in the wrong place we might
+        // not be able to parse a parser statement in one go
+        // this is officially deprecated but can be switched on via an option
+        // in the case it causes too much performance issues.
+        if (chunkInput) {
+            chunks = chunker(str, function fail(msg, index) {
+                throw new(LessError)(parser, {
+                    index: index,
+                    type: 'Parse',
+                    message: msg,
+                    filename: env.currentFileInfo.filename
+                }, env);
+            });
+        } else {
+            chunks = [str];
+        }
+
+        current = chunks[0];
+
+        skipWhitespace(0);
+    };
+
+    parserInput.end = function() {
+        var message,
+            isFinished = parserInput.i >= input.length - 1;
+
+        if (parserInput.i < furthest) {
+            message = furthestPossibleErrorMessage;
+            parserInput.i = furthest;
+        }
+        return {
+            isFinished: isFinished,
+            furthest: parserInput.i,
+            furthestPossibleErrorMessage: message,
+            furthestReachedEnd: parserInput.i >= input.length - 1,
+            furthestChar: input[parserInput.i]
+        };
+    };
+
+    return parserInput;
+};
diff --git a/lib/less/parser/parser.js b/lib/less/parser/parser.js
index bc5052b..164780e 100644
--- a/lib/less/parser/parser.js
+++ b/lib/less/parser/parser.js
@@ -1,9 +1,9 @@
-var chunker = require('./chunker.js'),
-    LessError = require('../less-error.js'),
+var LessError = require('../less-error.js'),
     tree = require("../tree/index.js"),
     visitor = require("../visitor/index.js"),
     contexts = require("../contexts.js"),
-    getImportManager = require("./imports.js");
+    getImportManager = require("./imports.js"),
+    getParserInput = require("./parser-input.js");
 
 module.exports = function(less) {
 //
@@ -40,17 +40,9 @@ module.exports = function(less) {
 //
 //
 var Parser = function Parser(env) {
-    var input,       // LeSS input string
-        i,           // current index in `input`
-        j,           // current chunk
-        saveStack = [],   // holds state for backtracking
-        furthest,    // furthest index the parser has gone to
-        furthestPossibleErrorMessage,// if this is furthest we got to, this is the probably cause
-        chunks,      // chunkified input
-        current,     // current chunk
-        currentPos,  // index of current chunk, in `input`
-        parser,
-        parsers;
+    var parser,
+        parsers,
+        parserInput = getParserInput();
 
     // Top parser on an import tree must be sure there is one "env"
     // which will then be passed around by reference.
@@ -61,243 +53,45 @@ var Parser = function Parser(env) {
 
     var imports = this.imports = getImportManager(less, env, Parser);
 
-    function save() {
-        currentPos = i;
-        saveStack.push( { current: current, i: i, j: j });
-    }
-    function restore(possibleErrorMessage) {
-        if (i > furthest) {
-            furthest = i;
-            furthestPossibleErrorMessage = possibleErrorMessage;
-        }
-        var state = saveStack.pop();
-        current = state.current;
-        currentPos = i = state.i;
-        j = state.j;
-    }
-    function forget() {
-        saveStack.pop();
-    }
-
-    function sync() {
-        if (i > currentPos) {
-            current = current.slice(i - currentPos);
-            currentPos = i;
-        }
-    }
-    function isWhitespace(str, pos) {
-        var code = str.charCodeAt(pos | 0);
-        return (code === CHARCODE_SPACE || code === CHARCODE_CR || code === CHARCODE_TAB || code === CHARCODE_LF);
-    }
-    //
-    // Parse from a token, regexp or string, and move forward if match
-    //
-    function $(tok) {
-        var tokType = typeof tok,
-            match, length;
-
-        // Either match a single character in the input,
-        // or match a regexp in the current chunk (`current`).
-        //
-        if (tokType === "string") {
-            if (input.charAt(i) !== tok) {
-                return null;
-            }
-            skipWhitespace(1);
-            return tok;
-        }
-
-        // regexp
-        sync ();
-        if (! (match = tok.exec(current))) {
-            return null;
-        }
-
-        length = match[0].length;
-
-        // The match is confirmed, add the match length to `i`,
-        // and consume any extra white-space characters (' ' || '\n')
-        // which come after that. The reason for this is that LeSS's
-        // grammar is mostly white-space insensitive.
-        //
-        skipWhitespace(length);
-
-        if(typeof(match) === 'string') {
-            return match;
-        } else {
-            return match.length === 1 ? match[0] : match;
-        }
-    }
-
-    // Specialization of $(tok)
-    function $re(tok) {
-        if (i > currentPos) {
-            current = current.slice(i - currentPos);
-            currentPos = i;
-        }
-        var m = tok.exec(current);
-        if (!m) {
-            return null;
-        }
-
-        skipWhitespace(m[0].length);
-        if(typeof m === "string") {
-            return m;
-        }
-
-        return m.length === 1 ? m[0] : m;
-    }
-
-    // Specialization of $(tok)
-    function $char(tok) {
-        if (input.charAt(i) !== tok) {
-            return null;
-        }
-        skipWhitespace(1);
-        return tok;
-    }
-
-    var CHARCODE_SPACE = 32,
-        CHARCODE_TAB = 9,
-        CHARCODE_LF = 10,
-        CHARCODE_CR = 13,
-        CHARCODE_FORWARD_SLASH = 47;
-
-    var autoCommentAbsorb = true,
-        commentStore = [];
-
-    function skipWhitespace(length) {
-        var oldi = i, oldj = j,
-            curr = i - currentPos,
-            endIndex = i + current.length - curr,
-            mem = (i += length),
-            inp = input,
-            c, nextChar, comment;
-
-        for (; i < endIndex; i++) {
-            c = inp.charCodeAt(i);
-
-            if (autoCommentAbsorb && c === CHARCODE_FORWARD_SLASH) {
-                nextChar = inp[i + 1];
-                if (nextChar === '/') {
-                    comment = {index: i, isLineComment: true};
-                    var nextNewLine = inp.indexOf("\n", i + 1);
-                    if (nextNewLine < 0) {
-                        nextNewLine = endIndex;
-                    }
-                    i = nextNewLine;
-                    comment.text = inp.substr(comment.i, i - comment.i);
-                    commentStore.push(comment);
-                    continue;
-                } else if (nextChar === '*') {
-                    var haystack = inp.substr(i);
-                    var comment_search_result = haystack.match(/^\/\*(?:[^*]|\*+[^\/*])*\*+\//);
-                    if (comment_search_result) {
-                        comment = {
-                            index: i,
-                            text: comment_search_result[0],
-                            isLineComment: false
-                        };
-                        i += comment.text.length - 1;
-                        commentStore.push(comment);
-                        continue;
-                    }
-                }
-                break;
-            }
-
-            if ((c !== CHARCODE_SPACE) && (c !== CHARCODE_LF) && (c !== CHARCODE_TAB) && (c !== CHARCODE_CR)) {
-                break;
-            }
-         }
-
-        current = current.slice(length + i - mem + curr);
-        currentPos = i;
-
-        if (!current.length && (j < chunks.length - 1)) {
-            current = chunks[++j];
-            skipWhitespace(0); // skip space at the beginning of a chunk
-            return true; // things changed
-        }
-
-        return oldi !== i || oldj !== j;
-    }
-
     function expect(arg, msg, index) {
         // some older browsers return typeof 'function' for RegExp
-        var result = (Object.prototype.toString.call(arg) === '[object Function]') ? arg.call(parsers) : $(arg);
+        var result = (Object.prototype.toString.call(arg) === '[object Function]') ? arg.call(parsers) : parserInput.$(arg);
         if (result) {
             return result;
         }
-        error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'"
+        error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + parserInput.currentChar() + "'"
                                                : "unexpected token"));
     }
 
     // Specialization of expect()
     function expectChar(arg, msg) {
-        if (input.charAt(i) === arg) {
-            skipWhitespace(1);
+        if (parserInput.$char(arg)) {
             return arg;
         }
-        error(msg || "expected '" + arg + "' got '" + input.charAt(i) + "'");
+        error(msg || "expected '" + arg + "' got '" + parserInput.currentChar() + "'");
     }
 
     function error(msg, type) {
         var e = new Error(msg);
-        e.index = i;
+        e.index = parserInput.i;
         e.type = type || 'Syntax';
         throw e;
     }
 
-    // Same as $(), but don't change the state of the parser,
-    // just return the match.
-    function peek(tok) {
-        if (typeof(tok) === 'string') {
-            return input.charAt(i) === tok;
-        } else {
-            return tok.test(current);
-        }
-    }
-
-    // Specialization of peek()
-    function peekChar(tok) {
-        return input.charAt(i) === tok;
-    }
-
-
     function getInput(e, env) {
         if (e.filename && env.currentFileInfo.filename && (e.filename !== env.currentFileInfo.filename)) {
             return parser.imports.contents[e.filename];
         } else {
-            return input;
-        }
-    }
-
-    function getLocation(index, inputStream) {
-        var n = index + 1,
-            line = null,
-            column = -1;
-
-        while (--n >= 0 && inputStream.charAt(n) !== '\n') {
-            column++;
+            return parserInput.getInput();
         }
-
-        if (typeof index === 'number') {
-            line = (inputStream.slice(0, index).match(/\n/g) || "").length;
-        }
-
-        return {
-            line: line,
-            column: column
-        };
     }
 
-    function getDebugInfo(index, inputStream, env) {
+    function getDebugInfo(index) {
         var filename = env.currentFileInfo.filename;
         filename = less.environment.getAbsolutePath(env, filename);
 
         return {
-            lineNumber: getLocation(index, inputStream).line + 1,
+            lineNumber: parserInput.getLocation(index).line + 1,
             fileName: filename
         };
     }
@@ -315,9 +109,7 @@ var Parser = function Parser(env) {
         // @param [additionalData] An optional map which can contains vars - a map (key, value) of variables to apply
         //
         parse: function (str, callback, additionalData) {
-            var root, line, lines, error = null, globalVars, modifyVars, preText = "";
-
-            i = j = currentPos = furthest = 0;
+            var root, error = null, globalVars, modifyVars, preText = "";
 
             globalVars = (additionalData && additionalData.globalVars) ? less.Parser.serializeVars(additionalData.globalVars) + '\n' : '';
             modifyVars = (additionalData && additionalData.modifyVars) ? '\n' + less.Parser.serializeVars(additionalData.modifyVars) : '';
@@ -329,44 +121,16 @@ var Parser = function Parser(env) {
 
             str = str.replace(/\r\n/g, '\n');
             // Remove potential UTF Byte Order Mark
-            input = str = preText + str.replace(/^\uFEFF/, '') + modifyVars;
+            str = preText + str.replace(/^\uFEFF/, '') + modifyVars;
             parser.imports.contents[env.currentFileInfo.filename] = str;
 
-            // chunking apparantly makes things quicker (but my tests indicate
-            // it might actually make things slower in node at least)
-            // and it is a non-perfect parse - it can't recognise
-            // unquoted urls, meaning it can't distinguish comments
-            // meaning comments with quotes or {}() in them get 'counted'
-            // and then lead to parse errors.
-            // In addition if the chunking chunks in the wrong place we might
-            // not be able to parse a parser statement in one go
-            // this is officially deprecated but can be switched on via an option
-            // in the case it causes too much performance issues.
-            if (env.chunkInput) {
-                try {
-                    chunks = chunker(str, function fail(msg, index) {
-                        throw new(LessError)(parser, {
-                            index: index,
-                            type: 'Parse',
-                            message: msg,
-                            filename: env.currentFileInfo.filename
-                        }, env);
-                    });
-                } catch (ex) {
-                    return callback(new LessError(parser, ex, env));
-                }
-            } else {
-                chunks = [str];
-            }
-
-            current = chunks[0];
-
             // Start with the primary rule.
             // The whole syntax tree is held under a Ruleset node,
             // with the `root` property set to true, so no `{}` are
             // output. The callback is called when the input is parsed.
             try {
-                skipWhitespace(0);
+                parserInput.start(str, env.chunkInput, parser, env);
+
                 root = new(tree.Ruleset)(null, this.parsers.primary());
                 root.root = true;
                 root.firstRoot = true;
@@ -497,41 +261,28 @@ var Parser = function Parser(env) {
             // showing the line where the parse error occurred.
             // We split it up into two parts (the part which parsed,
             // and the part which didn't), so we can color them differently.
-            if (i < input.length - 1) {
+            var endInfo = parserInput.end();
+            if (!endInfo.isFinished) {
 
-                var message = "Unrecognised input";
-                if (i < furthest) {
-                    message = furthestPossibleErrorMessage || message;
-                    i = furthest;
-                }
+                var message = endInfo.furthestPossibleErrorMessage;
 
-                if (!furthestPossibleErrorMessage) {
-                    if (input[i] === '}') {
+                if (!message) {
+                    message = "Unrecognised input";
+                    if (endInfo.furthestChar === '}') {
                         message += ". Possibly missing opening '{'";
-                    } else if (input[i] === ')') {
+                    } else if (endInfo.furthestChar === ')') {
                         message += ". Possibly missing opening '('";
-                    } else if (i >= input.length - 1) {
+                    } else if (endInfo.furthestReachedEnd) {
                         message += ". Possibly missing something";
                     }
                 }
 
-                var loc = getLocation(i, input);
-                lines = input.split('\n');
-                line = loc.line + 1;
-
-                error = {
+                error = new LessError(parser, {
                     type: "Parse",
                     message: message,
-                    index: i,
-                    filename: env.currentFileInfo.filename,
-                    line: line,
-                    column: loc.column,
-                    extract: [
-                        lines[line - 2],
-                        lines[line - 1],
-                        lines[line]
-                    ]
-                };
+                    index: endInfo.furthest,
+                    filename: env.currentFileInfo.filename
+                }, env);
             }
 
             var finish = function (e) {
@@ -605,14 +356,14 @@ var Parser = function Parser(env) {
             primary: function () {
                 var mixin = this.mixin, root = [], node;
 
-                while (current)
+                while (!parserInput.finished)
                 {
                     while(true) {
                         node = this.comment();
                         if (!node) { break; }
                         root.push(node);
                     }
-                    if (peekChar('}')) {
+                    if (parserInput.peek('}')) {
                         break;
                     }
                     node = this.extendRule() || mixin.definition() || this.rule() || this.ruleset() ||
@@ -620,7 +371,7 @@ var Parser = function Parser(env) {
                     if (node) {
                         root.push(node);
                     } else {
-                        if (!($re(/^[\s\n]+/) || $re(/^;+/))) {
+                        if (!(parserInput.$re(/^[\s\n]+/) || parserInput.$re(/^;+/))) {
                             break;
                         }
                     }
@@ -632,8 +383,8 @@ var Parser = function Parser(env) {
             // comments are collected by the main parsing mechanism and then assigned to nodes
             // where the current structure allows it
             comment: function () {
-                if (commentStore.length) {
-                    var comment = commentStore.shift();
+                if (parserInput.commentStore.length) {
+                    var comment = parserInput.commentStore.shift();
                     return new(tree.Comment)(comment.text, comment.isLineComment, comment.index, env.currentFileInfo);
                 }
             },
@@ -648,16 +399,11 @@ var Parser = function Parser(env) {
                 //     "milky way" 'he\'s the one!'
                 //
                 quoted: function () {
-                    var str, j = i, e, index = i;
+                    var str, index = parserInput.i;
 
-                    if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings
-                    if (input.charAt(j) !== '"' && input.charAt(j) !== "'") { return; }
-
-                    if (e) { $char('~'); }
-
-                    str = $re(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/);
+                    str = parserInput.$re(/^(~)?("((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)')/);
                     if (str) {
-                        return new(tree.Quoted)(str[0], str[1] || str[2], e, index, env.currentFileInfo);
+                        return new(tree.Quoted)(str[2], str[3] || str[4], Boolean(str[1]), index, env.currentFileInfo);
                     }
                 },
 
@@ -667,7 +413,7 @@ var Parser = function Parser(env) {
                 //     black border-collapse
                 //
                 keyword: function () {
-                    var k = $re(/^%|^[_A-Za-z-][_A-Za-z0-9-]*/);
+                    var k = parserInput.$re(/^%|^[_A-Za-z-][_A-Za-z0-9-]*/);
                     if (k) {
                         return tree.Color.fromKeyword(k) || new(tree.Keyword)(k);
                     }
@@ -684,35 +430,36 @@ var Parser = function Parser(env) {
                 // The arguments are parsed with the `entities.arguments` parser.
                 //
                 call: function () {
-                    var name, nameLC, args, alpha_ret, index = i;
+                    var name, nameLC, args, alpha, index = parserInput.i;
 
-                    name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(current);
-                    if (!name) { return; }
+                    if (parserInput.peek(/^url\(/i)) {
+                        return;
+                    }
+
+                    parserInput.save();
+
+                    name = parserInput.$re(/^([\w-]+|%|progid:[\w\.]+)\(/);
+                    if (!name) { parserInput.forget(); return; }
 
                     name = name[1];
                     nameLC = name.toLowerCase();
-                    if (nameLC === 'url') {
-                        return null;
-                    }
-
-                    i += name.length;
 
                     if (nameLC === 'alpha') {
-                        alpha_ret = parsers.alpha();
-                        if(typeof alpha_ret !== 'undefined') {
-                            return alpha_ret;
+                        alpha = parsers.alpha();
+                        if(alpha) {
+                            return alpha;
                         }
                     }
 
-                    $char('('); // Parse the '(' and consume whitespace.
-
                     args = this.arguments();
 
-                    if (! $char(')')) {
+                    if (! parserInput.$char(')')) {
+                        parserInput.restore("Could not parse call arguments or missing ')'");
                         return;
                     }
 
-                    if (name) { return new(tree.Call)(name, args, index, env.currentFileInfo); }
+                    parserInput.forget();
+                    return new(tree.Call)(name, args, index, env.currentFileInfo);
                 },
                 arguments: function () {
                     var args = [], arg;
@@ -723,7 +470,7 @@ var Parser = function Parser(env) {
                             break;
                         }
                         args.push(arg);
-                        if (! $char(',')) {
+                        if (! parserInput.$char(',')) {
                             break;
                         }
                     }
@@ -744,11 +491,11 @@ var Parser = function Parser(env) {
 
                 assignment: function () {
                     var key, value;
-                    key = $re(/^\w+(?=\s?=)/i);
+                    key = parserInput.$re(/^\w+(?=\s?=)/i);
                     if (!key) {
                         return;
                     }
-                    if (!$char('=')) {
+                    if (!parserInput.$char('=')) {
                         return;
                     }
                     value = parsers.entity();
@@ -767,16 +514,16 @@ var Parser = function Parser(env) {
                 url: function () {
                     var value;
 
-                    if (input.charAt(i) !== 'u' || !$re(/^url\(/)) {
+                    if (parserInput.currentChar() !== 'u' || !parserInput.$re(/^url\(/)) {
                         return;
                     }
 
-                    autoCommentAbsorb = false;
+                    parserInput.autoCommentAbsorb = false;
 
                     value = this.quoted() || this.variable() ||
-                            $re(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/) || "";
+                            parserInput.$re(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/) || "";
 
-                    autoCommentAbsorb = true;
+                    parserInput.autoCommentAbsorb = true;
 
                     expectChar(')');
 
@@ -793,18 +540,18 @@ var Parser = function Parser(env) {
                 // see `parsers.variable`.
                 //
                 variable: function () {
-                    var name, index = i;
+                    var name, index = parserInput.i;
 
-                    if (input.charAt(i) === '@' && (name = $re(/^@@?[\w-]+/))) {
+                    if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^@@?[\w-]+/))) {
                         return new(tree.Variable)(name, index, env.currentFileInfo);
                     }
                 },
 
                 // A variable entity useing the protective {} e.g. @{var}
                 variableCurly: function () {
-                    var curly, index = i;
+                    var curly, index = parserInput.i;
 
-                    if (input.charAt(i) === '@' && (curly = $re(/^@\{([\w-]+)\}/))) {
+                    if (parserInput.currentChar() === '@' && (curly = parserInput.$re(/^@\{([\w-]+)\}/))) {
                         return new(tree.Variable)("@" + curly[1], index, env.currentFileInfo);
                     }
                 },
@@ -819,7 +566,7 @@ var Parser = function Parser(env) {
                 color: function () {
                     var rgb;
 
-                    if (input.charAt(i) === '#' && (rgb = $re(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) {
+                    if (parserInput.currentChar() === '#' && (rgb = parserInput.$re(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) {
                         var colorCandidateString = rgb.input.match(/^#([\w]+).*/); // strip colons, brackets, whitespaces and other characters that should not definitely be part of color string
                         colorCandidateString = colorCandidateString[1];
                         if (!colorCandidateString.match(/^[A-Fa-f0-9]+$/)) { // verify if candidate consists only of allowed HEX characters
@@ -835,13 +582,11 @@ var Parser = function Parser(env) {
                 //     0.5em 95%
                 //
                 dimension: function () {
-                    var value, c = input.charCodeAt(i);
-                    //Is the first char of the dimension 0-9, '.', '+' or '-'
-                    if ((c > 57 || c < 43) || c === 47 || c == 44) {
+                    if (parserInput.peekNotNumeric()) {
                         return;
                     }
 
-                    value = $re(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/);
+                    var value = parserInput.$re(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/);
                     if (value) {
                         return new(tree.Dimension)(value[1], value[2]);
                     }
@@ -855,7 +600,7 @@ var Parser = function Parser(env) {
                 unicodeDescriptor: function () {
                     var ud;
 
-                    ud = $re(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/);
+                    ud = parserInput.$re(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/);
                     if (ud) {
                         return new(tree.UnicodeDescriptor)(ud[0]);
                     }
@@ -867,19 +612,11 @@ var Parser = function Parser(env) {
                 //     `window.location.href`
                 //
                 javascript: function () {
-                    var str, j = i, e;
+                    var js, index = parserInput.i;
 
-                    if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings
-                    if (input.charAt(j) !== '`') { return; }
-                    if (env.javascriptEnabled !== undefined && !env.javascriptEnabled) {
-                        error("You are using JavaScript, which has been disabled.");
-                    }
-
-                    if (e) { $char('~'); }
-
-                    str = $re(/^`([^`]*)`/);
-                    if (str) {
-                        return new(tree.JavaScript)(str[1], i, e);
+                    js = parserInput.$re(/^(~)?`([^`]*)`/);
+                    if (js) {
+                        return new(tree.JavaScript)(js[2], index, Boolean(js[1]));
                     }
                 }
             },
@@ -892,7 +629,7 @@ var Parser = function Parser(env) {
             variable: function () {
                 var name;
 
-                if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*:/))) { return name[1]; }
+                if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^(@[\w-]+)\s*:/))) { return name[1]; }
             },
 
             //
@@ -903,7 +640,7 @@ var Parser = function Parser(env) {
             rulesetCall: function () {
                 var name;
 
-                if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*\(\s*\)\s*;/))) {
+                if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^(@[\w-]+)\s*\(\s*\)\s*;/))) {
                     return new tree.RulesetCall(name[1]);
                 }
             },
@@ -912,14 +649,14 @@ var Parser = function Parser(env) {
             // extend syntax - used to extend selectors
             //
             extend: function(isRule) {
-                var elements, e, index = i, option, extendList, extend;
+                var elements, e, index = parserInput.i, option, extendList, extend;
 
-                if (!(isRule ? $re(/^&:extend\(/) : $re(/^:extend\(/))) { return; }
+                if (!(isRule ? parserInput.$re(/^&:extend\(/) : parserInput.$re(/^:extend\(/))) { return; }
 
                 do {
                     option = null;
                     elements = null;
-                    while (! (option = $re(/^(all)(?=\s*(\)|,))/))) {
+                    while (! (option = parserInput.$re(/^(all)(?=\s*(\)|,))/))) {
                         e = this.element();
                         if (!e) { break; }
                         if (elements) { elements.push(e); } else { elements = [ e ]; }
@@ -931,7 +668,7 @@ var Parser = function Parser(env) {
                     extend = new(tree.Extend)(new(tree.Selector)(elements), option, index);
                     if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; }
 
-                } while($char(","));
+                } while(parserInput.$char(","));
 
                 expect(/^\)/);
 
@@ -965,26 +702,26 @@ var Parser = function Parser(env) {
                 // selector for now.
                 //
                 call: function () {
-                    var s = input.charAt(i), important = false, index = i, elemIndex,
+                    var s = parserInput.currentChar(), important = false, index = parserInput.i, elemIndex,
                         elements, elem, e, c, args;
 
                     if (s !== '.' && s !== '#') { return; }
 
-                    save(); // stop us absorbing part of an invalid selector
+                    parserInput.save(); // stop us absorbing part of an invalid selector
 
                     while (true) {
-                        elemIndex = i;
-                        e = $re(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/);
+                        elemIndex = parserInput.i;
+                        e = parserInput.$re(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/);
                         if (!e) {
                             break;
                         }
                         elem = new(tree.Element)(c, e, elemIndex, env.currentFileInfo);
                         if (elements) { elements.push(elem); } else { elements = [ elem ]; }
-                        c = $char('>');
+                        c = parserInput.$char('>');
                     }
 
                     if (elements) {
-                        if ($char('(')) {
+                        if (parserInput.$char('(')) {
                             args = this.args(true).args;
                             expectChar(')');
                         }
@@ -994,12 +731,12 @@ var Parser = function Parser(env) {
                         }
 
                         if (parsers.end()) {
-                            forget();
+                            parserInput.forget();
                             return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important);
                         }
                     }
 
-                    restore();
+                    parserInput.restore();
                 },
                 args: function (isCall) {
                     var parsers = parser.parsers, entities = parsers.entities,
@@ -1007,16 +744,16 @@ var Parser = function Parser(env) {
                         expressions = [], argsSemiColon = [], argsComma = [],
                         isSemiColonSeperated, expressionContainsNamed, name, nameLoop, value, arg;
 
-                    save();
+                    parserInput.save();
 
                     while (true) {
                         if (isCall) {
                             arg = parsers.detachedRuleset() || parsers.expression();
                         } else {
-                            commentStore.length = 0;
-                            if (input.charAt(i) === '.' && $re(/^\.{3}/)) {
+                            parserInput.commentStore.length = 0;
+                            if (parserInput.currentChar() === '.' && parserInput.$re(/^\.{3}/)) {
                                 returner.variadic = true;
-                                if ($char(";") && !isSemiColonSeperated) {
+                                if (parserInput.$char(";") && !isSemiColonSeperated) {
                                     isSemiColonSeperated = true;
                                 }
                                 (isSemiColonSeperated ? argsSemiColon : argsComma)
@@ -1047,7 +784,7 @@ var Parser = function Parser(env) {
                         }
 
                         if (val && val instanceof tree.Variable) {
-                            if ($char(':')) {
+                            if (parserInput.$char(':')) {
                                 if (expressions.length > 0) {
                                     if (isSemiColonSeperated) {
                                         error("Cannot mix ; and , as delimiter types");
@@ -1064,15 +801,15 @@ var Parser = function Parser(env) {
                                     if (isCall) {
                                         error("could not understand value for named argument");
                                     } else {
-                                        restore();
+                                        parserInput.restore();
                                         returner.args = [];
                                         return returner;
                                     }
                                 }
                                 nameLoop = (name = val.name);
-                            } else if (!isCall && $re(/^\.{3}/)) {
+                            } else if (!isCall && parserInput.$re(/^\.{3}/)) {
                                 returner.variadic = true;
-                                if ($char(";") && !isSemiColonSeperated) {
+                                if (parserInput.$char(";") && !isSemiColonSeperated) {
                                     isSemiColonSeperated = true;
                                 }
                                 (isSemiColonSeperated ? argsSemiColon : argsComma)
@@ -1090,11 +827,11 @@ var Parser = function Parser(env) {
 
                         argsComma.push({ name:nameLoop, value:value });
 
-                        if ($char(',')) {
+                        if (parserInput.$char(',')) {
                             continue;
                         }
 
-                        if ($char(';') || isSemiColonSeperated) {
+                        if (parserInput.$char(';') || isSemiColonSeperated) {
 
                             if (expressionContainsNamed) {
                                 error("Cannot mix ; and , as delimiter types");
@@ -1113,7 +850,7 @@ var Parser = function Parser(env) {
                         }
                     }
 
-                    forget();
+                    parserInput.forget();
                     returner.args = isSemiColonSeperated ? argsSemiColon : argsComma;
                     return returner;
                 },
@@ -1138,14 +875,14 @@ var Parser = function Parser(env) {
                 //
                 definition: function () {
                     var name, params = [], match, ruleset, cond, variadic = false;
-                    if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') ||
-                        peek(/^[^{]*\}/)) {
+                    if ((parserInput.currentChar() !== '.' && parserInput.currentChar() !== '#') ||
+                        parserInput.peek(/^[^{]*\}/)) {
                         return;
                     }
 
-                    save();
+                    parserInput.save();
 
-                    match = $re(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/);
+                    match = parserInput.$re(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/);
                     if (match) {
                         name = match[1];
 
@@ -1158,27 +895,27 @@ var Parser = function Parser(env) {
                         // also
                         // .mixincall(@a: {rule: set;});
                         // so we have to be nice and restore
-                        if (!$char(')')) {
-                            restore("Missing closing ')'");
+                        if (!parserInput.$char(')')) {
+                            parserInput.restore("Missing closing ')'");
                             return;
                         }
 
-                        commentStore.length = 0;
+                        parserInput.commentStore.length = 0;
 
-                        if ($re(/^when/)) { // Guard
+                        if (parserInput.$re(/^when/)) { // Guard
                             cond = expect(parsers.conditions, 'expected condition');
                         }
 
                         ruleset = parsers.block();
 
                         if (ruleset) {
-                            forget();
+                            parserInput.forget();
                             return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);
                         } else {
-                            restore();
+                            parserInput.restore();
                         }
                     } else {
-                        forget();
+                        parserInput.forget();
                     }
                 }
             },
@@ -1200,7 +937,7 @@ var Parser = function Parser(env) {
             // it's there, if ';' was ommitted.
             //
             end: function () {
-                return $char(';') || peekChar('}');
+                return parserInput.$char(';') || parserInput.peek('}');
             },
 
             //
@@ -1211,12 +948,13 @@ var Parser = function Parser(env) {
             alpha: function () {
                 var value;
 
-                if (! $re(/^\(opacity=/i)) { return; }
-                value = $re(/^\d+/) || this.entities.variable();
-                if (value) {
-                    expectChar(')');
-                    return new(tree.Alpha)(value);
+                if (! parserInput.$re(/^opacity=/i)) { return; }
+                value = parserInput.$re(/^\d+/);
+                if (!value) {
+                    value = expect(this.entities.variable, "Could not parse alpha");
                 }
+                expectChar(')');
+                return new(tree.Alpha)(value);
             },
 
             //
@@ -1232,25 +970,25 @@ var Parser = function Parser(env) {
             // and an element name, such as a tag a class, or `*`.
             //
             element: function () {
-                var e, c, v, index = i;
+                var e, c, v, index = parserInput.i;
 
                 c = this.combinator();
 
-                e = $re(/^(?:\d+\.\d+|\d+)%/) || $re(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) ||
-                    $char('*') || $char('&') || this.attribute() || $re(/^\([^()@]+\)/) || $re(/^[\.#](?=@)/) ||
+                e = parserInput.$re(/^(?:\d+\.\d+|\d+)%/) || parserInput.$re(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) ||
+                    parserInput.$char('*') || parserInput.$char('&') || this.attribute() || parserInput.$re(/^\([^()@]+\)/) || parserInput.$re(/^[\.#](?=@)/) ||
                     this.entities.variableCurly();
 
                 if (! e) {
-                    save();
-                    if ($char('(')) {
-                        if ((v = this.selector()) && $char(')')) {
+                    parserInput.save();
+                    if (parserInput.$char('(')) {
+                        if ((v = this.selector()) && parserInput.$char(')')) {
                             e = new(tree.Paren)(v);
-                            forget();
+                            parserInput.forget();
                         } else {
-                            restore("Missing closing ')'");
+                            parserInput.restore("Missing closing ')'");
                         }
                     } else {
-                        forget();
+                        parserInput.forget();
                     }
                 }
 
@@ -1267,27 +1005,27 @@ var Parser = function Parser(env) {
             // we deal with this in *combinator.js*.
             //
             combinator: function () {
-                var c = input.charAt(i);
+                var c = parserInput.currentChar();
 
                 if (c === '/') {
-                    save();
-                    var slashedCombinator = $re(/^\/[a-z]+\//i);
+                    parserInput.save();
+                    var slashedCombinator = parserInput.$re(/^\/[a-z]+\//i);
                     if (slashedCombinator) {
-                        forget();
+                        parserInput.forget();
                         return new(tree.Combinator)(slashedCombinator);
                     }
-                    restore();
+                    parserInput.restore();
                 }
 
                 if (c === '>' || c === '+' || c === '~' || c === '|' || c === '^') {
-                    i++;
-                    if (c === '^' && input.charAt(i) === '^') {
+                    parserInput.i++;
+                    if (c === '^' && parserInput.currentChar() === '^') {
                         c = '^^';
-                        i++;
+                        parserInput.i++;
                     }
-                    while (isWhitespace(input, i)) { i++; }
+                    while (parserInput.isWhitespace()) { parserInput.i++; }
                     return new(tree.Combinator)(c);
-                } else if (isWhitespace(input, i - 1)) {
+                } else if (parserInput.isWhitespace(-1)) {
                     return new(tree.Combinator)(" ");
                 } else {
                     return new(tree.Combinator)(null);
@@ -1309,9 +1047,9 @@ var Parser = function Parser(env) {
             // Selectors are made out of one or more Elements, see above.
             //
             selector: function (isLess) {
-                var index = i, elements, extendList, c, e, extend, when, condition;
+                var index = parserInput.i, elements, extendList, c, e, extend, when, condition;
 
-                while ((isLess && (extend = this.extend())) || (isLess && (when = $re(/^when/))) || (e = this.element())) {
+                while ((isLess && (extend = this.extend())) || (isLess && (when = parserInput.$re(/^when/))) || (e = this.element())) {
                     if (when) {
                         condition = expect(this.conditions, 'expected condition');
                     } else if (condition) {
@@ -1320,7 +1058,7 @@ var Parser = function Parser(env) {
                         if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; }
                     } else {
                         if (extendList) { error("Extend can only be used at the end of selector"); }
-                        c = input.charAt(i);
+                        c = parserInput.currentChar();
                         if (elements) { elements.push(e); } else { elements = [ e ]; }
                         e = null;
                     }
@@ -1333,7 +1071,7 @@ var Parser = function Parser(env) {
                 if (extendList) { error("Extend must be used to extend a selector, it cannot be used on its own"); }
             },
             attribute: function () {
-                if (! $char('[')) { return; }
+                if (! parserInput.$char('[')) { return; }
 
                 var entities = this.entities,
                     key, val, op;
@@ -1342,9 +1080,9 @@ var Parser = function Parser(env) {
                     key = expect(/^(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/);
                 }
 
-                op = $re(/^[|~*$^]?=/);
+                op = parserInput.$re(/^[|~*$^]?=/);
                 if (op) {
-                    val = entities.quoted() || $re(/^[0-9]+%/) || $re(/^[\w-]+/) || entities.variableCurly();
+                    val = entities.quoted() || parserInput.$re(/^[0-9]+%/) || parserInput.$re(/^[\w-]+/) || entities.variableCurly();
                 }
 
                 expectChar(']');
@@ -1358,7 +1096,7 @@ var Parser = function Parser(env) {
             //
             block: function () {
                 var content;
-                if ($char('{') && (content = this.primary()) && $char('}')) {
+                if (parserInput.$char('{') && (content = this.primary()) && parserInput.$char('}')) {
                     return content;
                 }
             },
@@ -1385,10 +1123,10 @@ var Parser = function Parser(env) {
             ruleset: function () {
                 var selectors, s, rules, debugInfo;
 
-                save();
+                parserInput.save();
 
                 if (env.dumpLineNumbers) {
-                    debugInfo = getDebugInfo(i, input, env);
+                    debugInfo = getDebugInfo(parserInput.i);
                 }
 
                 while (true) {
@@ -1397,34 +1135,34 @@ var Parser = function Parser(env) {
                         break;
                     }
                     if (selectors) { selectors.push(s); } else { selectors = [ s ]; }
-                    commentStore.length = 0;
+                    parserInput.commentStore.length = 0;
                     if (s.condition && selectors.length > 1) {
                         error("Guards are only currently allowed on a single selector.");
                     }
-                    if (! $char(',')) { break; }
+                    if (! parserInput.$char(',')) { break; }
                     if (s.condition) {
                         error("Guards are only currently allowed on a single selector.");
                     }
-                    commentStore.length = 0;
+                    parserInput.commentStore.length = 0;
                 }
 
                 if (selectors && (rules = this.block())) {
-                    forget();
+                    parserInput.forget();
                     var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports);
                     if (env.dumpLineNumbers) {
                         ruleset.debugInfo = debugInfo;
                     }
                     return ruleset;
                 } else {
-                    restore();
+                    parserInput.restore();
                 }
             },
             rule: function (tryAnonymous) {
-                var name, value, startOfRule = i, c = input.charAt(startOfRule), important, merge, isVariable;
+                var name, value, startOfRule = parserInput.i, c = parserInput.currentChar(), important, merge, isVariable;
 
                 if (c === '.' || c === '#' || c === '&') { return; }
 
-                save();
+                parserInput.save();
 
                 name = this.variable() || this.ruleProperty();
                 if (name) {
@@ -1435,38 +1173,49 @@ var Parser = function Parser(env) {
                     }
 
                     if (!value) {
-                        // prefer to try to parse first if its a variable or we are compressing
-                        // but always fallback on the other one
-                        value = !tryAnonymous && (env.compress || isVariable) ?
-                            (this.value() || this.anonymousValue()) :
-                            (this.anonymousValue() || this.value());
-
-                        important = this.important();
-
                         // a name returned by this.ruleProperty() is always an array of the form:
                         // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
                         // where each item is a tree.Keyword or tree.Variable
                         merge = !isVariable && name.pop().value;
+
+                        // prefer to try to parse first if its a variable or we are compressing
+                        // but always fallback on the other one
+                        var tryValueFirst = !tryAnonymous && (env.compress || isVariable);
+
+                        if (tryValueFirst) {
+                            value = this.value();
+                        }
+                        if (!value) {
+                            value = this.anonymousValue();
+                            if (value) {
+                                parserInput.forget();
+                                // anonymous values absorb the end ';' which is reequired for them to work
+                                return new (tree.Rule)(name, value, false, merge, startOfRule, env.currentFileInfo);
+                            }
+                        }
+                        if (!tryValueFirst && !value) {
+                            value = this.value();
+                        }
+
+                        important = this.important();
                     }
 
                     if (value && this.end()) {
-                        forget();
+                        parserInput.forget();
                         return new (tree.Rule)(name, value, important, merge, startOfRule, env.currentFileInfo);
                     } else {
-                        restore();
+                        parserInput.restore();
                         if (value && !tryAnonymous) {
                             return this.rule(true);
                         }
                     }
                 } else {
-                    forget();
+                    parserInput.forget();
                 }
             },
             anonymousValue: function () {
-                var match;
-                match = /^([^@+\/'"*`(;{}-]*);/.exec(current);
+                var match = parserInput.$re(/^([^@+\/'"*`(;{}-]*);/);
                 if (match) {
-                    i += match[0].length - 1;
                     return new(tree.Anonymous)(match[1]);
                 }
             },
@@ -1482,9 +1231,9 @@ var Parser = function Parser(env) {
             // stored in `import`, which we pass to the Import constructor.
             //
             "import": function () {
-                var path, features, index = i;
+                var path, features, index = parserInput.i;
 
-                var dir = $re(/^@import?\s+/);
+                var dir = parserInput.$re(/^@import?\s+/);
 
                 if (dir) {
                     var options = (dir ? this.importOptions() : null) || {};
@@ -1492,8 +1241,8 @@ var Parser = function Parser(env) {
                     if ((path = this.entities.quoted() || this.entities.url())) {
                         features = this.mediaFeatures();
 
-                        if (!$(';')) {
-                            i = index;
+                        if (!parserInput.$(';')) {
+                            parserInput.i = index;
                             error("missing semi-colon or unrecognised media features on import");
                         }
                         features = features && new(tree.Value)(features);
@@ -1501,7 +1250,7 @@ var Parser = function Parser(env) {
                     }
                     else
                     {
-                        i = index;
+                        parserInput.i = index;
                         error("malformed import statement");
                     }
                 }
@@ -1511,7 +1260,7 @@ var Parser = function Parser(env) {
                 var o, options = {}, optionName, value;
 
                 // list of options, surrounded by parens
-                if (! $char('(')) { return null; }
+                if (! parserInput.$char('(')) { return null; }
                 do {
                     o = this.importOption();
                     if (o) {
@@ -1528,7 +1277,7 @@ var Parser = function Parser(env) {
                             break;
                         }
                         options[optionName] = value;
-                        if (! $char(',')) { break; }
+                        if (! parserInput.$char(',')) { break; }
                     }
                 } while (o);
                 expectChar(')');
@@ -1536,7 +1285,7 @@ var Parser = function Parser(env) {
             },
 
             importOption: function() {
-                var opt = $re(/^(less|css|multiple|once|inline|reference)/);
+                var opt = parserInput.$re(/^(less|css|multiple|once|inline|reference)/);
                 if (opt) {
                     return opt[1];
                 }
@@ -1544,31 +1293,31 @@ var Parser = function Parser(env) {
 
             mediaFeature: function () {
                 var entities = this.entities, nodes = [], e, p;
-                save();
+                parserInput.save();
                 do {
                     e = entities.keyword() || entities.variable();
                     if (e) {
                         nodes.push(e);
-                    } else if ($char('(')) {
+                    } else if (parserInput.$char('(')) {
                         p = this.property();
                         e = this.value();
-                        if ($char(')')) {
+                        if (parserInput.$char(')')) {
                             if (p && e) {
-                                nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, null, i, env.currentFileInfo, true)));
+                                nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, null, parserInput.i, env.currentFileInfo, true)));
                             } else if (e) {
                                 nodes.push(new(tree.Paren)(e));
                             } else {
-                                restore("badly formed media feature definition");
+                                parserInput.restore("badly formed media feature definition");
                                 return null;
                             }
                         } else {
-                            restore("Missing closing ')'");
+                            parserInput.restore("Missing closing ')'");
                             return null;
                         }
                     }
                 } while (e);
 
-                forget();
+                parserInput.forget();
                 if (nodes.length > 0) {
                     return new(tree.Expression)(nodes);
                 }
@@ -1580,12 +1329,12 @@ var Parser = function Parser(env) {
                     e = this.mediaFeature();
                     if (e) {
                         features.push(e);
-                        if (! $char(',')) { break; }
+                        if (! parserInput.$char(',')) { break; }
                     } else {
                         e = entities.variable();
                         if (e) {
                             features.push(e);
-                            if (! $char(',')) { break; }
+                            if (! parserInput.$char(',')) { break; }
                         }
                     }
                 } while (e);
@@ -1597,15 +1346,15 @@ var Parser = function Parser(env) {
                 var features, rules, media, debugInfo;
 
                 if (env.dumpLineNumbers) {
-                    debugInfo = getDebugInfo(i, input, env);
+                    debugInfo = getDebugInfo(parserInput.i);
                 }
 
-                if ($re(/^@media/)) {
+                if (parserInput.$re(/^@media/)) {
                     features = this.mediaFeatures();
 
                     rules = this.block();
                     if (rules) {
-                        media = new(tree.Media)(rules, features, i, env.currentFileInfo);
+                        media = new(tree.Media)(rules, features, parserInput.i, env.currentFileInfo);
                         if (env.dumpLineNumbers) {
                             media.debugInfo = debugInfo;
                         }
@@ -1620,19 +1369,19 @@ var Parser = function Parser(env) {
             //     @charset "utf-8";
             //
             directive: function () {
-                var index = i, name, value, rules, nonVendorSpecificName,
+                var index = parserInput.i, name, value, rules, nonVendorSpecificName,
                     hasIdentifier, hasExpression, hasUnknown, hasBlock = true;
 
-                if (input.charAt(i) !== '@') { return; }
+                if (parserInput.currentChar() !== '@') { return; }
 
                 value = this['import']() || this.media();
                 if (value) {
                     return value;
                 }
 
-                save();
+                parserInput.save();
 
-                name = $re(/^@[a-z-]+/);
+                name = parserInput.$re(/^@[a-z-]+/);
 
                 if (!name) { return; }
 
@@ -1694,7 +1443,7 @@ var Parser = function Parser(env) {
                         error("expected " + name + " expression");
                     }
                 } else if (hasUnknown) {
-                    value = ($re(/^[^{;]+/) || '').trim();
+                    value = (parserInput.$re(/^[^{;]+/) || '').trim();
                     if (value) {
                         value = new(tree.Anonymous)(value);
                     }
@@ -1704,13 +1453,13 @@ var Parser = function Parser(env) {
                     rules = this.blockRuleset();
                 }
 
-                if (rules || (!hasBlock && value && $char(';'))) {
-                    forget();
+                if (rules || (!hasBlock && value && parserInput.$char(';'))) {
+                    parserInput.forget();
                     return new(tree.Directive)(name, value, rules, index, env.currentFileInfo,
-                        env.dumpLineNumbers ? getDebugInfo(index, input, env) : null);
+                        env.dumpLineNumbers ? getDebugInfo(index) : null);
                 }
 
-                restore("directive options not recognised");
+                parserInput.restore("directive options not recognised");
             },
 
             //
@@ -1728,7 +1477,7 @@ var Parser = function Parser(env) {
                     e = this.expression();
                     if (e) {
                         expressions.push(e);
-                        if (! $char(',')) { break; }
+                        if (! parserInput.$char(',')) { break; }
                     }
                 } while(e);
 
@@ -1737,14 +1486,14 @@ var Parser = function Parser(env) {
                 }
             },
             important: function () {
-                if (input.charAt(i) === '!') {
-                    return $re(/^! *important/);
+                if (parserInput.currentChar() === '!') {
+                    return parserInput.$re(/^! *important/);
                 }
             },
             sub: function () {
                 var a, e;
 
-                if ($char('(')) {
+                if (parserInput.$char('(')) {
                     a = this.addition();
                     if (a) {
                         e = new(tree.Expression)([a]);
@@ -1758,27 +1507,27 @@ var Parser = function Parser(env) {
                 var m, a, op, operation, isSpaced;
                 m = this.operand();
                 if (m) {
-                    isSpaced = isWhitespace(input, i - 1);
+                    isSpaced = parserInput.isWhitespace(-1);
                     while (true) {
-                        if (peek(/^\/[*\/]/)) {
+                        if (parserInput.peek(/^\/[*\/]/)) {
                             break;
                         }
 
-                        save();
+                        parserInput.save();
 
-                        op = $char('/') || $char('*');
+                        op = parserInput.$char('/') || parserInput.$char('*');
 
-                        if (!op) { forget(); break; }
+                        if (!op) { parserInput.forget(); break; }
 
                         a = this.operand();
 
-                        if (!a) { restore(); break; }
-                        forget();
+                        if (!a) { parserInput.restore(); break; }
+                        parserInput.forget();
 
                         m.parensInOp = true;
                         a.parensInOp = true;
                         operation = new(tree.Operation)(op, [operation || m, a], isSpaced);
-                        isSpaced = isWhitespace(input, i - 1);
+                        isSpaced = parserInput.isWhitespace(-1);
                     }
                     return operation || m;
                 }
@@ -1787,9 +1536,9 @@ var Parser = function Parser(env) {
                 var m, a, op, operation, isSpaced;
                 m = this.multiplication();
                 if (m) {
-                    isSpaced = isWhitespace(input, i - 1);
+                    isSpaced = parserInput.isWhitespace(-1);
                     while (true) {
-                        op = $re(/^[-+]\s+/) || (!isSpaced && ($char('+') || $char('-')));
+                        op = parserInput.$re(/^[-+]\s+/) || (!isSpaced && (parserInput.$char('+') || parserInput.$char('-')));
                         if (!op) {
                             break;
                         }
@@ -1801,18 +1550,18 @@ var Parser = function Parser(env) {
                         m.parensInOp = true;
                         a.parensInOp = true;
                         operation = new(tree.Operation)(op, [operation || m, a], isSpaced);
-                        isSpaced = isWhitespace(input, i - 1);
+                        isSpaced = parserInput.isWhitespace(-1);
                     }
                     return operation || m;
                 }
             },
             conditions: function () {
-                var a, b, index = i, condition;
+                var a, b, index = parserInput.i, condition;
 
                 a = this.condition();
                 if (a) {
                     while (true) {
-                        if (!peek(/^,\s*(not\s*)?\(/) || !$char(',')) {
+                        if (!parserInput.peek(/^,\s*(not\s*)?\(/) || !parserInput.$char(',')) {
                             break;
                         }
                         b = this.condition();
@@ -1825,14 +1574,14 @@ var Parser = function Parser(env) {
                 }
             },
             condition: function () {
-                var entities = this.entities, index = i, negate = false,
+                var entities = this.entities, index = parserInput.i, negate = false,
                     a, b, c, op;
 
-                if ($re(/^not/)) { negate = true; }
+                if (parserInput.$re(/^not/)) { negate = true; }
                 expectChar('(');
                 a = this.addition() || entities.keyword() || entities.quoted();
                 if (a) {
-                    op = $re(/^(?:>=|<=|=<|[<=>])/);
+                    op = parserInput.$re(/^(?:>=|<=|=<|[<=>])/);
                     if (op) {
                         b = this.addition() || entities.keyword() || entities.quoted();
                         if (b) {
@@ -1844,7 +1593,7 @@ var Parser = function Parser(env) {
                         c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate);
                     }
                     expectChar(')');
-                    return $re(/^and/) ? new(tree.Condition)('and', c, this.condition()) : c;
+                    return parserInput.$re(/^and/) ? new(tree.Condition)('and', c, this.condition()) : c;
                 }
             },
 
@@ -1853,10 +1602,12 @@ var Parser = function Parser(env) {
             // such as a Color, or a Variable
             //
             operand: function () {
-                var entities = this.entities,
-                    p = input.charAt(i + 1), negate;
+                var entities = this.entities, negate;
+
+                if (parserInput.peek(/^-[@\(]/)) {
+                    negate = parserInput.$char('-');
+                }
 
-                if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $char('-'); }
                 var o = this.sub() || entities.dimension() ||
                         entities.color() || entities.variable() ||
                         entities.call();
@@ -1889,8 +1640,8 @@ var Parser = function Parser(env) {
                     if (e) {
                         entities.push(e);
                         // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
-                        if (!peek(/^\/[\/*]/)) {
-                            delim = $char('/');
+                        if (!parserInput.peek(/^\/[\/*]/)) {
+                            delim = parserInput.$char('/');
                             if (delim) {
                                 entities.push(new(tree.Anonymous)(delim));
                             }
@@ -1902,30 +1653,37 @@ var Parser = function Parser(env) {
                 }
             },
             property: function () {
-                var name = $re(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/);
+                var name = parserInput.$re(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/);
                 if (name) {
                     return name[1];
                 }
             },
             ruleProperty: function () {
-                var c = current, name = [], index = [], length = 0, s, k;
+                var name = [], index = [], s, k;
+
+                parserInput.save();
 
                 function match(re) {
-                    var a = re.exec(c);
-                    if (a) {
-                        index.push(i + length);
-                        length += a[0].length;
-                        c = c.slice(a[1].length);
-                        return name.push(a[1]);
+                    var i = parserInput.i,
+                        chunk = parserInput.$re(re);
+                    if (chunk) {
+                        index.push(i);
+                        return name.push(chunk[1]);
                     }
                 }
 
                 match(/^(\*?)/);
-                while (match(/^((?:[\w-]+)|(?:@\{[\w-]+\}))/)); // !
-                if ((name.length > 1) && match(/^\s*((?:\+_|\+)?)\s*:/)) {
+                while (true) {
+                    if (!match(/^((?:[\w-]+)|(?:@\{[\w-]+\}))/)) {
+                        break;
+                    }
+                }
+
+                if ((name.length > 1) && match(/^\s*((?:\+_|\+)?)\s*:/)) { //TODO remove start \s* - un-necessary
+                    parserInput.forget();
+
                     // at last, we have the complete match now. move forward,
                     // convert name particles to tree objects and return:
-                    skipWhitespace(length);
                     if (name[0] === '') {
                         name.shift();
                         index.shift();
@@ -1939,12 +1697,13 @@ var Parser = function Parser(env) {
                     }
                     return name;
                 }
+                parserInput.restore();
             }
         }
     };
 
     parser.getInput = getInput;
-    parser.getLocation = getLocation;
+    parser.getLocation = parserInput.getLocation;
 
     return parser;
 };
diff --git a/lib/less/tree/js-eval-node.js b/lib/less/tree/js-eval-node.js
index 0693873..35cabab 100644
--- a/lib/less/tree/js-eval-node.js
+++ b/lib/less/tree/js-eval-node.js
@@ -10,6 +10,11 @@ jsEvalNode.prototype.evaluateJavaScript = function (expression, env) {
         that = this,
         context = {};
 
+    if (env.javascriptEnabled !== undefined && !env.javascriptEnabled) {
+        throw { message: "You are using JavaScript, which has been disabled." ,
+            index: this.index };
+    }
+
     expression = expression.replace(/@\{([\w-]+)\}/g, function (_, name) {
         return that.jsify(new(Variable)('@' + name, that.index).eval(env));
     });
diff --git a/test/less/errors/javascript-error.txt b/test/less/errors/javascript-error.txt
index c4da950..e5f7dc3 100644
--- a/test/less/errors/javascript-error.txt
+++ b/test/less/errors/javascript-error.txt
@@ -1,4 +1,4 @@
-SyntaxError: JavaScript evaluation error: 'TypeError: Cannot read property 'toJS' of undefined' in {path}javascript-error.less on line 2, column 25:
+SyntaxError: JavaScript evaluation error: 'TypeError: Cannot read property 'toJS' of undefined' in {path}javascript-error.less on line 2, column 10:
 1 .scope {
 2     var: `this.foo.toJS`;
 3 }
diff --git a/test/less/errors/javascript-undefined-var.txt b/test/less/errors/javascript-undefined-var.txt
index b363aff..5fb14ee 100644
--- a/test/less/errors/javascript-undefined-var.txt
+++ b/test/less/errors/javascript-undefined-var.txt
@@ -1,4 +1,4 @@
-NameError: variable @b is undefined in {path}javascript-undefined-var.less on line 2, column 15:
+NameError: variable @b is undefined in {path}javascript-undefined-var.less on line 2, column 9:
 1 .scope {
 2     @a: `@{b}`;
 3 }

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/less.js.git



More information about the Pkg-javascript-commits mailing list