diff --git a/lib/less/parser/parser.js b/lib/less/parser/parser.js index 2e933d35e..bc31194f0 100644 --- a/lib/less/parser/parser.js +++ b/lib/less/parser/parser.js @@ -435,15 +435,15 @@ var Parser = function Parser(context, imports, fileInfo) { return; } - value = this.quoted() || this.variable() || + value = this.quoted() || this.variable() || this.property() || parserInput.$re(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/) || ""; parserInput.autoCommentAbsorb = true; expectChar(')'); - return new(tree.URL)((value.value != null || value instanceof tree.Variable) ? - value : new(tree.Anonymous)(value), index, fileInfo); + return new(tree.URL)((value.value != null || value instanceof tree.Variable || value instanceof tree.Property) ? + value : new(tree.Anonymous)(value), index, fileInfo); }, // @@ -470,6 +470,27 @@ var Parser = function Parser(context, imports, fileInfo) { return new(tree.Variable)("@" + curly[1], index, fileInfo); } }, + // + // A Property accessor, such as `$color`, in + // + // background-color: $color + // + property: function () { + var name, index = parserInput.i; + + if (parserInput.currentChar() === '$' && (name = parserInput.$re(/^\$[\w-]+/))) { + return new(tree.Property)(name, index, fileInfo); + } + }, + + // A property entity useing the protective {} e.g. @{prop} + propertyCurly: function () { + var curly, index = parserInput.i; + + if (parserInput.currentChar() === '$' && (curly = parserInput.$re(/^\$\{([\w-]+)\}/))) { + return new(tree.Property)("$" + curly[1], index, fileInfo); + } + }, // // A Hexadecimal color @@ -706,7 +727,7 @@ var Parser = function Parser(context, imports, fileInfo) { .push({ variadic: true }); break; } - arg = entities.variable() || entities.literal() || entities.keyword(); + arg = entities.variable() || entities.property() || entities.literal() || entities.keyword(); } if (!arg) { @@ -729,7 +750,7 @@ var Parser = function Parser(context, imports, fileInfo) { val = arg; } - if (val && val instanceof tree.Variable) { + if (val && (val instanceof tree.Variable || val instanceof tree.Property)) { if (parserInput.$char(':')) { if (expressions.length > 0) { if (isSemiColonSeparated) { @@ -875,7 +896,7 @@ var Parser = function Parser(context, imports, fileInfo) { var entities = this.entities; return this.comment() || entities.literal() || entities.variable() || entities.url() || - entities.call() || entities.keyword() || entities.javascript(); + entities.property() || entities.call() || entities.keyword() || entities.javascript(); }, // @@ -1143,7 +1164,8 @@ var Parser = function Parser(context, imports, fileInfo) { // prefer to try to parse first if its a variable or we are compressing // but always fallback on the other one - var tryValueFirst = !tryAnonymous && (context.compress || isVariable); + // For property accessors, always evaluate + var tryValueFirst = !tryAnonymous; //&& (context.compress || isVariable) if (tryValueFirst) { value = this.value(); @@ -1177,7 +1199,7 @@ var Parser = function Parser(context, imports, fileInfo) { } }, anonymousValue: function () { - var match = parserInput.$re(/^([^@+\/'"*`(;{}-]*);/); + var match = parserInput.$re(/^([^@\$+\/'"*`(;{}-]*);/); if (match) { return new(tree.Anonymous)(match[1]); } @@ -1650,13 +1672,13 @@ var Parser = function Parser(context, imports, fileInfo) { operand: function () { var entities = this.entities, negate; - if (parserInput.peek(/^-[@\(]/)) { + if (parserInput.peek(/^-[@\$\(]/)) { negate = parserInput.$char('-'); } var o = this.sub() || entities.dimension() || entities.color() || entities.variable() || - entities.call(); + entities.property() || entities.call(); if (negate) { o.parensInOp = true; @@ -1727,7 +1749,7 @@ var Parser = function Parser(context, imports, fileInfo) { match(/^(\*?)/); while (true) { - if (!match(/^((?:[\w-]+)|(?:@\{[\w-]+\}))/)) { + if (!match(/^((?:[\w-]+)|(?:[@\$]\{[\w-]+\}))/)) { break; } } @@ -1743,10 +1765,11 @@ var Parser = function Parser(context, imports, fileInfo) { } for (k = 0; k < name.length; k++) { s = name[k]; - name[k] = (s.charAt(0) !== '@') ? + name[k] = (s.charAt(0) !== '@' && s.charAt(0) !== '$') ? new(tree.Keyword)(s) : - new(tree.Variable)('@' + s.slice(2, -1), - index[k], fileInfo); + (s.charAt(0) === '@' ? + new(tree.Variable)('@' + s.slice(2, -1), index[k], fileInfo) : + new(tree.Property)('$' + s.slice(2, -1), index[k], fileInfo)); } return name; } diff --git a/lib/less/tree/index.js b/lib/less/tree/index.js index c4624dd69..72501982b 100644 --- a/lib/less/tree/index.js +++ b/lib/less/tree/index.js @@ -10,6 +10,7 @@ tree.Dimension = require('./dimension'); tree.Unit = require('./unit'); tree.Keyword = require('./keyword'); tree.Variable = require('./variable'); +tree.Property = require('./property'); tree.Ruleset = require('./ruleset'); tree.Element = require('./element'); tree.Attribute = require('./attribute'); diff --git a/lib/less/tree/property.js b/lib/less/tree/property.js new file mode 100644 index 000000000..24748f8f1 --- /dev/null +++ b/lib/less/tree/property.js @@ -0,0 +1,69 @@ +var Node = require("./node"), + Rule = require("./rule"); + +var Property = function (name, index, currentFileInfo) { + this.name = name; + this.index = index; + this.currentFileInfo = currentFileInfo || {}; +}; +Property.prototype = new Node(); +Property.prototype.type = "Property"; +Property.prototype.eval = function (context) { + var property, name = this.name; + // Not sure if there's a shorter reference, but require seemed to delete prototype of Rule (different context?) + var mergeRules = context.pluginManager.less.visitors.ToCSSVisitor.prototype._mergeRules; + + if (this.evaluating) { + throw { type: 'Name', + message: "Recursive property reference for " + name, + filename: this.currentFileInfo.filename, + index: this.index }; + } + + this.evaluating = true; + + property = this.find(context.frames, function (frame) { + + var v, vArr = frame.property(name); + if (vArr) { + for (var i = 0; i < vArr.length; i++) { + v = vArr[i]; + + vArr[i] = new Rule (v.name, + v.value, + v.important, + v.merge, + v.index, + v.currentFileInfo, + v.inline, + v.variable + ); + } + mergeRules(vArr); + + v = vArr[vArr.length - 1]; + if (v.important) { + var importantScope = context.importantScope[context.importantScope.length - 1]; + importantScope.important = v.important; + } + return v.value.eval(context); + } + }); + if (property) { + this.evaluating = false; + return property; + } else { + throw { type: 'Name', + message: "Property " + name + " is undefined", + filename: this.currentFileInfo.filename, + index: this.index }; + } +}; +Property.prototype.find = function (obj, fun) { + for (var i = 0, r; i < obj.length; i++) { + r = fun.call(obj, obj[i]); + if (r) { return r; } + } + return null; +}; +module.exports = Property; diff --git a/lib/less/tree/quoted.js b/lib/less/tree/quoted.js index e1be87d10..32b26ee07 100644 --- a/lib/less/tree/quoted.js +++ b/lib/less/tree/quoted.js @@ -1,6 +1,7 @@ var Node = require("./node"), JsEvalNode = require("./js-eval-node"), - Variable = require("./variable"); + Variable = require("./variable"), + Property = require("./property"); var Quoted = function (str, content, escaped, index, currentFileInfo) { this.escaped = (escaped == null) ? true : escaped; @@ -28,10 +29,14 @@ Quoted.prototype.eval = function (context) { var javascriptReplacement = function (_, exp) { return String(that.evaluateJavaScript(exp, context)); }; - var interpolationReplacement = function (_, name) { + var variableReplacement = function (_, name) { var v = new Variable('@' + name, that.index, that.currentFileInfo).eval(context, true); return (v instanceof Quoted) ? v.value : v.toCSS(); }; + var propertyReplacement = function (_, name) { + var v = new Property('$' + name, that.index, that.currentFileInfo).eval(context, true); + return (v instanceof Quoted) ? v.value : v.toCSS(); + }; function iterativeReplace(value, regexp, replacementFnc) { var evaluatedValue = value; do { @@ -41,7 +46,8 @@ Quoted.prototype.eval = function (context) { return evaluatedValue; } value = iterativeReplace(value, /`([^`]+)`/g, javascriptReplacement); - value = iterativeReplace(value, /@\{([\w-]+)\}/g, interpolationReplacement); + value = iterativeReplace(value, /@\{([\w-]+)\}/g, variableReplacement); + value = iterativeReplace(value, /\$\{([\w-]+)\}/g, propertyReplacement); return new Quoted(this.quote + value + this.quote, value, this.escaped, this.index, this.currentFileInfo); }; Quoted.prototype.compare = function (other) { diff --git a/lib/less/tree/ruleset.js b/lib/less/tree/ruleset.js index f387d4bdb..d8fffadf7 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -6,7 +6,8 @@ var Node = require("./node"), contexts = require("../contexts"), globalFunctionRegistry = require("../functions/function-registry"), defaultFunc = require("../functions/default"), - getDebugInfo = require("./debug-info"); + getDebugInfo = require("./debug-info"), + Keyword = require("./keyword"); var Ruleset = function (selectors, rules, strictImports) { this.selectors = selectors; @@ -227,6 +228,7 @@ Ruleset.prototype.matchCondition = function (args, context) { Ruleset.prototype.resetCache = function () { this._rulesets = null; this._variables = null; + this._properties = null; this._lookups = {}; }; Ruleset.prototype.variables = function () { @@ -251,9 +253,31 @@ Ruleset.prototype.variables = function () { } return this._variables; }; +Ruleset.prototype.properties = function () { + if (!this._properties) { + this._properties = !this.rules ? {} : this.rules.reduce(function (hash, r) { + if (r instanceof Rule && r.variable !== true) { + var name = (r.name.length === 1) && (r.name[0] instanceof Keyword) ? + r.name[0].value : r.name; + // Properties don't overwrite as they can merge + if (!hash['$' + name]) { + hash['$' + name] = [ r ]; + } + else { + hash['$' + name].push(r); + } + } + return hash; + }, {}); + } + return this._properties; +}; Ruleset.prototype.variable = function (name) { return this.variables()[name]; }; +Ruleset.prototype.property = function (name) { + return this.properties()[name]; +}; Ruleset.prototype.rulesets = function () { if (!this.rules) { return []; } diff --git a/package.json b/package.json index b154a6bbf..8c22e0959 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "grunt-contrib-clean": "^0.6.0", "grunt-contrib-concat": "^0.5.0", "grunt-contrib-connect": "^0.9.0", - "grunt-contrib-jasmine": "^0.8.2", + "grunt-contrib-jasmine": "^0.9.2", "grunt-contrib-jshint": "^0.11.0", "grunt-contrib-uglify": "^0.8.0", "grunt-jscs": "^1.6.0", diff --git a/test/css/colors.css b/test/css/colors.css index 08a22abb8..c001615af 100644 --- a/test/css/colors.css +++ b/test/css/colors.css @@ -1,5 +1,5 @@ #yelow #short { - color: #fea; + color: #ffeeaa; } #yelow #long { color: #ffeeaa; @@ -11,7 +11,7 @@ color: #1affeeaa; } #blue #short { - color: #00f; + color: #0000ff; } #blue #long { color: #0000ff; diff --git a/test/css/comments.css b/test/css/comments.css index c8475cd9e..d2330281d 100644 --- a/test/css/comments.css +++ b/test/css/comments.css @@ -73,7 +73,7 @@ /* */ /* */ #div { - color: #A33; + color: #aa3333; } /* } */ /*by block */ diff --git a/test/css/css.css b/test/css/css.css index 4fb579711..9619301f4 100644 --- a/test/css/css.css +++ b/test/css/css.css @@ -40,7 +40,7 @@ div#id { } a:hover, a:link { - color: #999; + color: #999999; } p, p:first-child { @@ -53,7 +53,7 @@ p + h1 { font-size: 2.2em; } #shorthands { - border: 1px solid #000; + border: 1px solid #000000; font: 12px/16px Arial; font: 100%/16px Arial; margin: 1px 0; @@ -69,7 +69,7 @@ p + h1 { .misc { -moz-border-radius: 2px; display: -moz-inline-stack; - width: .1em; + width: 0.1em; background-color: #009998; background: -webkit-gradient(linear, left top, left bottom, from(red), to(blue)); margin: ; @@ -81,7 +81,7 @@ p + h1 { } #important { color: red !important; - width: 100%!important; + width: 100% !important; height: 20px ! important; } @font-face { diff --git a/test/css/extend-nest.css b/test/css/extend-nest.css index 2c3905d95..4ed37e457 100644 --- a/test/css/extend-nest.css +++ b/test/css/extend-nest.css @@ -9,8 +9,8 @@ .sidebar2 .box, .type1 .sidebar3 .box, .type2.sidebar4 .box { - background: #FFF; - border: 1px solid #000; + background: #ffffff; + border: 1px solid #000000; margin: 10px 0; } .sidebar2 { diff --git a/test/css/extend-selector.css b/test/css/extend-selector.css index da47254b3..01638efcb 100644 --- a/test/css/extend-selector.css +++ b/test/css/extend-selector.css @@ -1,7 +1,7 @@ .error, .badError { - border: 1px #f00; - background: #fdd; + border: 1px #ff0000; + background: #ffdddd; } .error.intrusion, .badError.intrusion { diff --git a/test/css/extend.css b/test/css/extend.css index 2895641a7..c39468b82 100644 --- a/test/css/extend.css +++ b/test/css/extend.css @@ -1,7 +1,7 @@ .error, .badError { - border: 1px #f00; - background: #fdd; + border: 1px #ff0000; + background: #ffdddd; } .error.intrusion, .badError.intrusion { diff --git a/test/css/operations.css b/test/css/operations.css index fb9e0aff7..1a1539692 100644 --- a/test/css/operations.css +++ b/test/css/operations.css @@ -30,7 +30,7 @@ font-size: 5.5rem; } .colors { - color: #123; + color: #112233; border-color: #334455; background-color: #000000; } diff --git a/test/css/property-accessors.css b/test/css/property-accessors.css new file mode 100644 index 000000000..300bb48a9 --- /dev/null +++ b/test/css/property-accessors.css @@ -0,0 +1,49 @@ +.block_1 { + color: red; + background-color: red; + width: 50px; + height: 25px; + border: 1px solid #ff3333; + content: "red"; + prop: red; +} +.block_1:hover { + background-color: green; + color: green; +} +.block_1 .one { + background: red; +} +.block_2 { + color: red; + color: blue; +} +.block_2 .two { + background-color: blue; +} +.block_3 { + color: red; + color: yellow; + color: blue; +} +.block_3 .three { + background-color: blue; +} +.block_4 { + color: red; + color: blue; + color: yellow; +} +.block_4 .four { + background-color: yellow; +} +a { + background-color: red, foo; +} +ab { + background: red, foo; +} +.value_as_property { + prop1: color; + color: #ff0000; +} diff --git a/test/css/whitespace.css b/test/css/whitespace.css index 38ad81c1c..a4d333eba 100644 --- a/test/css/whitespace.css +++ b/test/css/whitespace.css @@ -11,7 +11,7 @@ color: white; } .whitespace { - color: white ; + color: white; } .white, .space, @@ -29,12 +29,8 @@ border: 2px solid white; } .newlines { - background: the, - great, - wall; - border: 2px - solid - black; + background: the, great, wall; + border: 2px solid black; } .sel .newline_ws .tab_ws { color: white; diff --git a/test/less/property-accessors.less b/test/less/property-accessors.less new file mode 100644 index 000000000..24dbd604a --- /dev/null +++ b/test/less/property-accessors.less @@ -0,0 +1,66 @@ + +.block_1 { + color: red; + background-color: $color; + width: 50px; + height: ($width / 2); + @color: red; + border: 1px solid lighten($color, 10%); + &:hover { + color: $color; + background-color: $color; + .mixin1(); + } + .one { + background: $color; + } + content: "${color}"; + prop: $color; + +} + +.block_2 { + color: red; + .two { + background-color: $color; + } + color: blue; +} + +.block_3 { + color: red; + .three { + background-color: $color; + } + .mixin2(); + color: blue; +} +.block_4 { + color: red; + .four { + background-color: $color; + } + color: blue; + .mixin2(); +} +// property merging +a { + background-color+: red; + background-color+: foo; + + &b { + background: $background-color; + } +} + +.value_as_property { + prop1: color; + ${prop1}: #FF0000; // not sure why you'd want to do this, but ok +} + +.mixin1() { + color: green; +} +.mixin2() { + color: yellow; +}