Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
},
"globals": {},
"rules": {
"quotes": [
1,
"single"
],
"no-eval": 2,
"no-use-before-define": [
2,
Expand Down
131 changes: 126 additions & 5 deletions lib/less/parser/parser-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,15 @@ module.exports = function() {
return tok;
};

parserInput.$quoted = function() {
parserInput.$quoted = function(loc) {
var pos = loc || parserInput.i,
startChar = input.charAt(pos);

var startChar = input.charAt(parserInput.i);
if (startChar !== "'" && startChar !== '"') {
return;
}
var length = input.length,
currentPosition = parserInput.i;
currentPosition = pos;

for (var i = 1; i + currentPosition < length; i++) {
var nextChar = input.charAt(i + currentPosition);
Expand All @@ -165,14 +166,134 @@ module.exports = function() {
break;
case startChar:
var str = input.substr(currentPosition, i + 1);
skipWhitespace(i + 1);
return str;
if (!loc && loc !== 0) {
skipWhitespace(i + 1);
return str
}
return [startChar, str];
default:
}
}
return null;
};

/**
* Permissive parsing. Ignores everything except matching {} [] () and quotes
* until matching token (outside of blocks)
*/
parserInput.$parseUntil = function(tok) {
var quote = '',
returnVal = null,
inComment = false,
blockDepth = 0,
blockStack = [],
parseGroups = [],
length = input.length,
startPos = parserInput.i,
lastPos = parserInput.i,
i = parserInput.i,
loop = true,
testChar;

if (typeof tok === 'string') {
testChar = function(char) {
return char === tok;
}
} else {
testChar = function(char) {
return tok.test(char);
}
}

do {
var prevChar, nextChar = input.charAt(i);
if (blockDepth === 0 && testChar(nextChar)) {
returnVal = input.substr(lastPos, i - lastPos);
if (returnVal) {
parseGroups.push(returnVal);
returnVal = parseGroups;
}
else {
returnVal = [' '];
}
skipWhitespace(i - startPos);
loop = false
} else {
if (inComment) {
if (nextChar === "*" &&
input.charAt(i + 1) === "/") {
i++;
blockDepth--;
inComment = false;
}
i++;
continue;
}
switch (nextChar) {
case '\\':
i++;
nextChar = input.charAt(i);
parseGroups.push(input.substr(lastPos, i - lastPos + 1));
lastPos = i + 1;
break;
case "/":
if (input.charAt(i + 1) === "*") {
i++;
console.log(input.substr(lastPos, i - lastPos));
inComment = true;
blockDepth++;
}
break;
case "'":
case '"':
quote = parserInput.$quoted(i);
if (quote) {
parseGroups.push(input.substr(lastPos, i - lastPos), quote);
i += quote[1].length - 1;
lastPos = i + 1;
}
else {
skipWhitespace(i - startPos);
returnVal = nextChar;
loop = false;
}
break;
case "{":
blockStack.push("}");
blockDepth++;
break;
case "(":
blockStack.push(")");
blockDepth++;
break;
case "[":
blockStack.push("]");
blockDepth++;
break;
case "}":
case ")":
case "]":
var expected = blockStack.pop();
if (nextChar === expected) {
blockDepth--;
} else {
// move the parser to the error and return expected
skipWhitespace(i - startPos);
returnVal = expected;
loop = false;
}
}
i++;
if (i > length) {
loop = false;
}
}
prevChar = nextChar;
} while (loop);

return returnVal ? returnVal : null;
}

parserInput.autoCommentAbsorb = true;
parserInput.commentStore = [];
parserInput.finished = false;
Expand Down
71 changes: 63 additions & 8 deletions lib/less/parser/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1269,7 +1269,8 @@ var Parser = function Parser(context, imports, fileInfo) {
}
},
declaration: function () {
var name, value, startOfRule = parserInput.i, c = parserInput.currentChar(), important, merge, isVariable;
var name, value, index = parserInput.i,
c = parserInput.currentChar(), important, merge, isVariable;

if (c === '.' || c === '#' || c === '&' || c === ':') { return; }

Expand All @@ -1290,25 +1291,36 @@ var Parser = function Parser(context, imports, fileInfo) {
// where each item is a tree.Keyword or tree.Variable
merge = !isVariable && name.length > 1 && name.pop().value;

// Custom property values get permissive parsing
if (name[0].value && name[0].value.slice(0, 2) === '--') {
value = this.permissiveValue(';');
}
// Try to store values as anonymous
// If we need the value later we'll re-parse it in ruleset.parseValue
value = this.anonymousValue();
else {
value = this.anonymousValue();
}
if (value) {
parserInput.forget();
// anonymous values absorb the end ';' which is required for them to work
return new (tree.Declaration)(name, value, false, merge, startOfRule, fileInfo);
return new (tree.Declaration)(name, value, false, merge, index, fileInfo);
}

if (!value) {
value = this.value();
}

important = this.important();

// As a last resort, let a variable try to be parsed as a permissive value
if (!value && isVariable) {
value = this.permissiveValue(';');
}
}

if (value && this.end()) {
parserInput.forget();
return new (tree.Declaration)(name, value, important, merge, startOfRule, fileInfo);
return new (tree.Declaration)(name, value, important, merge, index, fileInfo);
}
else {
parserInput.restore();
Expand All @@ -1324,6 +1336,44 @@ var Parser = function Parser(context, imports, fileInfo) {
return new(tree.Anonymous)(match[1], index);
}
},
/**
* Used for custom properties and custom at-rules
* Parses almost anything inside of {} [] () "" blocks
* until it reaches outer-most tokens.
*/
permissiveValue: function (untilTokens) {
var i, index = parserInput.i,
value = parserInput.$parseUntil(untilTokens);

if (value) {
if (typeof value === 'string') {
error("Expected '" + value + "'", "Parse");
}
if (value.length === 1 && value[0] === ' ') {
return new tree.Anonymous('', index);
}
var item, args = [];
for (i = 0; i < value.length; i++) {
item = value[i];
if (Array.isArray(item)) {
// Treat actual quotes as normal quoted values
args.push(new tree.Quoted(item[0], item[1], true, index, fileInfo));
}
else {
if (i === value.length - 1) {
item = item.trim();
}
// Treat like quoted values, but replace vars like unquoted expressions
var quote = new tree.Quoted("'", item, true, index, fileInfo);
quote.variableRegex = /@([\w-]+)/g;
quote.propRegex = /\$([\w-]+)/g;
quote.reparse = true;
args.push(quote);
}
}
return new tree.Expression(args, true);
}
},

//
// An @import atrule
Expand Down Expand Up @@ -1595,10 +1645,15 @@ var Parser = function Parser(context, imports, fileInfo) {
error("expected " + name + " expression");
}
} else if (hasUnknown) {
value = (parserInput.$re(/^[^{;]+/) || '').trim();
hasBlock = (parserInput.currentChar() == '{');
if (value) {
value = new(tree.Anonymous)(value);
value = this.permissiveValue(/^[{;]/);
hasBlock = (parserInput.currentChar() === '{');
if (!value) {
if (!hasBlock && parserInput.currentChar() !== ';') {
error(name + " rule is missing block or ending semi-colon");
}
}
else if (!value.value) {
value = null;
}
}

Expand Down
7 changes: 4 additions & 3 deletions lib/less/tree/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ var Node = require("./node"),
Paren = require("./paren"),
Comment = require("./comment");

var Expression = function (value) {
var Expression = function (value, noSpacing) {
this.value = value;
this.noSpacing = noSpacing;
if (!value) {
throw new Error("Expression requires an array parameter");
}
Expand All @@ -23,7 +24,7 @@ Expression.prototype.eval = function (context) {
if (this.value.length > 1) {
returnValue = new Expression(this.value.map(function (e) {
return e.eval(context);
}));
}), this.noSpacing);
} else if (this.value.length === 1) {
if (this.value[0].parens && !this.value[0].parensInOp) {
doubleParen = true;
Expand All @@ -43,7 +44,7 @@ Expression.prototype.eval = function (context) {
Expression.prototype.genCSS = function (context, output) {
for (var i = 0; i < this.value.length; i++) {
this.value[i].genCSS(context, output);
if (i + 1 < this.value.length) {
if (!this.noSpacing && i + 1 < this.value.length) {
output.add(" ");
}
}
Expand Down
8 changes: 5 additions & 3 deletions lib/less/tree/quoted.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ var Quoted = function (str, content, escaped, index, currentFileInfo) {
this.quote = str.charAt(0);
this._index = index;
this._fileInfo = currentFileInfo;
this.variableRegex = /@\{([\w-]+)\}/g;
this.propRegex = /\$\{([\w-]+)\}/g;
};
Quoted.prototype = new Node();
Quoted.prototype.type = "Quoted";
Expand All @@ -21,7 +23,7 @@ Quoted.prototype.genCSS = function (context, output) {
}
};
Quoted.prototype.containsVariables = function() {
return this.value.match(/@\{([\w-]+)\}/);
return this.value.match(this.variableRegex);
};
Quoted.prototype.eval = function (context) {
var that = this, value = this.value;
Expand All @@ -41,8 +43,8 @@ Quoted.prototype.eval = function (context) {
} while (value !== evaluatedValue);
return evaluatedValue;
}
value = iterativeReplace(value, /@\{([\w-]+)\}/g, variableReplacement);
value = iterativeReplace(value, /\$\{([\w-]+)\}/g, propertyReplacement);
value = iterativeReplace(value, this.variableRegex, variableReplacement);
value = iterativeReplace(value, this.propRegex, propertyReplacement);
return new Quoted(this.quote + value + this.quote, value, this.escaped, this.getIndex(), this.fileInfo());
};
Quoted.prototype.compare = function (other) {
Expand Down
36 changes: 36 additions & 0 deletions test/css/permissive-parse.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@-moz-document regexp("(\d{0,15})") {
a {
color: red;
}
}
.custom-property {
--this: () => {
basically anything until final semi-colon;
even other stuff; // i\'m serious;
};
--that: () => {
basically anything until final semi-colon;
even other stuff; // i\'m serious;
};
--custom-color: #ff3333;
custom-color: #ff3333;
}
.var {
--fortran: read (*, *, iostat=1) radius, height;
}
@-moz-whatever (foo: "(" bam ")") {
bar: foo;
}
#selector, .bar, foo[attr="blah"] {
bar: value;
}
@media (min-width: 640px) {
.holy-crap {
this: works;
}
}
.test-comment {
--value: ;
--comment-within: ( /* okay?; comment; */ );
--empty: ;
}
2 changes: 1 addition & 1 deletion test/less-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = function() {
var oneTestOnly = process.argv[2],
isFinished = false;

var isVerbose = process.env.npm_config_loglevel === 'verbose';
var isVerbose = process.env.npm_config_loglevel !== 'concise';

var normalFolder = 'test/less';
var bomFolder = 'test/less-bom';
Expand Down
4 changes: 4 additions & 0 deletions test/less/errors/at-rules-unmatching-block.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

@unknown url( {
50% {width: 20px;}
}
4 changes: 4 additions & 0 deletions test/less/errors/at-rules-unmatching-block.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
SyntaxError: @unknown rule is missing block or ending semi-colon in {path}at-rules-unmatching-block.less on line 2, column 10:
1
2 @unknown url( {
3 50% {width: 20px;}
Loading