Skip to content

Commit

Permalink
Merge pull request #1859 from less/detached-rulesets
Browse files Browse the repository at this point in the history
detached rulesets
  • Loading branch information
lukeapage committed Feb 19, 2014
2 parents 9dbb18c + 42aff6f commit 8fa966e
Show file tree
Hide file tree
Showing 23 changed files with 412 additions and 40 deletions.
2 changes: 2 additions & 0 deletions build/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ tree:
- <%= build.lib %>/tree/color.js
- <%= build.lib %>/tree/comment.js
- <%= build.lib %>/tree/condition.js
- <%= build.lib %>/tree/detached-ruleset.js
- <%= build.lib %>/tree/dimension.js
- <%= build.lib %>/tree/directive.js
- <%= build.lib %>/tree/element.js
Expand All @@ -143,6 +144,7 @@ tree:
- <%= build.lib %>/tree/quoted.js
- <%= build.lib %>/tree/rule.js
- <%= build.lib %>/tree/ruleset.js
- <%= build.lib %>/tree/ruleset-call.js
- <%= build.lib %>/tree/selector.js
- <%= build.lib %>/tree/unicode-descriptor.js
- <%= build.lib %>/tree/url.js
Expand Down
2 changes: 2 additions & 0 deletions lib/less/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ var less = {

require('./tree/color');
require('./tree/directive');
require('./tree/detached-ruleset');
require('./tree/operation');
require('./tree/dimension');
require('./tree/keyword');
Expand All @@ -120,6 +121,7 @@ require('./tree/media');
require('./tree/unicode-descriptor');
require('./tree/negative');
require('./tree/extend');
require('./tree/ruleset-call');


var isUrlRe = /^(?:https?:)?\/\//i;
Expand Down
131 changes: 103 additions & 28 deletions lib/less/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ less.Parser = function Parser(env) {
var input, // LeSS input string
i, // current index in `input`
j, // current chunk
temp, // temporarily holds a chunk's state, for backtracking
memo, // temporarily holds `i`, when backtracking
saveStack = [], // holds state for backtracking
furthest, // furthest index the parser has gone to
chunks, // chunkified input
current, // current chunk
Expand Down Expand Up @@ -111,8 +110,9 @@ less.Parser = function Parser(env) {
}
};

function save() { temp = current; memo = currentPos = i; }
function restore() { current = temp; currentPos = i = memo; }
function save() { currentPos = i; saveStack.push( { current: current, i: i, j: j }); }
function restore() { var state = saveStack.pop(); current = state.current; currentPos = i = state.i; j = state.j; }
function forget() { saveStack.pop(); }

function sync() {
if (i > currentPos) {
Expand Down Expand Up @@ -424,7 +424,7 @@ less.Parser = function Parser(env) {
if (--level < 0) {
return fail("missing opening `{`");
}
if (!level) { emitChunk(); }
if (!level && !parenLevel) { emitChunk(); }
continue;
case 92: // \
if (parserCurrentIndex < len - 1) { parserCurrentIndex++; continue; }
Expand Down Expand Up @@ -729,14 +729,17 @@ less.Parser = function Parser(env) {
while (current)
{
node = this.extendRule() || mixin.definition() || this.rule() || this.ruleset() ||
mixin.call() || this.comment() || this.directive();
mixin.call() || this.comment() || this.rulesetCall() || this.directive();
if (node) {
root.push(node);
} else {
if (!($re(/^[\s\n]+/) || $re(/^;+/))) {
break;
}
}
if (peekChar('}')) {
break;
}
}

return root;
Expand Down Expand Up @@ -1027,6 +1030,19 @@ less.Parser = function Parser(env) {
if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*:/))) { return name[1]; }
},

//
// The variable part of a variable definition. Used in the `rule` parser
//
// @fink();
//
rulesetCall: function () {
var name;

if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*\(\s*\)\s*;/))) {
return new tree.RulesetCall(name[1]);
}
},

//
// extend syntax - used to extend selectors
//
Expand Down Expand Up @@ -1112,6 +1128,7 @@ less.Parser = function Parser(env) {
}

if (parsers.end()) {
forget();
return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important);
}
}
Expand All @@ -1124,9 +1141,11 @@ less.Parser = function Parser(env) {
expressions = [], argsSemiColon = [], argsComma = [],
isSemiColonSeperated, expressionContainsNamed, name, nameLoop, value, arg;

save();

while (true) {
if (isCall) {
arg = parsers.expression();
arg = parsers.detachedRuleset() || parsers.expression();
} else {
parsers.comments();
if (input.charAt(i) === '.' && $re(/^\.{3}/)) {
Expand Down Expand Up @@ -1154,7 +1173,7 @@ less.Parser = function Parser(env) {

if (isCall) {
// Variable
if (arg.value.length == 1) {
if (arg.value && arg.value.length == 1) {
val = arg.value[0];
}
} else {
Expand All @@ -1169,7 +1188,21 @@ less.Parser = function Parser(env) {
}
expressionContainsNamed = true;
}
value = expect(parsers.expression);

// we do not support setting a ruleset as a default variable - it doesn't make sense
// However if we do want to add it, there is nothing blocking it, just don't error
// and remove isCall dependency below
value = (isCall && parsers.detachedRuleset()) || parsers.expression();

if (!value) {
if (isCall) {
error("could not understand value for named argument");
} else {
restore();
returner.args = [];
return returner;
}
}
nameLoop = (name = val.name);
} else if (!isCall && $re(/^\.{3}/)) {
returner.variadic = true;
Expand Down Expand Up @@ -1214,6 +1247,7 @@ less.Parser = function Parser(env) {
}
}

forget();
returner.args = isSemiColonSeperated ? argsSemiColon : argsComma;
return returner;
},
Expand Down Expand Up @@ -1254,10 +1288,14 @@ less.Parser = function Parser(env) {
variadic = argInfo.variadic;

// .mixincall("@{a}");
// looks a bit like a mixin definition.. so we have to be nice and restore
// looks a bit like a mixin definition..
// also
// .mixincall(@a: {rule: set;});
// so we have to be nice and restore
if (!$char(')')) {
furthest = i;
restore();
return;
}

parsers.comments();
Expand All @@ -1269,10 +1307,13 @@ less.Parser = function Parser(env) {
ruleset = parsers.block();

if (ruleset) {
forget();
return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);
} else {
restore();
}
} else {
forget();
}
}
},
Expand Down Expand Up @@ -1336,10 +1377,16 @@ less.Parser = function Parser(env) {
this.entities.variableCurly();

if (! e) {
save();
if ($char('(')) {
if ((v = this.selector()) && $char(')')) {
e = new(tree.Paren)(v);
forget();
} else {
restore();
}
} else {
forget();
}
}

Expand Down Expand Up @@ -1442,6 +1489,22 @@ less.Parser = function Parser(env) {
}
},

blockRuleset: function() {
var block = this.block();

if (block) {
block = new tree.Ruleset(null, block);
}
return block;
},

detachedRuleset: function() {
var blockRuleset = this.blockRuleset();
if (blockRuleset) {
return new tree.DetachedRuleset(blockRuleset);
}
},

//
// div, .class, body > p {...}
//
Expand Down Expand Up @@ -1472,6 +1535,7 @@ less.Parser = function Parser(env) {
}

if (selectors && (rules = this.block())) {
forget();
var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports);
if (env.dumpLineNumbers) {
ruleset.debugInfo = debugInfo;
Expand All @@ -1484,35 +1548,47 @@ less.Parser = function Parser(env) {
}
},
rule: function (tryAnonymous) {
var name, value, c = input.charAt(i), important, merge;
save();
var name, value, startOfRule = i, c = input.charAt(startOfRule), important, merge, isVariable;

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

save();

name = this.variable() || this.ruleProperty();
if (name) {
// 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 || (name.charAt && (name.charAt(0) === '@'))) ?
(this.value() || this.anonymousValue()) :
(this.anonymousValue() || this.value());

important = this.important();
isVariable = typeof name === "string";

// 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 = name.pop && name.pop().value;
if (isVariable) {
value = this.detachedRuleset();
}

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;
}

if (value && this.end()) {
return new (tree.Rule)(name, value, important, merge, memo, env.currentFileInfo);
forget();
return new (tree.Rule)(name, value, important, merge, startOfRule, env.currentFileInfo);
} else {
furthest = i;
restore();
if (value && !tryAnonymous) {
return this.rule(true);
}
}
} else {
forget();
}
},
anonymousValue: function () {
Expand Down Expand Up @@ -1546,6 +1622,7 @@ less.Parser = function Parser(env) {
if (dir && (path = this.entities.quoted() || this.entities.url())) {
features = this.mediaFeatures();
if ($char(';')) {
forget();
features = features && new(tree.Value)(features);
return new(tree.Import)(path, features, options, index, env.currentFileInfo);
}
Expand Down Expand Up @@ -1742,13 +1819,11 @@ less.Parser = function Parser(env) {
}

if (hasBlock) {
rules = this.block();
if (rules) {
rules = new(tree.Ruleset)(null, rules);
}
rules = this.blockRuleset();
}

if (rules || (!hasBlock && value && $char(';'))) {
forget();
return new(tree.Directive)(name, value, rules, index, env.currentFileInfo,
env.dumpLineNumbers ? getDebugInfo(index, input, env) : null);
}
Expand Down
20 changes: 20 additions & 0 deletions lib/less/tree/detached-ruleset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
(function (tree) {

tree.DetachedRuleset = function (ruleset, frames) {
this.ruleset = ruleset;
this.frames = frames;
};
tree.DetachedRuleset.prototype = {
type: "DetachedRuleset",
accept: function (visitor) {
this.ruleset = visitor.visit(this.ruleset);
},
eval: function (env) {
var frames = this.frames || env.frames.slice(0);
return new tree.DetachedRuleset(this.ruleset, frames);
},
callEval: function (env) {
return this.ruleset.eval(this.frames ? new(tree.evalEnv)(env, this.frames.concat(env.frames)) : env);
}
};
})(require('../tree'));
2 changes: 2 additions & 0 deletions lib/less/tree/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ tree.Media.prototype = {
}
},
bubbleSelectors: function (selectors) {
if (!selectors)
return;
this.rules = [new(tree.Ruleset)(selectors.slice(0), [this.rules[0]])];
}
};
Expand Down
13 changes: 8 additions & 5 deletions lib/less/tree/mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ tree.mixin.Call.prototype = {
mixin.originalRuleset = mixins[m].originalRuleset || mixins[m];
}
Array.prototype.push.apply(
rules, mixin.eval(env, args, this.important).rules);
rules, mixin.evalCall(env, args, this.important).rules);
} catch (e) {
throw { message: e.message, index: this.index, filename: this.currentFileInfo.filename, stack: e.stack };
}
Expand Down Expand Up @@ -150,7 +150,7 @@ tree.mixin.Call.prototype = {
}
};

tree.mixin.Definition = function (name, params, rules, condition, variadic) {
tree.mixin.Definition = function (name, params, rules, condition, variadic, frames) {
this.name = name;
this.selectors = [new(tree.Selector)([new(tree.Element)(null, name, this.index, this.currentFileInfo)])];
this.params = params;
Expand All @@ -164,7 +164,7 @@ tree.mixin.Definition = function (name, params, rules, condition, variadic) {
else { return count; }
}, 0);
this.parent = tree.Ruleset.prototype;
this.frames = [];
this.frames = frames;
};
tree.mixin.Definition.prototype = {
type: "MixinDefinition",
Expand Down Expand Up @@ -258,9 +258,12 @@ tree.mixin.Definition.prototype = {

return frame;
},
eval: function (env, args, important) {
eval: function (env) {
return new tree.mixin.Definition(this.name, this.params, this.rules, this.condition, this.variadic, this.frames || env.frames.slice(0));
},
evalCall: function (env, args, important) {
var _arguments = [],
mixinFrames = this.frames.concat(env.frames),
mixinFrames = this.frames ? this.frames.concat(env.frames) : env.frames,
frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments),
rules, ruleset;

Expand Down
Loading

0 comments on commit 8fa966e

Please sign in to comment.