Skip to content

feat: container query support via css-tree extension #8275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you expand on that createRequire usage thing? I'm not sure what this is about.

Copy link
Contributor Author

@typhonrt typhonrt Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just repeating info I included in the post below:

So, the Node version of css-tree when including fork has a path that invokes createRequire in data.js:
https://github.com/csstree/csstree/blob/master/lib/data.js#L4-L7

The ESM bundled / browser version of css-tree has all of those resources in the ESM bundle thus doesn't depend on Node specific functionality to import a local JSON file.

When building Svelte for production (not local testing) it causes an issue to use the Node version of css-tree when accessing fork for extension as those JSON files can not be referenced by the production / zero-dependency build of Svelte as the bundling / distribution process doesn't include the internal css-tree JSON files referenced. When not producing a production build css-tree and a couple of other Node dependencies are not bundled / marked external thus it works.

https://github.com/sveltejs/svelte/blob/master/rollup.config.js#L144-L146

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Rollup this sounds ok. Just to double check: Once we go unbundled ESM then can't do this anymore like this. css-tree will be a regular dependency which means we have to use the public imports. But that would be ok because the JSON file will be available then within css-tree (because it's installed as a regular dependency), correct?

Copy link
Contributor Author

@typhonrt typhonrt Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably when going unbundled ESM the dependencies previously bundled that are marked external in the local test build will be moved to package.json dependencies; likely: acorn, css-tree, and magic-string. In this case it will certainly be possible to directly reference the Node import path IE import { fork } from 'css-tree';

Outside of course SvelteKit taking the unbundled ESM approach, some day I'd be glad to learn more about the impetus for native ESM vs TS. I too have always been a native ESM proponent and all my Svelte UI framework / library work is native ESM. I also work on tooling to be able to generate bundled TS declarations from ESM source + JSDoc. Not saying that is applicable in Svelte's future, but a curious conversation overall for another day.

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, ')');
}
Loading