mirror of
https://github.com/less/less.js.git
synced 2026-05-01 03:00:22 -04:00
* Adds permissive parsing for at-rules and custom properties * Added error tests for permissive parsing * Change custom property value to quoted-like value * Allow interpolation in unknown at-rules * Allows variables to fallback to permissive parsing * Allow escaping of blocks
This commit is contained in:
@@ -4,6 +4,10 @@
|
||||
},
|
||||
"globals": {},
|
||||
"rules": {
|
||||
"quotes": [
|
||||
1,
|
||||
"single"
|
||||
],
|
||||
"no-eval": 2,
|
||||
"no-use-before-define": [
|
||||
2,
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -1290,13 +1291,19 @@ 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) {
|
||||
@@ -1304,11 +1311,16 @@ var Parser = function Parser(context, imports, fileInfo) {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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(" ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
36
test/css/permissive-parse.css
Normal file
36
test/css/permissive-parse.css
Normal file
@@ -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: ;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
4
test/less/errors/at-rules-unmatching-block.less
Normal file
4
test/less/errors/at-rules-unmatching-block.less
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
@unknown url( {
|
||||
50% {width: 20px;}
|
||||
}
|
||||
4
test/less/errors/at-rules-unmatching-block.txt
Normal file
4
test/less/errors/at-rules-unmatching-block.txt
Normal file
@@ -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;}
|
||||
6
test/less/errors/custom-property-unmatched-block-1.less
Normal file
6
test/less/errors/custom-property-unmatched-block-1.less
Normal file
@@ -0,0 +1,6 @@
|
||||
.custom {
|
||||
--custom: ({
|
||||
this;
|
||||
is-unmatched: [
|
||||
})
|
||||
}
|
||||
4
test/less/errors/custom-property-unmatched-block-1.txt
Normal file
4
test/less/errors/custom-property-unmatched-block-1.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
ParseError: Expected ']' in {path}custom-property-unmatched-block-1.less on line 5, column 3:
|
||||
4 is-unmatched: [
|
||||
5 })
|
||||
6 }
|
||||
6
test/less/errors/custom-property-unmatched-block-2.less
Normal file
6
test/less/errors/custom-property-unmatched-block-2.less
Normal file
@@ -0,0 +1,6 @@
|
||||
.custom {
|
||||
--custom: {{
|
||||
this;
|
||||
is-unmatched: [
|
||||
}}
|
||||
}
|
||||
4
test/less/errors/custom-property-unmatched-block-2.txt
Normal file
4
test/less/errors/custom-property-unmatched-block-2.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
ParseError: Expected ']' in {path}custom-property-unmatched-block-2.less on line 5, column 3:
|
||||
4 is-unmatched: [
|
||||
5 }}
|
||||
6 }
|
||||
6
test/less/errors/custom-property-unmatched-block-3.less
Normal file
6
test/less/errors/custom-property-unmatched-block-3.less
Normal file
@@ -0,0 +1,6 @@
|
||||
.custom {
|
||||
--custom: {{
|
||||
this;
|
||||
is-unmatched: "
|
||||
}}
|
||||
}
|
||||
4
test/less/errors/custom-property-unmatched-block-3.txt
Normal file
4
test/less/errors/custom-property-unmatched-block-3.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
ParseError: Expected '"' in {path}custom-property-unmatched-block-3.less on line 4, column 19:
|
||||
3 this;
|
||||
4 is-unmatched: "
|
||||
5 }}
|
||||
51
test/less/permissive-parse.less
Normal file
51
test/less/permissive-parse.less
Normal file
@@ -0,0 +1,51 @@
|
||||
@function-name: regexp;
|
||||
@d-value: 15;
|
||||
@-moz-document @function-name("(\d{0,@{d-value}})") {
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-property {
|
||||
--this: () => {
|
||||
basically anything until final semi-colon;
|
||||
even other stuff; // i\'m serious;
|
||||
};
|
||||
@this: () => {
|
||||
basically anything until final semi-colon;
|
||||
even other stuff; // i\'m serious;
|
||||
};
|
||||
--that: @this;
|
||||
@red: lighten(red, 10%);
|
||||
--custom-color: @red;
|
||||
custom-color: $--custom-color;
|
||||
}
|
||||
|
||||
@iostat: 1;
|
||||
.var {
|
||||
--fortran: read (*, *, iostat=@iostat) radius, height;
|
||||
}
|
||||
|
||||
@boom-boom: bam;
|
||||
@-moz-whatever (foo: "(" @boom-boom ")") {
|
||||
bar: foo;
|
||||
}
|
||||
|
||||
@selectorList: #selector, .bar, foo[attr="blah"];
|
||||
@{selectorList} {
|
||||
bar: value;
|
||||
}
|
||||
|
||||
@size: 640px;
|
||||
@tablet: (min-width: @size);
|
||||
@media @tablet {
|
||||
.holy-crap {
|
||||
this: works;
|
||||
}
|
||||
}
|
||||
// @todo - fix comment absorption after property
|
||||
.test-comment {
|
||||
--value: /* { ; } */;
|
||||
--comment-within: ( /* okay?; comment; */ );
|
||||
--empty: ;
|
||||
}
|
||||
Reference in New Issue
Block a user