Skip to content

Commit

Permalink
fix(issue:3766) add support for container queries
Browse files Browse the repository at this point in the history
* Add support for CSS Container Queries
* Add tests for CSS Container Queries
  • Loading branch information
puckowski committed Jul 21, 2023
1 parent 4d3189c commit 856ca74
Show file tree
Hide file tree
Showing 16 changed files with 14,794 additions and 14,058 deletions.
14,062 changes: 7,098 additions & 6,964 deletions dist/less.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/less.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/less.min.js.map

Large diffs are not rendered by default.

14,062 changes: 7,098 additions & 6,964 deletions packages/less/dist/less.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/less/dist/less.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/less/dist/less.min.js.map

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions packages/less/src/less/parser/parser-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ export default () => {
return tok;
};

parserInput.$peekChar = tok => {
if (input.charAt(parserInput.i) !== tok) {
return null;
}
return tok;
};

parserInput.$str = tok => {
const tokLength = tok.length;

Expand Down
77 changes: 47 additions & 30 deletions packages/less/src/less/parser/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import visitors from '../visitors';
import getParserInput from './parser-input';
import * as utils from '../utils';
import functionRegistry from '../functions/function-registry';
import { ContainerSyntaxOptions, MediaSyntaxOptions } from '../tree/atrule-syntax';

//
// less.js - parser
Expand Down Expand Up @@ -1698,7 +1699,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
const options = (dir ? this.importOptions() : null) || {};

if ((path = this.entities.quoted() || this.entities.url())) {
features = this.mediaFeatures();
features = this.mediaFeatures({});

if (!parserInput.$char(';')) {
parserInput.i = index;
Expand Down Expand Up @@ -1752,7 +1753,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
}
},

mediaFeature: function () {
mediaFeature: function (syntaxOptions) {
const entities = this.entities;
const nodes = [];
let e;
Expand All @@ -1764,10 +1765,20 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
nodes.push(e);
} else if (parserInput.$char('(')) {
p = this.property();
e = this.value();
parserInput.save();
if (!p && syntaxOptions.queryInParens && parserInput.$re(/^[a-z-]*\s*([<>]=|<=|>=|[<>]|=)/)) {
parserInput.restore();
p = this.condition();
} else {
parserInput.restore();
e = this.value();
}
if (parserInput.$char(')')) {
if (p && e) {
nodes.push(new(tree.Paren)(new(tree.Declaration)(p, e, null, null, parserInput.i + currentIndex, fileInfo, true)));
if (p && !e) {
nodes.push(new (tree.Paren)(new (tree.QueryInParens)(p.op, p.lvalue, p.rvalue, p._index)));
e = p;
} else if (p && e) {
nodes.push(new (tree.Paren)(new (tree.Declaration)(p, e, null, null, parserInput.i + currentIndex, fileInfo, true)));
} else if (e) {
nodes.push(new(tree.Paren)(e));
} else {
Expand All @@ -1785,12 +1796,12 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
}
},

mediaFeatures: function () {
mediaFeatures: function (syntaxOptions) {
const entities = this.entities;
const features = [];
let e;
do {
e = this.mediaFeature();
e = this.mediaFeature(syntaxOptions);
if (e) {
features.push(e);
if (!parserInput.$char(',')) { break; }
Expand All @@ -1806,38 +1817,44 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
return features.length > 0 ? features : null;
},

media: function () {
let features;
let rules;
let media;
prepareAndGetNestableAtRule: function (treeType, index, debugInfo, syntaxOptions) {
const features = this.mediaFeatures(syntaxOptions);

const rules = this.block();

if (!rules) {
error('media definitions require block statements after any features');
}

parserInput.forget();

const atRule = new (treeType)(rules, features, index + currentIndex, fileInfo);
if (context.dumpLineNumbers) {
atRule.debugInfo = debugInfo;
}

return atRule;
},

nestableAtRule: function () {
let debugInfo;
const index = parserInput.i;

if (context.dumpLineNumbers) {
debugInfo = getDebugInfo(index);
}

parserInput.save();

if (parserInput.$str('@media')) {
features = this.mediaFeatures();

rules = this.block();

if (!rules) {
error('media definitions require block statements after any features');
if (parserInput.$peekChar('@')) {
if (parserInput.$str('@media')) {
return this.prepareAndGetNestableAtRule(tree.Media, index, debugInfo, MediaSyntaxOptions);
}

parserInput.forget();

media = new(tree.Media)(rules, features, index + currentIndex, fileInfo);
if (context.dumpLineNumbers) {
media.debugInfo = debugInfo;

if (parserInput.$str('@container')) {
return this.prepareAndGetNestableAtRule(tree.Container, index, debugInfo, ContainerSyntaxOptions);
}

return media;
}

parserInput.restore();
},

Expand Down Expand Up @@ -1919,7 +1936,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {

if (parserInput.currentChar() !== '@') { return; }

value = this['import']() || this.plugin() || this.media();
value = this['import']() || this.plugin() || this.nestableAtRule();
if (value) {
return value;
}
Expand Down Expand Up @@ -2422,4 +2439,4 @@ Parser.serializeVars = vars => {
return s;
};

export default Parser;
export default Parser;
7 changes: 7 additions & 0 deletions packages/less/src/less/tree/atrule-syntax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const MediaSyntaxOptions = {
queryInParens: false
};

export const ContainerSyntaxOptions = {
queryInParens: true
};
63 changes: 63 additions & 0 deletions packages/less/src/less/tree/container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Ruleset from './ruleset';
import Value from './value';
import Selector from './selector';
import AtRule from './atrule';
import NestableAtRulePrototype from './nested-at-rule';

const Container = function(value, features, index, currentFileInfo, visibilityInfo) {
this._index = index;
this._fileInfo = currentFileInfo;

const selectors = (new Selector([], null, null, this._index, this._fileInfo)).createEmptySelectors();

this.features = new Value(features);
this.rules = [new Ruleset(selectors, value)];
this.rules[0].allowImports = true;
this.copyVisibilityInfo(visibilityInfo);
this.allowRoot = true;
this.setParent(selectors, this);
this.setParent(this.features, this);
this.setParent(this.rules, this);
};

Container.prototype = Object.assign(new AtRule(), {
type: 'Container',

...NestableAtRulePrototype,

genCSS(context, output) {
output.add('@container ', this._fileInfo, this._index);
this.features.genCSS(context, output);
this.outputRuleset(context, output, this.rules);
},

eval(context) {
if (!context.mediaBlocks) {
context.mediaBlocks = [];
context.mediaPath = [];
}

const media = new Container(null, [], this._index, this._fileInfo, this.visibilityInfo());
if (this.debugInfo) {
this.rules[0].debugInfo = this.debugInfo;
media.debugInfo = this.debugInfo;
}

media.features = this.features.eval(context);

context.mediaPath.push(media);
context.mediaBlocks.push(media);

this.rules[0].functionRegistry = context.frames[0].functionRegistry.inherit();
context.frames.unshift(this.rules[0]);
media.rules = [this.rules[0].eval(context)];
context.frames.shift();

context.mediaPath.pop();

return context.mediaPath.length === 0 ? media.evalTop(context) :
media.evalNested(context);
}
});

export default Container;
7 changes: 5 additions & 2 deletions packages/less/src/less/tree/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ import Value from './value';
import JavaScript from './javascript';
import Assignment from './assignment';
import Condition from './condition';
import QueryInParens from './query-in-parens';
import Paren from './paren';
import Media from './media';
import Container from './container';
import UnicodeDescriptor from './unicode-descriptor';
import Negative from './negative';
import Extend from './extend';
Expand All @@ -43,8 +45,9 @@ export default {
Ruleset, Element, Attribute, Combinator, Selector,
Quoted, Expression, Declaration, Call, URL, Import,
Comment, Anonymous, Value, JavaScript, Assignment,
Condition, Paren, Media, UnicodeDescriptor, Negative,
Extend, VariableCall, NamespaceValue,
Condition, Paren, Media, Container, QueryInParens,
UnicodeDescriptor, Negative, Extend, VariableCall,
NamespaceValue,
mixin: {
Call: MixinCall,
Definition: MixinDefinition
Expand Down
94 changes: 2 additions & 92 deletions packages/less/src/less/tree/media.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import Ruleset from './ruleset';
import Value from './value';
import Selector from './selector';
import Anonymous from './anonymous';
import Expression from './expression';
import AtRule from './atrule';
import * as utils from '../utils';
import NestableAtRulePrototype from './nested-at-rule';

const Media = function(value, features, index, currentFileInfo, visibilityInfo) {
this._index = index;
Expand All @@ -25,18 +23,7 @@ const Media = function(value, features, index, currentFileInfo, visibilityInfo)
Media.prototype = Object.assign(new AtRule(), {
type: 'Media',

isRulesetLike() {
return true;
},

accept(visitor) {
if (this.features) {
this.features = visitor.visit(this.features);
}
if (this.rules) {
this.rules = visitor.visitArray(this.rules);
}
},
...NestableAtRulePrototype,

genCSS(context, output) {
output.add('@media ', this._fileInfo, this._index);
Expand Down Expand Up @@ -70,83 +57,6 @@ Media.prototype = Object.assign(new AtRule(), {

return context.mediaPath.length === 0 ? media.evalTop(context) :
media.evalNested(context);
},

evalTop(context) {
let result = this;

// Render all dependent Media blocks.
if (context.mediaBlocks.length > 1) {
const selectors = (new Selector([], null, null, this.getIndex(), this.fileInfo())).createEmptySelectors();
result = new Ruleset(selectors, context.mediaBlocks);
result.multiMedia = true;
result.copyVisibilityInfo(this.visibilityInfo());
this.setParent(result, this);
}

delete context.mediaBlocks;
delete context.mediaPath;

return result;
},

evalNested(context) {
let i;
let value;
const path = context.mediaPath.concat([this]);

// Extract the media-query conditions separated with `,` (OR).
for (i = 0; i < path.length; i++) {
value = path[i].features instanceof Value ?
path[i].features.value : path[i].features;
path[i] = Array.isArray(value) ? value : [value];
}

// Trace all permutations to generate the resulting media-query.
//
// (a, b and c) with nested (d, e) ->
// a and d
// a and e
// b and c and d
// b and c and e
this.features = new Value(this.permute(path).map(path => {
path = path.map(fragment => fragment.toCSS ? fragment : new Anonymous(fragment));

for (i = path.length - 1; i > 0; i--) {
path.splice(i, 0, new Anonymous('and'));
}

return new Expression(path);
}));
this.setParent(this.features, this);

// Fake a tree-node that doesn't output anything.
return new Ruleset([], []);
},

permute(arr) {
if (arr.length === 0) {
return [];
} else if (arr.length === 1) {
return arr[0];
} else {
const result = [];
const rest = this.permute(arr.slice(1));
for (let i = 0; i < rest.length; i++) {
for (let j = 0; j < arr[0].length; j++) {
result.push([arr[0][j]].concat(rest[i]));
}
}
return result;
}
},

bubbleSelectors(selectors) {
if (!selectors) {
return;
}
this.rules = [new Ruleset(utils.copyArray(selectors), [this.rules[0]])];
this.setParent(this.rules, this);
}
});

Expand Down
Loading

0 comments on commit 856ca74

Please sign in to comment.