From 41732cc60bbcf5191ebb5bf1794719963f84671d Mon Sep 17 00:00:00 2001 From: Luke Page Date: Mon, 25 Aug 2014 12:19:04 +0100 Subject: [PATCH] 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 | 769 ++++++------------ 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, 558 insertions(+), 509 deletions(-) create mode 100644 lib/less/parser/parser-input.js diff --git a/lib/less/contexts.js b/lib/less/contexts.js index 5df2cf80..269048ae 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 00000000..7b03e249 --- /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 bc5052b1..164780e1 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; + return parserInput.getInput(); } } - function getLocation(index, 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 - }; - } - - 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; } - - 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; - } - } - - $char('('); // Parse the '(' and consume whitespace. - - args = this.arguments(); - - if (! $char(')')) { + if (parserInput.peek(/^url\(/i)) { return; } - if (name) { return new(tree.Call)(name, args, index, env.currentFileInfo); } + parserInput.save(); + + name = parserInput.$re(/^([\w-]+|%|progid:[\w\.]+)\(/); + if (!name) { parserInput.forget(); return; } + + name = name[1]; + nameLC = name.toLowerCase(); + + if (nameLC === 'alpha') { + alpha = parsers.alpha(); + if(alpha) { + return alpha; + } + } + + args = this.arguments(); + + if (! parserInput.$char(')')) { + parserInput.restore("Could not parse call arguments or missing ')'"); + return; + } + + 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 0693873c..35cabab1 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 c4da9508..e5f7dc39 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 b363aff9..5fb14ee0 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 }