Skip to content
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

add atContainer rule support #97

Merged
merged 11 commits into from
Feb 22, 2023
16 changes: 16 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -228,6 +228,22 @@ The `@supports` at-rule.
- rules: `Array` of nodes with the types `rule`, `comment` and any of the
at-rule types.

### container

The `@container` at-rule.

- conatiner: `String`. The part following `@container `.
- rules: `Array` of nodes with the types `rule`, `comment` and any of the
at-rule types.

### layer

The `@layer` at-rule.

- layer: `String`. The part following `@layer `.
- rules: `Array` of nodes with the types `rule`, `comment` and any of the
at-rule types. This may be null, if the rule did not contain any.

### Example

CSS:
68 changes: 67 additions & 1 deletion src/parse/index.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import {
CssCharsetAST,
CssCommentAST,
CssCommonPositionAST,
CssContainerAST,
CssCustomMediaAST,
CssDeclarationAST,
CssDocumentAST,
@@ -13,6 +14,7 @@ import {
CssImportAST,
CssKeyframeAST,
CssKeyframesAST,
CssLayerAST,
CssMediaAST,
CssNamespaceAST,
CssPageAST,
@@ -422,6 +424,68 @@ export const parse = (
});
}

/**
* Parse container.
*/
function atcontainer(): CssContainerAST | void {
const pos = position();
const m = match(/^@container *([^{]+)/);

if (!m) {
return;
}
const container = trim(m[1]);

if (!open()) {
return error("@container missing '{'");
}

const style = comments<CssAtRuleAST>().concat(rules());

if (!close()) {
return error("@container missing '}'");
}

return pos<CssContainerAST>({
type: CssTypes.container,
container: container,
rules: style,
});
}

/**
* Parse container.
*/
function atlayer(): CssLayerAST | void {
const pos = position();
const m = match(/^@layer *([^{;@]+)/);

if (!m) {
return;
}
const layer = trim(m[1]);

if (!open()) {
match(/^[;\s]*/);
return pos<CssLayerAST>({
type: CssTypes.layer,
layer: layer,
});
}

const style = comments<CssAtRuleAST>().concat(rules());

if (!close()) {
return error("@layer missing '}'");
}

return pos<CssLayerAST>({
type: CssTypes.layer,
layer: layer,
rules: style,
});
}

/**
* Parse media.
*/
@@ -626,7 +690,9 @@ export const parse = (
atdocument() ||
atpage() ||
athost() ||
atfontface()
atfontface() ||
atcontainer() ||
atlayer()
);
}

58 changes: 54 additions & 4 deletions src/stringify/compiler.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import {
CssCharsetAST,
CssCommentAST,
CssCommonPositionAST,
CssContainerAST,
CssCustomMediaAST,
CssDeclarationAST,
CssDocumentAST,
@@ -11,6 +12,7 @@ import {
CssImportAST,
CssKeyframeAST,
CssKeyframesAST,
CssLayerAST,
CssMediaAST,
CssNamespaceAST,
CssPageAST,
@@ -64,6 +66,8 @@ class Compiler {
return this.declaration(node);
case CssTypes.comment:
return this.comment(node);
case CssTypes.container:
return this.container(node);
case CssTypes.charset:
return this.charset(node);
case CssTypes.document:
@@ -80,6 +84,8 @@ class Compiler {
return this.keyframes(node);
case CssTypes.keyframe:
return this.keyframe(node);
case CssTypes.layer:
return this.layer(node);
case CssTypes.media:
return this.media(node);
case CssTypes.namespace:
@@ -130,6 +136,50 @@ class Compiler {
return this.emit(this.indent() + '/*' + node.comment + '*/', node.position);
}

/**
* Visit container node.
*/
container(node: CssContainerAST) {
if (this.compress) {
return (
this.emit('@container ' + node.container, node.position) +
this.emit('{') +
this.mapVisit(node.rules) +
this.emit('}')
);
}
return (
this.emit(this.indent() + '@container ' + node.container, node.position) +
this.emit(' {\n' + this.indent(1)) +
this.mapVisit(node.rules, '\n\n') +
this.emit('\n' + this.indent(-1) + this.indent() + '}')
);
}

/**
* Visit container node.
*/
layer(node: CssLayerAST) {
if (this.compress) {
return (
this.emit('@layer ' + node.layer, node.position) +
(node.rules
? this.emit('{') +
this.mapVisit(<CssAllNodesAST[]>node.rules) +
this.emit('}')
: ';')
);
}
return (
this.emit(this.indent() + '@layer ' + node.layer, node.position) +
(node.rules
? this.emit(' {\n' + this.indent(1)) +
this.mapVisit(<CssAllNodesAST[]>node.rules, '\n\n') +
this.emit('\n' + this.indent(-1) + this.indent() + '}')
: ';')
);
}

/**
* Visit import node.
*/
@@ -150,10 +200,10 @@ class Compiler {
);
}
return (
this.emit('@media ' + node.media, node.position) +
this.emit(this.indent() + '@media ' + node.media, node.position) +
this.emit(' {\n' + this.indent(1)) +
this.mapVisit(node.rules, '\n\n') +
this.emit(this.indent(-1) + '\n}')
this.emit('\n' + this.indent(-1) + this.indent() + '}')
);
}

@@ -205,10 +255,10 @@ class Compiler {
);
}
return (
this.emit('@supports ' + node.supports, node.position) +
this.emit(this.indent() + '@supports ' + node.supports, node.position) +
this.emit(' {\n' + this.indent(1)) +
this.mapVisit(node.rules, '\n\n') +
this.emit(this.indent(-1) + '\n}')
this.emit('\n' + this.indent(-1) + this.indent() + '}')
);
}

14 changes: 14 additions & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ export enum CssTypes {
rule = 'rule',
declaration = 'declaration',
comment = 'comment',
container = 'container',
charset = 'charset',
document = 'document',
customMedia = 'custom-media',
@@ -14,6 +15,7 @@ export enum CssTypes {
import = 'import',
keyframes = 'keyframes',
keyframe = 'keyframe',
layer = 'layer',
media = 'media',
namespace = 'namespace',
page = 'page',
@@ -54,6 +56,11 @@ export type CssCommentAST = CssCommonPositionAST & {
type: CssTypes.comment;
comment: string;
};
export type CssContainerAST = CssCommonPositionAST & {
type: CssTypes.container;
container: string;
rules: Array<CssAtRuleAST>;
};

export type CssCharsetAST = CssCommonPositionAST & {
type: CssTypes.charset;
@@ -93,6 +100,11 @@ export type CssKeyframeAST = CssCommonPositionAST & {
values: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
export type CssLayerAST = CssCommonPositionAST & {
type: CssTypes.layer;
layer: string;
rules?: Array<CssAtRuleAST>;
};
export type CssMediaAST = CssCommonPositionAST & {
type: CssTypes.media;
media: string;
@@ -116,13 +128,15 @@ export type CssSupportsAST = CssCommonPositionAST & {
export type CssAtRuleAST =
| CssRuleAST
| CssCommentAST
| CssContainerAST
| CssCharsetAST
| CssCustomMediaAST
| CssDocumentAST
| CssFontFaceAST
| CssHostAST
| CssImportAST
| CssKeyframesAST
| CssLayerAST
| CssMediaAST
| CssNamespaceAST
| CssPageAST
17 changes: 17 additions & 0 deletions test/cases.test.ts
Original file line number Diff line number Diff line change
@@ -13,16 +13,25 @@ cases.forEach((name: string) => {

it('should match ast.json', () => {
const ast = parseInput();
if (!fs.existsSync(astFile)) {
writeFile(astFile, JSON.stringify(ast));
}
expect(ast).toMatchObject(JSON.parse(readFile(astFile)));
});

it('should match output.css', () => {
const output = stringify(parseInput());
if (!fs.existsSync(outputFile)) {
writeFile(outputFile, output);
}
expect(output).toBe(readFile(outputFile).trim());
});

it('should match compressed.css', () => {
const compressed = stringify(parseInput(), {compress: true});
if (!fs.existsSync(compressedFile)) {
writeFile(compressedFile, compressed);
}
expect(compressed).toBe(readFile(compressedFile));
});

@@ -41,3 +50,11 @@ function readFile(file: string) {

return src;
}

function writeFile(file: string, text: string) {
// normalize line endings
text = text.replace(/\r\n/, '\n');
// remove trailing newline
text = text.replace(/\n$/, '');
fs.writeFileSync(file, text, 'utf8');
}
1 change: 1 addition & 0 deletions test/cases/container/ast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type":"stylesheet","stylesheet":{"source":"input.css","rules":[{"type":"container","container":"(width > 400px)","rules":[{"type":"rule","selectors":["h2"],"declarations":[{"type":"declaration","property":"font-size","value":"1.5em","position":{"start":{"line":3,"column":5},"end":{"line":3,"column":21},"source":"input.css"}}],"position":{"start":{"line":2,"column":3},"end":{"line":4,"column":4},"source":"input.css"}}],"position":{"start":{"line":1,"column":1},"end":{"line":5,"column":2},"source":"input.css"}},{"type":"container","container":"(width < 650px)","rules":[{"type":"rule","selectors":[".card"],"declarations":[{"type":"declaration","property":"width","value":"50%","position":{"start":{"line":9,"column":5},"end":{"line":9,"column":15},"source":"input.css"}},{"type":"declaration","property":"background-color","value":"gray","position":{"start":{"line":10,"column":5},"end":{"line":10,"column":27},"source":"input.css"}},{"type":"declaration","property":"font-size","value":"1em","position":{"start":{"line":11,"column":5},"end":{"line":11,"column":19},"source":"input.css"}}],"position":{"start":{"line":8,"column":3},"end":{"line":12,"column":4},"source":"input.css"}}],"position":{"start":{"line":7,"column":1},"end":{"line":13,"column":2},"source":"input.css"}},{"type":"container","container":"summary (min-width: 400px)","rules":[{"type":"rule","selectors":[".card"],"declarations":[{"type":"declaration","property":"font-size","value":"1.5em","position":{"start":{"line":17,"column":5},"end":{"line":17,"column":21},"source":"input.css"}}],"position":{"start":{"line":16,"column":3},"end":{"line":18,"column":4},"source":"input.css"}}],"position":{"start":{"line":15,"column":1},"end":{"line":19,"column":2},"source":"input.css"}},{"type":"container","container":"summary (min-width: 400px)","rules":[{"type":"container","container":"(min-width: 800px)","rules":[{"type":"rule","selectors":[".card"],"declarations":[{"type":"declaration","property":"font-size","value":"1.5em","position":{"start":{"line":24,"column":7},"end":{"line":24,"column":23},"source":"input.css"}}],"position":{"start":{"line":23,"column":5},"end":{"line":25,"column":6},"source":"input.css"}}],"position":{"start":{"line":22,"column":3},"end":{"line":26,"column":4},"source":"input.css"}}],"position":{"start":{"line":21,"column":1},"end":{"line":27,"column":2},"source":"input.css"}},{"type":"container","container":"summary (min-width: 400px)","rules":[{"type":"container","container":"(min-width: 800px)","rules":[{"type":"container","container":"(min-width: 900px)","rules":[{"type":"rule","selectors":[".card"],"declarations":[{"type":"declaration","property":"font-size","value":"1.5em","position":{"start":{"line":33,"column":9},"end":{"line":33,"column":25},"source":"input.css"}}],"position":{"start":{"line":32,"column":7},"end":{"line":34,"column":8},"source":"input.css"}}],"position":{"start":{"line":31,"column":5},"end":{"line":35,"column":6},"source":"input.css"}}],"position":{"start":{"line":30,"column":3},"end":{"line":36,"column":4},"source":"input.css"}}],"position":{"start":{"line":29,"column":1},"end":{"line":37,"column":2},"source":"input.css"}}],"parsingErrors":[]}}
1 change: 1 addition & 0 deletions test/cases/container/compressed.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions test/cases/container/input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@container (width > 400px) {
h2 {
font-size: 1.5em;
}
}

@container (width < 650px) {
.card {
width: 50%;
background-color: gray;
font-size: 1em;
}
}

@container summary (min-width: 400px) {
.card {
font-size: 1.5em;
}
}

@container summary (min-width: 400px) {
@container (min-width: 800px) {
.card {
font-size: 1.5em;
}
}
}

@container summary (min-width: 400px) {
@container (min-width: 800px) {
@container (min-width: 900px) {
.card {
font-size: 1.5em;
}
}
}
}
Loading