Skip to content

Commit 91e8dfc

Browse files
authored
feat: container query support via css-tree extension (#8275)
Closes #6969 As discussed there, container query support is quite useful to add to Svelte as it is now broadly available with Firefox releasing support imminently w/ FF v110 this upcoming week (~Feb 14th). Chrome has had support since ~Aug '22. The central issue is that css-tree which is a dependency for CSS AST parsing is significantly lagging behind on adding more recent features such as container query support. Ample time has been given to the maintainer to update css-tree and I do have every confidence that in time css-tree will receive a new major version with all sorts of modern CSS syntax supported including container queries. This PR provides an interim solution for what Svelte needs to support container queries now.
1 parent d49b568 commit 91e8dfc

File tree

11 files changed

+607
-3
lines changed

11 files changed

+607
-3
lines changed

src/compiler/compile/css/Stylesheet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ class Atrule {
173173
}
174174

175175
apply(node: Element) {
176-
if (this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') {
176+
if (this.node.name === 'container' || this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') {
177177
this.children.forEach(child => {
178178
child.apply(node);
179179
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// @ts-nocheck
2+
// Note: Must import from the `css-tree` browser bundled distribution due to `createRequire` usage if importing from
3+
// `css-tree` Node module directly. This allows the production build of Svelte to work correctly.
4+
import { fork } from '../../../../../node_modules/css-tree/dist/csstree.esm.js';
5+
6+
import * as Comparison from './node/comparison';
7+
import * as ContainerFeature from './node/container_feature';
8+
import * as ContainerFeatureRange from './node/container_feature_range';
9+
import * as ContainerFeatureStyle from './node/container_feature_style';
10+
import * as ContainerQuery from './node/container_query';
11+
import * as QueryCSSFunction from './node/query_css_function';
12+
13+
/**
14+
* Extends `css-tree` for container query support by forking and adding new nodes and at-rule support for `@container`.
15+
*
16+
* The new nodes are located in `./node`.
17+
*/
18+
const cqSyntax = fork({
19+
atrule: { // extend or override at-rule dictionary
20+
container: {
21+
parse: {
22+
prelude() {
23+
return this.createSingleNodeList(
24+
this.ContainerQuery()
25+
);
26+
},
27+
block(isStyleBlock = false) {
28+
return this.Block(isStyleBlock);
29+
}
30+
}
31+
}
32+
},
33+
node: { // extend node types
34+
Comparison,
35+
ContainerFeature,
36+
ContainerFeatureRange,
37+
ContainerFeatureStyle,
38+
ContainerQuery,
39+
QueryCSSFunction
40+
}
41+
});
42+
43+
export const parse = cqSyntax.parse;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// @ts-nocheck
2+
import { Delim } from 'css-tree/tokenizer';
3+
4+
export const name = 'Comparison';
5+
export const structure = {
6+
value: String
7+
};
8+
9+
export function parse() {
10+
const start = this.tokenStart;
11+
12+
const char1 = this.consume(Delim);
13+
14+
// The first character in the comparison operator must match '<', '=', or '>'.
15+
if (char1 !== '<' && char1 !== '>' && char1 !== '=') {
16+
this.error('Malformed comparison operator');
17+
}
18+
19+
let char2;
20+
21+
if (this.tokenType === Delim) {
22+
char2 = this.consume(Delim);
23+
24+
// The second character in the comparison operator must match '='.
25+
if (char2 !== '=') {
26+
this.error('Malformed comparison operator');
27+
}
28+
}
29+
30+
// If the next token is also 'Delim' then it is malformed.
31+
if (this.tokenType === Delim) {
32+
this.error('Malformed comparison operator');
33+
}
34+
35+
const value = char2 ? `${char1}${char2}` : char1;
36+
37+
return {
38+
type: 'Comparison',
39+
loc: this.getLocation(start, this.tokenStart),
40+
value
41+
};
42+
}
43+
44+
export function generate(node) {
45+
for (let index = 0; index < node.value.length; index++) {
46+
this.token(Delim, node.value.charAt(index));
47+
}
48+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// @ts-nocheck
2+
import {
3+
Ident,
4+
Number,
5+
Dimension,
6+
Function,
7+
LeftParenthesis,
8+
RightParenthesis,
9+
Colon,
10+
Delim
11+
} from 'css-tree/tokenizer';
12+
13+
export const name = 'ContainerFeature';
14+
export const structure = {
15+
name: String,
16+
value: ['Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
17+
};
18+
19+
export function parse() {
20+
const start = this.tokenStart;
21+
let value = null;
22+
23+
this.eat(LeftParenthesis);
24+
this.skipSC();
25+
26+
const name = this.consume(Ident);
27+
this.skipSC();
28+
29+
if (this.tokenType !== RightParenthesis) {
30+
this.eat(Colon);
31+
this.skipSC();
32+
33+
switch (this.tokenType) {
34+
case Number:
35+
if (this.lookupNonWSType(1) === Delim) {
36+
value = this.Ratio();
37+
} else {
38+
value = this.Number();
39+
}
40+
break;
41+
42+
case Dimension:
43+
value = this.Dimension();
44+
break;
45+
46+
case Function:
47+
value = this.QueryCSSFunction();
48+
break;
49+
50+
case Ident:
51+
value = this.Identifier();
52+
break;
53+
54+
default:
55+
this.error('Number, dimension, ratio, function, or identifier is expected');
56+
break;
57+
}
58+
59+
this.skipSC();
60+
}
61+
62+
this.eat(RightParenthesis);
63+
64+
return {
65+
type: 'ContainerFeature',
66+
loc: this.getLocation(start, this.tokenStart),
67+
name,
68+
value
69+
};
70+
}
71+
72+
export function generate(node) {
73+
this.token(LeftParenthesis, '(');
74+
this.token(Ident, node.name);
75+
76+
if (node.value !== null) {
77+
this.token(Colon, ':');
78+
this.node(node.value);
79+
}
80+
81+
this.token(RightParenthesis, ')');
82+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// @ts-nocheck
2+
import {
3+
Ident,
4+
Number,
5+
Delim,
6+
Dimension,
7+
Function,
8+
LeftParenthesis,
9+
RightParenthesis,
10+
WhiteSpace
11+
} from 'css-tree/tokenizer';
12+
13+
export const name = 'ContainerFeatureRange';
14+
export const structure = {
15+
name: String,
16+
value: ['Identifier', 'Number', 'Comparison', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
17+
};
18+
19+
function lookup_non_WS_type_and_value(offset, type, referenceStr) {
20+
let current_type;
21+
22+
do {
23+
current_type = this.lookupType(offset++);
24+
if (current_type !== WhiteSpace) {
25+
break;
26+
}
27+
} while (current_type !== 0); // NULL -> 0
28+
29+
return current_type === type ? this.lookupValue(offset - 1, referenceStr) : false;
30+
}
31+
32+
export function parse() {
33+
const children = this.createList();
34+
let child = null;
35+
36+
this.eat(LeftParenthesis);
37+
this.skipSC();
38+
39+
while (!this.eof && this.tokenType !== RightParenthesis) {
40+
switch (this.tokenType) {
41+
case Number:
42+
if (lookup_non_WS_type_and_value.call(this, 1, Delim, '/')) {
43+
child = this.Ratio();
44+
} else {
45+
child = this.Number();
46+
}
47+
break;
48+
49+
case Delim:
50+
child = this.Comparison();
51+
break;
52+
53+
case Dimension:
54+
child = this.Dimension();
55+
break;
56+
57+
case Function:
58+
child = this.QueryCSSFunction();
59+
break;
60+
61+
case Ident:
62+
child = this.Identifier();
63+
break;
64+
65+
default:
66+
this.error('Number, dimension, comparison, ratio, function, or identifier is expected');
67+
break;
68+
}
69+
70+
children.push(child);
71+
72+
this.skipSC();
73+
}
74+
75+
this.eat(RightParenthesis);
76+
77+
return {
78+
type: 'ContainerFeatureRange',
79+
loc: this.getLocationFromList(children),
80+
children
81+
};
82+
}
83+
84+
export function generate(node) {
85+
this.children(node);
86+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// @ts-nocheck
2+
import {
3+
Function,
4+
Ident,
5+
Number,
6+
Dimension,
7+
RightParenthesis,
8+
Colon,
9+
Delim
10+
} from 'css-tree/tokenizer';
11+
12+
export const name = 'ContainerFeatureStyle';
13+
export const structure = {
14+
name: String,
15+
value: ['Function', 'Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
16+
};
17+
18+
export function parse() {
19+
const start = this.tokenStart;
20+
let value = null;
21+
22+
const function_name = this.consumeFunctionName();
23+
if (function_name !== 'style') {
24+
this.error('Unknown container style query identifier; "style" is expected');
25+
}
26+
27+
this.skipSC();
28+
29+
const name = this.consume(Ident);
30+
this.skipSC();
31+
32+
if (this.tokenType !== RightParenthesis) {
33+
this.eat(Colon);
34+
this.skipSC();
35+
36+
switch (this.tokenType) {
37+
case Number:
38+
if (this.lookupNonWSType(1) === Delim) {
39+
value = this.Ratio();
40+
} else {
41+
value = this.Number();
42+
}
43+
break;
44+
45+
case Dimension:
46+
value = this.Dimension();
47+
break;
48+
49+
case Function:
50+
value = this.QueryCSSFunction();
51+
break;
52+
53+
case Ident:
54+
value = this.Identifier();
55+
break;
56+
57+
default:
58+
this.error('Number, dimension, ratio, function or identifier is expected');
59+
break;
60+
}
61+
62+
this.skipSC();
63+
}
64+
65+
this.eat(RightParenthesis);
66+
67+
return {
68+
type: 'ContainerFeatureStyle',
69+
loc: this.getLocation(start, this.tokenStart),
70+
name,
71+
value
72+
};
73+
}
74+
75+
export function generate(node) {
76+
this.token(Function, 'style(');
77+
this.token(Ident, node.name);
78+
79+
if (node.value !== null) {
80+
this.token(Colon, ':');
81+
this.node(node.value);
82+
}
83+
84+
this.token(RightParenthesis, ')');
85+
}

0 commit comments

Comments
 (0)