Skip to content

Commit

Permalink
feat: container query support via css-tree extension (#8275)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
typhonrt committed Mar 27, 2023
1 parent d49b568 commit 91e8dfc
Show file tree
Hide file tree
Showing 11 changed files with 607 additions and 3 deletions.
2 changes: 1 addition & 1 deletion src/compiler/compile/css/Stylesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class Atrule {
}

apply(node: Element) {
if (this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') {
if (this.node.name === 'container' || this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') {
this.children.forEach(child => {
child.apply(node);
});
Expand Down
43 changes: 43 additions & 0 deletions src/compiler/parse/read/css-tree-cq/css_tree_parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @ts-nocheck
// Note: Must import from the `css-tree` browser bundled distribution due to `createRequire` usage if importing from
// `css-tree` Node module directly. This allows the production build of Svelte to work correctly.
import { fork } from '../../../../../node_modules/css-tree/dist/csstree.esm.js';

import * as Comparison from './node/comparison';
import * as ContainerFeature from './node/container_feature';
import * as ContainerFeatureRange from './node/container_feature_range';
import * as ContainerFeatureStyle from './node/container_feature_style';
import * as ContainerQuery from './node/container_query';
import * as QueryCSSFunction from './node/query_css_function';

/**
* Extends `css-tree` for container query support by forking and adding new nodes and at-rule support for `@container`.
*
* The new nodes are located in `./node`.
*/
const cqSyntax = fork({
atrule: { // extend or override at-rule dictionary
container: {
parse: {
prelude() {
return this.createSingleNodeList(
this.ContainerQuery()
);
},
block(isStyleBlock = false) {
return this.Block(isStyleBlock);
}
}
}
},
node: { // extend node types
Comparison,
ContainerFeature,
ContainerFeatureRange,
ContainerFeatureStyle,
ContainerQuery,
QueryCSSFunction
}
});

export const parse = cqSyntax.parse;
48 changes: 48 additions & 0 deletions src/compiler/parse/read/css-tree-cq/node/comparison.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @ts-nocheck
import { Delim } from 'css-tree/tokenizer';

export const name = 'Comparison';
export const structure = {
value: String
};

export function parse() {
const start = this.tokenStart;

const char1 = this.consume(Delim);

// The first character in the comparison operator must match '<', '=', or '>'.
if (char1 !== '<' && char1 !== '>' && char1 !== '=') {
this.error('Malformed comparison operator');
}

let char2;

if (this.tokenType === Delim) {
char2 = this.consume(Delim);

// The second character in the comparison operator must match '='.
if (char2 !== '=') {
this.error('Malformed comparison operator');
}
}

// If the next token is also 'Delim' then it is malformed.
if (this.tokenType === Delim) {
this.error('Malformed comparison operator');
}

const value = char2 ? `${char1}${char2}` : char1;

return {
type: 'Comparison',
loc: this.getLocation(start, this.tokenStart),
value
};
}

export function generate(node) {
for (let index = 0; index < node.value.length; index++) {
this.token(Delim, node.value.charAt(index));
}
}
82 changes: 82 additions & 0 deletions src/compiler/parse/read/css-tree-cq/node/container_feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// @ts-nocheck
import {
Ident,
Number,
Dimension,
Function,
LeftParenthesis,
RightParenthesis,
Colon,
Delim
} from 'css-tree/tokenizer';

export const name = 'ContainerFeature';
export const structure = {
name: String,
value: ['Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};

export function parse() {
const start = this.tokenStart;
let value = null;

this.eat(LeftParenthesis);
this.skipSC();

const name = this.consume(Ident);
this.skipSC();

if (this.tokenType !== RightParenthesis) {
this.eat(Colon);
this.skipSC();

switch (this.tokenType) {
case Number:
if (this.lookupNonWSType(1) === Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;

case Dimension:
value = this.Dimension();
break;

case Function:
value = this.QueryCSSFunction();
break;

case Ident:
value = this.Identifier();
break;

default:
this.error('Number, dimension, ratio, function, or identifier is expected');
break;
}

this.skipSC();
}

this.eat(RightParenthesis);

return {
type: 'ContainerFeature',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
}

export function generate(node) {
this.token(LeftParenthesis, '(');
this.token(Ident, node.name);

if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}

this.token(RightParenthesis, ')');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// @ts-nocheck
import {
Ident,
Number,
Delim,
Dimension,
Function,
LeftParenthesis,
RightParenthesis,
WhiteSpace
} from 'css-tree/tokenizer';

export const name = 'ContainerFeatureRange';
export const structure = {
name: String,
value: ['Identifier', 'Number', 'Comparison', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};

function lookup_non_WS_type_and_value(offset, type, referenceStr) {
let current_type;

do {
current_type = this.lookupType(offset++);
if (current_type !== WhiteSpace) {
break;
}
} while (current_type !== 0); // NULL -> 0

return current_type === type ? this.lookupValue(offset - 1, referenceStr) : false;
}

export function parse() {
const children = this.createList();
let child = null;

this.eat(LeftParenthesis);
this.skipSC();

while (!this.eof && this.tokenType !== RightParenthesis) {
switch (this.tokenType) {
case Number:
if (lookup_non_WS_type_and_value.call(this, 1, Delim, '/')) {
child = this.Ratio();
} else {
child = this.Number();
}
break;

case Delim:
child = this.Comparison();
break;

case Dimension:
child = this.Dimension();
break;

case Function:
child = this.QueryCSSFunction();
break;

case Ident:
child = this.Identifier();
break;

default:
this.error('Number, dimension, comparison, ratio, function, or identifier is expected');
break;
}

children.push(child);

this.skipSC();
}

this.eat(RightParenthesis);

return {
type: 'ContainerFeatureRange',
loc: this.getLocationFromList(children),
children
};
}

export function generate(node) {
this.children(node);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// @ts-nocheck
import {
Function,
Ident,
Number,
Dimension,
RightParenthesis,
Colon,
Delim
} from 'css-tree/tokenizer';

export const name = 'ContainerFeatureStyle';
export const structure = {
name: String,
value: ['Function', 'Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};

export function parse() {
const start = this.tokenStart;
let value = null;

const function_name = this.consumeFunctionName();
if (function_name !== 'style') {
this.error('Unknown container style query identifier; "style" is expected');
}

this.skipSC();

const name = this.consume(Ident);
this.skipSC();

if (this.tokenType !== RightParenthesis) {
this.eat(Colon);
this.skipSC();

switch (this.tokenType) {
case Number:
if (this.lookupNonWSType(1) === Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;

case Dimension:
value = this.Dimension();
break;

case Function:
value = this.QueryCSSFunction();
break;

case Ident:
value = this.Identifier();
break;

default:
this.error('Number, dimension, ratio, function or identifier is expected');
break;
}

this.skipSC();
}

this.eat(RightParenthesis);

return {
type: 'ContainerFeatureStyle',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
}

export function generate(node) {
this.token(Function, 'style(');
this.token(Ident, node.name);

if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}

this.token(RightParenthesis, ')');
}

0 comments on commit 91e8dfc

Please sign in to comment.