From 7998cab7c05c57fc009ddec817a3a58ed6946b22 Mon Sep 17 00:00:00 2001 From: gucio321 Date: Fri, 30 May 2025 09:15:56 +0200 Subject: [PATCH 1/7] tests: detect multiple style names in one stmt in short, detect missing ; --- parser.go | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/parser.go b/parser.go index 6ae246e..9844891 100644 --- a/parser.go +++ b/parser.go @@ -10,6 +10,8 @@ import ( "text/scanner" ) +var InvalidCSSError = errors.New("invalid CSS") + //go:generate stringer -type=tokenType type tokenType int @@ -130,6 +132,7 @@ func parse(l *list.List) (map[Rule]map[string]string, error) { selector string isBlock bool + isValue bool // Parsed styles. css = make(map[Rule]map[string]string) @@ -150,31 +153,45 @@ func parse(l *list.List) (map[Rule]map[string]string, error) { rule = append(rule, token.value) case tokenSelector: rule = append(rule, selector+token.value) - case tokenBlockStart, tokenStatementEnd: + case tokenBlockStart, tokenStatementEnd: // { or ; style = token.value case tokenStyleSeparator: + if isValue { // multiple separators without ; + return css, fmt.Errorf("line %d: multiple style names before value: %w", token.pos.Line, InvalidCSSError) + } + + isValue = true value = token.value case tokenValue: - rule = append(rule, token.value) + if !isBlock { // descendant selector + rule = append(rule, token.value) + } else { // technically, this could mean we put multiple style values. + if !isValue { // want to parse multiple style names? denied. + return css, fmt.Errorf("line %d: expected only one name before value: %w", token.pos.Line, InvalidCSSError) + } + + value += " " + token.value + } default: - return css, fmt.Errorf("line %d: invalid syntax", token.pos.Line) + return css, fmt.Errorf("line %d: invalid syntax: %w", token.pos.Line, InvalidCSSError) } case tokenSelector: selector = token.value case tokenBlockStart: if prevToken != tokenValue { - return css, fmt.Errorf("line %d: block is missing rule identifier", token.pos.Line) + return css, fmt.Errorf("line %d: block is missing rule identifier: %w", token.pos.Line, InvalidCSSError) } isBlock = true + isValue = false case tokenStatementEnd: - // fmt.Printf("prevToken: %v, style: %v, value: %v\n", prevToken, style, value) if prevToken != tokenValue || style == "" || value == "" { - return css, fmt.Errorf("line %d: expected style before semicolon", token.pos.Line) + return css, fmt.Errorf("line %d: expected style before semicolon: %w", token.pos.Line, InvalidCSSError) } styles[style] = value + isValue = false case tokenBlockEnd: if !isBlock { - return css, fmt.Errorf("line %d: rule block ends without a beginning", token.pos.Line) + return css, fmt.Errorf("line %d: rule block ends without a beginning: %w", token.pos.Line, InvalidCSSError) } for i := range rule { From 0692e99c206d14ccdbc9a41c327259c7ffb3ebc9 Mon Sep 17 00:00:00 2001 From: gucio321 Date: Fri, 30 May 2025 09:19:16 +0200 Subject: [PATCH 2/7] parser: add support for descendant rules rule1 rule2 { style1: style2; } --- parser.go | 2 +- parser_test.go | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/parser.go b/parser.go index 9844891..55e64bc 100644 --- a/parser.go +++ b/parser.go @@ -164,7 +164,7 @@ func parse(l *list.List) (map[Rule]map[string]string, error) { value = token.value case tokenValue: if !isBlock { // descendant selector - rule = append(rule, token.value) + rule[len(rule)-1] += " " + token.value } else { // technically, this could mean we put multiple style values. if !isValue { // want to parse multiple style names? denied. return css, fmt.Errorf("line %d: expected only one name before value: %w", token.pos.Line, InvalidCSSError) diff --git a/parser_test.go b/parser_test.go index 16ad22a..3af6aa4 100644 --- a/parser_test.go +++ b/parser_test.go @@ -38,6 +38,10 @@ rule1 { background-repeat: repeat-x; }` + ex6 := `rule1 descendant { + style1:value1; +}` + cases := []struct { name string CSS string @@ -75,6 +79,11 @@ rule1 { "background-repeat": "repeat-x", }, }}, + {"Descendant selector", ex6, map[Rule]map[string]string{ + "rule1 descendant": { + "style1": "value1", + }, + }}, } for _, tt := range cases { @@ -117,8 +126,7 @@ rule { }{ {"Missing rule", ex1}, {"Missing style", ex2}, - // TODO: this hsould not crash - //{"Statement Missing Semicolon", ex3}, + {"Statement Missing Semicolon", ex3}, {"BlockEndsWithoutBeginning", ex4}, } From b7508afb6cec92a38a122916f0c3060892ebd88c Mon Sep 17 00:00:00 2001 From: gucio321 Date: Fri, 30 May 2025 09:37:53 +0200 Subject: [PATCH 3/7] add comments support --- parser.go | 42 +++++++++++++++++++++++++++++++++--------- parser_test.go | 18 +++++++++++++++++- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/parser.go b/parser.go index 55e64bc..9c85db0 100644 --- a/parser.go +++ b/parser.go @@ -25,6 +25,8 @@ const ( tokenSelector tokenStyleSeparator tokenStatementEnd + tokenCommentStart + tokenCommentEnd ) type tokenEntry struct { @@ -34,12 +36,14 @@ type tokenEntry struct { func newTokenType(typ string) tokenType { types := map[string]tokenType{ - "{": tokenBlockStart, - "}": tokenBlockEnd, - ":": tokenStyleSeparator, - ";": tokenStatementEnd, - ".": tokenSelector, - "#": tokenSelector, + "{": tokenBlockStart, + "}": tokenBlockEnd, + ":": tokenStyleSeparator, + ";": tokenStatementEnd, + ".": tokenSelector, + "#": tokenSelector, + "/*": tokenCommentStart, + "*/": tokenCommentEnd, } result, ok := types[typ] @@ -131,8 +135,9 @@ func parse(l *list.List) (map[Rule]map[string]string, error) { value string selector string - isBlock bool - isValue bool + isBlock bool + isValue bool + isComment bool // Parsed styles. css = make(map[Rule]map[string]string) @@ -145,7 +150,26 @@ func parse(l *list.List) (map[Rule]map[string]string, error) { for e := l.Front(); e != nil; e = l.Front() { token := e.Value.(tokenEntry) l.Remove(e) - // fmt.Printf("typ: %s, value: %q, prevToken: %v\n", token.typ(), token.value, prevToken) + + // handle comment - we continue after this because we don't want to override prevToken + switch token.typ() { + case tokenCommentStart: + isComment = true + continue + case tokenCommentEnd: + // handle standalone endComment token + if !isComment { + return css, fmt.Errorf("line %d: unexpected end of comment: %w", token.pos.Line, InvalidCSSError) + } + + isComment = false + continue + } + + if isComment { // skip everything regardless what it is if processing in comment mode + continue + } + switch token.typ() { case tokenValue: switch prevToken { diff --git a/parser_test.go b/parser_test.go index 3af6aa4..a7215e8 100644 --- a/parser_test.go +++ b/parser_test.go @@ -42,6 +42,11 @@ rule1 { style1:value1; }` + ex7 := `rule1 { + /* this is a comment */ + style: value; +}` + cases := []struct { name string CSS string @@ -84,6 +89,11 @@ rule1 { "style1": "value1", }, }}, + {"Comment in rule", ex7, map[Rule]map[string]string{ + "rule1": { + "style": "value", + }, + }}, } for _, tt := range cases { @@ -118,7 +128,12 @@ rule { style1: value1; style2:; }` - _ = ex3 + + ex5 := ` +body { + style1:value1; + */ +}` cases := []struct { name string @@ -128,6 +143,7 @@ rule { {"Missing style", ex2}, {"Statement Missing Semicolon", ex3}, {"BlockEndsWithoutBeginning", ex4}, + {"Unexpected end of comment", ex5}, } for _, tt := range cases { From 10238c715b2566391accdeacf5a5a91f803cbbc9 Mon Sep 17 00:00:00 2001 From: gucio321 Date: Fri, 30 May 2025 09:39:57 +0200 Subject: [PATCH 4/7] tests: remove TestParseSelectors according to https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_combinator the test was invalid. --- parser_test.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/parser_test.go b/parser_test.go index a7215e8..8dc79d1 100644 --- a/parser_test.go +++ b/parser_test.go @@ -155,28 +155,6 @@ body { } } -func TestParseSelectors(t *testing.T) { - ex1 := `.rule { - style1: value1; - style2: value2; -} -#rule1 sad asd { - style3: value3; - style4: value4; -}` - - css, err := Unmarshal([]byte(ex1)) - if err != nil { - t.Fatal(err) - } - if _, ok := css[".rule"]; !ok { - t.Fatal("Missing '.rule' rule") - } - if _, ok := css["#rule1"]; !ok { - t.Fatal("Missing '.rule' rule") - } -} - func TestParseSelectorGroup(t *testing.T) { ex1 := `.rule1 #rule2 rule3 { style1: value1; From 1e51b6732d6bb92ff27add344aac634f5cda4d1a Mon Sep 17 00:00:00 2001 From: gucio321 Date: Fri, 30 May 2025 09:55:36 +0200 Subject: [PATCH 5/7] run go generate --- tokentype_string.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tokentype_string.go b/tokentype_string.go index c6840e4..3f9e953 100644 --- a/tokentype_string.go +++ b/tokentype_string.go @@ -16,11 +16,13 @@ func _() { _ = x[tokenSelector-4] _ = x[tokenStyleSeparator-5] _ = x[tokenStatementEnd-6] + _ = x[tokenCommentStart-7] + _ = x[tokenCommentEnd-8] } -const _tokenType_name = "tokenFirstTokentokenBlockStarttokenBlockEndtokenRuleNametokenValuetokenSelectortokenStyleSeparatortokenStatementEnd" +const _tokenType_name = "tokenFirstTokentokenBlockStarttokenBlockEndtokenRuleNametokenValuetokenSelectortokenStyleSeparatortokenStatementEndtokenCommentStarttokenCommentEnd" -var _tokenType_index = [...]uint8{0, 15, 30, 43, 56, 66, 79, 98, 115} +var _tokenType_index = [...]uint8{0, 15, 30, 43, 56, 66, 79, 98, 115, 132, 147} func (i tokenType) String() string { i -= -1 From 3094a707aede35384bb938d7d3008665947841c8 Mon Sep 17 00:00:00 2001 From: gucio321 Date: Tue, 3 Jun 2025 15:46:13 +0200 Subject: [PATCH 6/7] parser_test: find another broken descendant rules bug --- parser_test.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/parser_test.go b/parser_test.go index 8dc79d1..62ed306 100644 --- a/parser_test.go +++ b/parser_test.go @@ -47,6 +47,10 @@ rule1 { style: value; }` + ex8 := `.rule1 #rule2 { + style: value; +}` + cases := []struct { name string CSS string @@ -94,6 +98,11 @@ rule1 { "style": "value", }, }}, + {"Selector with descentant ID and Class", ex8, map[Rule]map[string]string{ + ".rule1 #rule2": { + "style": "value", + }, + }}, } for _, tt := range cases { @@ -165,6 +174,7 @@ func TestParseSelectorGroup(t *testing.T) { if err != nil { t.Fatal(err) } + fmt.Println(css) if _, ok := css[".rule1"]; !ok { t.Fatal("Missing '.rule1' rule") @@ -172,9 +182,11 @@ func TestParseSelectorGroup(t *testing.T) { if _, ok := css["#rule2"]; !ok { t.Fatal("Missing '#rule2' rule") } - if _, ok := css["rule3"]; !ok { - t.Fatal("Missing '.rule3' rule") - } + /* + if _, ok := css["rule3"]; !ok { + t.Fatal("Missing '.rule3' rule") + } + */ } func BenchmarkParser(b *testing.B) { From 9a9fc4295e9e7ecf66baf4401181ade783ea2961 Mon Sep 17 00:00:00 2001 From: gucio321 Date: Tue, 3 Jun 2025 15:59:44 +0200 Subject: [PATCH 7/7] parser: fix descendant rules with ID or Class tags --- parser.go | 13 +++++++++---- parser_test.go | 25 ------------------------- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/parser.go b/parser.go index 9c85db0..c2422c8 100644 --- a/parser.go +++ b/parser.go @@ -130,7 +130,7 @@ func buildList(r io.Reader) *list.List { func parse(l *list.List) (map[Rule]map[string]string, error) { var ( // Information about the current block that is parsed. - rule []string + rule = make([]string, 1) style string value string selector string @@ -174,9 +174,14 @@ func parse(l *list.List) (map[Rule]map[string]string, error) { case tokenValue: switch prevToken { case tokenFirstToken, tokenBlockEnd: - rule = append(rule, token.value) + rule[len(rule)-1] += token.value case tokenSelector: - rule = append(rule, selector+token.value) + // if not empty - we already added a part of a rule and this is a descendant selector for that rule + if rule[len(rule)-1] != "" { + rule[len(rule)-1] += " " + } + + rule[len(rule)-1] += selector + token.value case tokenBlockStart, tokenStatementEnd: // { or ; style = token.value case tokenStyleSeparator: @@ -237,7 +242,7 @@ func parse(l *list.List) (map[Rule]map[string]string, error) { styles = map[string]string{} style, value = "", "" isBlock = false - rule = make([]string, 0) + rule = make([]string, 1) } prevToken = token.typ() } diff --git a/parser_test.go b/parser_test.go index 62ed306..9cdd687 100644 --- a/parser_test.go +++ b/parser_test.go @@ -164,31 +164,6 @@ body { } } -func TestParseSelectorGroup(t *testing.T) { - ex1 := `.rule1 #rule2 rule3 { - style1: value1; - style2: value2; -}` - - css, err := Unmarshal([]byte(ex1)) - if err != nil { - t.Fatal(err) - } - fmt.Println(css) - - if _, ok := css[".rule1"]; !ok { - t.Fatal("Missing '.rule1' rule") - } - if _, ok := css["#rule2"]; !ok { - t.Fatal("Missing '#rule2' rule") - } - /* - if _, ok := css["rule3"]; !ok { - t.Fatal("Missing '.rule3' rule") - } - */ -} - func BenchmarkParser(b *testing.B) { ex1 := "" for i := 0; i < 100; i++ {