From 794232cad9ef3cccd7d9d8dfde1de584c1df61ac Mon Sep 17 00:00:00 2001 From: OverflowCat Date: Thu, 3 Jul 2025 07:13:55 +0800 Subject: [PATCH 1/3] CSS: support CSS Nesting --- css/lex.go | 7 ++++++ css/parse.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++++--- input.go | 9 ++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/css/lex.go b/css/lex.go index 3d1ff7e..a349334 100644 --- a/css/lex.go +++ b/css/lex.go @@ -140,6 +140,13 @@ type Lexer struct { r *parse.Input } +func CloneLexer(l *Lexer) *Lexer { + rr := parse.CloneInput(l.r) + return &Lexer{ + r: rr, + } +} + // NewLexer returns a new Lexer for a given io.Reader. func NewLexer(r *parse.Input) *Lexer { return &Lexer{ diff --git a/css/parse.go b/css/parse.go index 381db41..05a1504 100644 --- a/css/parse.go +++ b/css/parse.go @@ -27,6 +27,8 @@ const ( BeginRulesetGrammar EndRulesetGrammar DeclarationGrammar + BeginNestedRuleGrammar + EndNestedRuleGrammar TokenGrammar CustomPropertyGrammar ) @@ -56,12 +58,22 @@ func (tt GrammarType) String() string { return "Token" case CustomPropertyGrammar: return "CustomProperty" + case BeginNestedRuleGrammar: + return "BeginNestedRule" + case EndNestedRuleGrammar: + return "EndNestedRule" } return "Invalid(" + strconv.Itoa(int(tt)) + ")" } //////////////////////////////////////////////////////////////// +func isCombinator(data byte) bool { + return data == ',' || data == '/' || data == ':' || data == '!' || data == '=' +} + +//////////////////////////////////////////////////////////////// + // State is the state function the parser currently is in. type State func(*Parser) GrammarType @@ -147,6 +159,22 @@ func (p *Parser) Values() []Token { return p.buf } +func (p *Parser) CloneParser() *Parser { + return &Parser{ + l: CloneLexer(p.l), + state: append([]State{}, p.state...), + err: p.err, + buf: append([]Token{}, p.buf...), + data: append([]byte{}, p.data...), + tt: p.tt, + keepWS: p.keepWS, + prevWS: p.prevWS, + prevEnd: p.prevEnd, + prevComment: p.prevComment, + level: p.level, + } +} + func (p *Parser) popToken(allowComment bool) (TokenType, []byte) { p.prevWS = false p.prevComment = false @@ -207,12 +235,41 @@ func (p *Parser) parseDeclarationList() GrammarType { return ErrorGrammar } else if p.tt == AtKeywordToken { return p.parseAtRule() - } else if p.tt == IdentToken || p.tt == DelimToken { - return p.parseDeclaration() } else if p.tt == CustomPropertyNameToken { return p.parseCustomProperty() } + pp := p.CloneParser() + // peek until we find a colon or semicolon or data length is 1 and is a combinator -> + // not a child rule, but a declaration + isDeclaration := false + for { + if pp.tt == SemicolonToken || pp.tt == RightBraceToken { + isDeclaration = true + break + } else if pp.tt == LeftBraceToken { + isDeclaration = false + break + } + if len(pp.data) == 1 { + if pp.data[0] == '&' { + isDeclaration = false + break + } + if isCombinator(pp.data[0]) { + isDeclaration = true + break + } + } + pp.tt, pp.data = pp.popToken(false) + } + + if isDeclaration && (p.tt == IdentToken || p.tt == DelimToken) { + return p.parseDeclaration() + } else { + return p.parseQualifiedRule() + } + // parse error p.initBuf() p.l.r.Move(-len(p.data)) @@ -426,7 +483,7 @@ func (p *Parser) parseDeclaration() GrammarType { } p.level-- } - if len(data) == 1 && (data[0] == ',' || data[0] == '/' || data[0] == ':' || data[0] == '!' || data[0] == '=') { + if len(data) == 1 && isCombinator(data[0]) { skipWS = true } else if (p.prevWS || p.prevComment) && !skipWS { p.pushBuf(WhitespaceToken, wsBytes) diff --git a/input.go b/input.go index 586ad73..d1489b9 100644 --- a/input.go +++ b/input.go @@ -74,6 +74,15 @@ func NewInputBytes(b []byte) *Input { return z } +func CloneInput(z *Input) *Input { + return &Input{ + buf: append([]byte{}, z.buf...), + pos: z.pos, + start: z.start, + err: z.err, + } +} + // Restore restores the replaced byte past the end of the buffer by NULL. func (z *Input) Restore() { if z.restore != nil { From edf4f2bd96f92904004af2b179ab7bc9aa3c95d1 Mon Sep 17 00:00:00 2001 From: OverflowCat Date: Thu, 3 Jul 2025 07:14:45 +0800 Subject: [PATCH 2/3] CSS: fix `TestParseError/}`, etc. --- css/parse.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/css/parse.go b/css/parse.go index 05a1504..b6b2992 100644 --- a/css/parse.go +++ b/css/parse.go @@ -264,10 +264,10 @@ func (p *Parser) parseDeclarationList() GrammarType { pp.tt, pp.data = pp.popToken(false) } - if isDeclaration && (p.tt == IdentToken || p.tt == DelimToken) { - return p.parseDeclaration() - } else { + if !isDeclaration { return p.parseQualifiedRule() + } else if p.tt == IdentToken || p.tt == DelimToken { + return p.parseDeclaration() } // parse error From be0de24bd47e627069da546667bfc9aefd89aff3 Mon Sep 17 00:00:00 2001 From: OverflowCat Date: Thu, 3 Jul 2025 07:41:33 +0800 Subject: [PATCH 3/3] CSS: fix dead loop & correct a testcase --- css/parse.go | 10 ++-------- css/parse_test.go | 4 +++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/css/parse.go b/css/parse.go index b6b2992..a164e77 100644 --- a/css/parse.go +++ b/css/parse.go @@ -27,8 +27,6 @@ const ( BeginRulesetGrammar EndRulesetGrammar DeclarationGrammar - BeginNestedRuleGrammar - EndNestedRuleGrammar TokenGrammar CustomPropertyGrammar ) @@ -58,10 +56,6 @@ func (tt GrammarType) String() string { return "Token" case CustomPropertyGrammar: return "CustomProperty" - case BeginNestedRuleGrammar: - return "BeginNestedRule" - case EndNestedRuleGrammar: - return "EndNestedRule" } return "Invalid(" + strconv.Itoa(int(tt)) + ")" } @@ -242,8 +236,8 @@ func (p *Parser) parseDeclarationList() GrammarType { pp := p.CloneParser() // peek until we find a colon or semicolon or data length is 1 and is a combinator -> // not a child rule, but a declaration - isDeclaration := false - for { + isDeclaration := true + for pp.tt != ErrorToken { if pp.tt == SemicolonToken || pp.tt == RightBraceToken { isDeclaration = true break diff --git a/css/parse_test.go b/css/parse_test.go index 471d47c..a2b638c 100644 --- a/css/parse_test.go +++ b/css/parse_test.go @@ -39,6 +39,8 @@ func TestParse(t *testing.T) { {false, "@media { @viewport ; }", "@media{@viewport;}"}, {false, "@keyframes 'diagonal-slide' { from { left: 0; top: 0; } to { left: 100px; top: 100px; } }", "@keyframes 'diagonal-slide'{from{left:0;top:0;}to{left:100px;top:100px;}}"}, {false, "@keyframes movingbox{0%{left:90%;}50%{left:10%;}100%{left:90%;}}", "@keyframes movingbox{0%{left:90%;}50%{left:10%;}100%{left:90%;}}"}, + {false, "a { &:hover { color: #f4a; } }", "a{&:hover{color:#f4a;}}"}, + {false, ".foo { .bar > &:hover span { backgroud: orange } ; }", ".foo{.bar>&:hover span{backgroud:orange;}}"}, {false, ".foo { color: #fff;}", ".foo{color:#fff;}"}, {false, ".foo { ; _color: #fff;}", ".foo{_color:#fff;}"}, {false, "a { color: red; border: 0; }", "a{color:red;border:0;}"}, @@ -81,7 +83,7 @@ func TestParse(t *testing.T) { {false, "[class*=\"column\"]+[class*=\"column\"]:last-child{a:b;}", "[class*=\"column\"]+[class*=\"column\"]:last-child{a:b;}"}, {false, "@media { @viewport }", "@media{@viewport;}"}, {false, "table { @unknown }", "table{@unknown;}"}, - {false, "a{@media{width:70%;} b{width:60%;}}", "a{@media{ERROR(width:70%;})ERROR(b{width:60%;})}"}, + {false, "a{@media{width:70%;} b{width:60%;}}", "a{@media{ERROR(width:70%;})b{width:60%;}}"}, // early endings {false, "selector{", "selector{"},