diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index 4d6056c4..7bc09a3f 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -98,7 +98,8 @@ export enum NodeType { Layer, LayerNameList, LayerName, - PropertyAtRule + PropertyAtRule, + Container } export enum ReferenceType { @@ -1239,6 +1240,17 @@ export class Document extends BodyDeclaration { } } +export class Container extends BodyDeclaration { + + constructor(offset: number, length: number) { + super(offset, length); + } + + public get type(): NodeType { + return NodeType.Container; + } +} + export class Medialist extends Node { constructor(offset: number, length: number) { super(offset, length); diff --git a/src/parser/cssParser.ts b/src/parser/cssParser.ts index fc25df56..22b0c371 100644 --- a/src/parser/cssParser.ts +++ b/src/parser/cssParser.ts @@ -325,6 +325,7 @@ export class Parser { || this._parseViewPort() || this._parseNamespace() || this._parseDocument() + || this._parseContainer() || this._parseUnknownAtRule(); } @@ -1292,6 +1293,110 @@ export class Parser { return this._parseBody(node, this._parseStylesheetStatement.bind(this)); } + public _parseContainer(): nodes.Node | null { + if (!this.peekKeyword('@container')) { + return null; + } + const node = this.create(nodes.Container); + this.consumeToken(); // @container + + node.addChild(this._parseIdent()); // optional container name + node.addChild(this._parseContainerQuery()); + + return this._parseBody(node, this._parseStylesheetStatement.bind(this)); + } + + public _parseContainerQuery(): nodes.Node | null { + // = not + // | [ [ and ]* | [ or ]* ] + const node = this.create(nodes.Node); + if (this.acceptIdent('not')) { + node.addChild(this._parseContainerQueryInParens()); + } else { + node.addChild(this._parseContainerQueryInParens()); + if (this.peekIdent('and')) { + while (this.acceptIdent('and')) { + node.addChild(this._parseContainerQueryInParens()); + } + } else if (this.peekIdent('or')) { + while (this.acceptIdent('or')) { + node.addChild(this._parseContainerQueryInParens()); + } + } + } + return this.finish(node); + } + + public _parseContainerQueryInParens(): nodes.Node { + // = ( ) + // | ( ) + // | style( ) + // | + const node = this.create(nodes.Node); + if (this.accept(TokenType.ParenthesisL)) { + if (this.peekIdent('not') || this.peek(TokenType.ParenthesisL)) { + node.addChild(this._parseContainerQuery()); + } else { + node.addChild(this._parseMediaFeature()); + } + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.CurlyL]); + } + } else if (this.acceptIdent('style')) { + if (this.hasWhitespace() || !this.accept(TokenType.ParenthesisL)) { + return this.finish(node, ParseError.LeftParenthesisExpected, [], [TokenType.CurlyL]); + } + node.addChild(this._parseStyleQuery()); + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.CurlyL]); + } + } else { + return this.finish(node, ParseError.LeftParenthesisExpected, [], [TokenType.CurlyL]); + } + return this.finish(node); + } + + public _parseStyleQuery(): nodes.Node { + // = not + // | [ [ and ]* | [ or ]* ] + // | + // = ( ) + // | ( ) + // | + const node = this.create(nodes.Node); + + if (this.acceptIdent('not')) { + node.addChild(this._parseStyleInParens()); + } else if (this.peek(TokenType.ParenthesisL)) { + node.addChild(this._parseStyleInParens()); + if (this.peekIdent('and')) { + while (this.acceptIdent('and')) { + node.addChild(this._parseStyleInParens()); + } + } else if (this.peekIdent('or')) { + while (this.acceptIdent('or')) { + node.addChild(this._parseStyleInParens()); + } + } + } else { + node.addChild(this._parseDeclaration([TokenType.ParenthesisR])); + } + return this.finish(node); + } + + public _parseStyleInParens(): nodes.Node { + const node = this.create(nodes.Node); + if (this.accept(TokenType.ParenthesisL)) { + node.addChild(this._parseStyleQuery()); + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.CurlyL]); + } + } else { + return this.finish(node, ParseError.LeftParenthesisExpected, [], [TokenType.CurlyL]); + } + return this.finish(node); + } + // https://www.w3.org/TR/css-syntax-3/#consume-an-at-rule public _parseUnknownAtRule(): nodes.Node | null { if (!this.peek(TokenType.AtKeyword)) { diff --git a/src/test/css/parser.test.ts b/src/test/css/parser.test.ts index b53702a9..2cf0ead6 100644 --- a/src/test/css/parser.test.ts +++ b/src/test/css/parser.test.ts @@ -170,6 +170,13 @@ suite('CSS - Parser', () => { assertError(`@property { }`, parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected); }); + test('@container', function () { + const parser = new Parser(); + assertNode(`@container (width <= 150px) { #inner { background-color: skyblue; }}`, parser, parser._parseStylesheet.bind(parser)); + assertNode(`@container card (inline-size > 30em) and style(--responsive: true) { }`, parser, parser._parseStylesheet.bind(parser)); + assertNode(`@container card (inline-size > 30em) { @container style(--responsive: true) {} }`, parser, parser._parseStylesheet.bind(parser)); + }); + test('@import', function () { const parser = new Parser(); assertNode('@import "asdasdsa"', parser, parser._parseImport.bind(parser));