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..a164e77 100644 --- a/css/parse.go +++ b/css/parse.go @@ -62,6 +62,12 @@ func (tt GrammarType) String() string { //////////////////////////////////////////////////////////////// +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 +153,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 +229,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 := true + for pp.tt != ErrorToken { + 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 { + return p.parseQualifiedRule() + } else if p.tt == IdentToken || p.tt == DelimToken { + return p.parseDeclaration() + } + // parse error p.initBuf() p.l.r.Move(-len(p.data)) @@ -426,7 +477,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/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{"}, 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 {