├── go.mod ├── .gitignore ├── .travis.yml ├── styles_test.go ├── go.sum ├── .github └── workflows │ └── build.yml ├── tokentype_string.go ├── README.md ├── LICENSE ├── parser_test.go ├── styles.go ├── parser.go └── styles-handlers.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/napsy/go-css 2 | 3 | go 1.19 4 | 5 | require github.com/stretchr/testify v1.10.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | # Don't build other branches that are being used for PRs. 4 | # Currently only the master branch is used in this repo. 5 | branches: 6 | only: 7 | - master 8 | 9 | matrix: 10 | include: 11 | - go: 1.8 12 | 13 | script: 14 | - go test -race -coverprofile=coverage.txt -covermode=atomic 15 | 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | -------------------------------------------------------------------------------- /styles_test.go: -------------------------------------------------------------------------------- 1 | package css 2 | 3 | import "testing" 4 | 5 | func TestStyles(t *testing.T) { 6 | _, err := CSSStyle("background-color", map[string]string{"background-color": "bla"}) 7 | if err == nil { 8 | t.Fatal("should report invalid color") 9 | } 10 | _, err = CSSStyle("background-color", map[string]string{"background-color": "#aabbccdd"}) 11 | if err != nil { 12 | t.Fatalf("should be valid color, but got %v", err) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 6 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'master' 8 | tags: 9 | - 'v*' 10 | pull_request: 11 | 12 | jobs: 13 | Building: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | # https://github.com/actions/virtual-environments#available-environments 19 | os: [ubuntu-latest] 20 | steps: 21 | - name: Checkout out source code 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | submodules: 'true' 26 | - name: Set up Go environment 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version-file: 'go.mod' 30 | id: go 31 | 32 | - name: Cache Go modules 33 | uses: actions/cache@v4 34 | with: 35 | path: ~/go/pkg/mod 36 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 37 | restore-keys: | 38 | ${{ runner.os }}-go- 39 | 40 | - name: make build 41 | run: | 42 | go test -v 43 | -------------------------------------------------------------------------------- /tokentype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=tokenType"; DO NOT EDIT. 2 | 3 | package css 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[tokenFirstToken - -1] 12 | _ = x[tokenBlockStart-0] 13 | _ = x[tokenBlockEnd-1] 14 | _ = x[tokenRuleName-2] 15 | _ = x[tokenValue-3] 16 | _ = x[tokenSelector-4] 17 | _ = x[tokenStyleSeparator-5] 18 | _ = x[tokenStatementEnd-6] 19 | _ = x[tokenCommentStart-7] 20 | _ = x[tokenCommentEnd-8] 21 | } 22 | 23 | const _tokenType_name = "tokenFirstTokentokenBlockStarttokenBlockEndtokenRuleNametokenValuetokenSelectortokenStyleSeparatortokenStatementEndtokenCommentStarttokenCommentEnd" 24 | 25 | var _tokenType_index = [...]uint8{0, 15, 30, 43, 56, 66, 79, 98, 115, 132, 147} 26 | 27 | func (i tokenType) String() string { 28 | i -= -1 29 | if i < 0 || i >= tokenType(len(_tokenType_index)-1) { 30 | return "tokenType(" + strconv.FormatInt(int64(i+-1), 10) + ")" 31 | } 32 | return _tokenType_name[_tokenType_index[i]:_tokenType_index[i+1]] 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-css 2 | 3 | ![Build Status](https://github.com/napsy/go-css/actions/workflows/build.yml/badge.svg) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/napsy/go-css)](https://goreportcard.com/report/github.com/napsy/go-css) 5 | [![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/vendor/package/blob/master/LICENSE.md) 6 | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/napsy/go-css) 7 | 8 | 9 | 10 | This parser understands simple CSS and comes with a basic CSS syntax checker. 11 | 12 | 13 | ``` 14 | go get github.com/napsy/go-css 15 | ``` 16 | 17 | Example usage: 18 | 19 | ```go 20 | 21 | import "github.com/napsy/go-css" 22 | 23 | ex1 := `rule { 24 | style1: value1; 25 | style2: value2; 26 | }` 27 | 28 | stylesheet, err := css.Unmarshal([]byte(ex1)) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | fmt.Printf("Defined rules:\n") 34 | 35 | for k, _ := range stylesheet { 36 | fmt.Printf("- rule %q\n", k) 37 | } 38 | ``` 39 | 40 | You can get a CSS verifiable property by calling ``CSSStyle``: 41 | 42 | ```go 43 | style, err := css.CSSStyle("background-color", styleSheet["body"]) 44 | if err != nil { 45 | fmt.Printf("Error checking body background color: %v\n", err) 46 | } else { 47 | fmt.Printf("Body background color is %v", style) 48 | } 49 | ``` 50 | 51 | Most of the CSS properties are currently not implemented, but you can always write your own handler by writing a ``StyleHandler`` function and adding it to the ``StylesTable`` map. 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package css 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseSimple(t *testing.T) { 11 | ex1 := `rule { 12 | style1: value1; 13 | style2: value2; 14 | }` 15 | 16 | ex2 := `rule1 { 17 | style1: value1; 18 | style2: value2; 19 | } 20 | rule2 { 21 | style3: value3; 22 | }` 23 | 24 | ex3 := `body { 25 | font-family: 'Zil', serif; 26 | }` 27 | 28 | ex4 := `rule1 { 29 | style1: value1; 30 | style2: value2; 31 | } 32 | rule1 { 33 | style1: value3; 34 | }` 35 | 36 | ex5 := `body { 37 | background-image: url("gradient_bg.png"); 38 | background-repeat: repeat-x; 39 | }` 40 | 41 | ex6 := `rule1 descendant { 42 | style1:value1; 43 | }` 44 | 45 | ex7 := `rule1 { 46 | /* this is a comment */ 47 | style: value; 48 | }` 49 | 50 | ex8 := `.rule1 #rule2 { 51 | style: value; 52 | }` 53 | 54 | cases := []struct { 55 | name string 56 | CSS string 57 | expected map[Rule]map[string]string 58 | }{ 59 | {"Single rule (simple)", ex1, map[Rule]map[string]string{ 60 | "rule": { 61 | "style1": "value1", 62 | "style2": "value2", 63 | }, 64 | }}, 65 | {"Multiple rules", ex2, map[Rule]map[string]string{ 66 | "rule1": { 67 | "style1": "value1", 68 | "style2": "value2", 69 | }, 70 | "rule2": { 71 | "style3": "value3", 72 | }, 73 | }}, 74 | {"Property with spaces", ex3, map[Rule]map[string]string{ 75 | "body": { 76 | "font-family": "'Zil', serif", 77 | }, 78 | }}, 79 | {"Merged rules", ex4, map[Rule]map[string]string{ 80 | "rule1": { 81 | "style1": "value3", 82 | "style2": "value2", 83 | }, 84 | }}, 85 | {"Real world css", ex5, map[Rule]map[string]string{ 86 | "body": { 87 | "background-image": "url(\"gradient_bg.png\")", 88 | "background-repeat": "repeat-x", 89 | }, 90 | }}, 91 | {"Descendant selector", ex6, map[Rule]map[string]string{ 92 | "rule1 descendant": { 93 | "style1": "value1", 94 | }, 95 | }}, 96 | {"Comment in rule", ex7, map[Rule]map[string]string{ 97 | "rule1": { 98 | "style": "value", 99 | }, 100 | }}, 101 | {"Selector with descentant ID and Class", ex8, map[Rule]map[string]string{ 102 | ".rule1 #rule2": { 103 | "style": "value", 104 | }, 105 | }}, 106 | } 107 | 108 | for _, tt := range cases { 109 | t.Run(tt.name, func(t *testing.T) { 110 | css, err := Unmarshal([]byte(tt.CSS)) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | 115 | assert.Equal(t, tt.expected, css, "Expected CSS to be equal") 116 | }) 117 | } 118 | } 119 | 120 | func TestParseError(t *testing.T) { 121 | ex1 := `{ 122 | style1: value1; 123 | }` 124 | 125 | ex2 := `rule { 126 | style1: value1; 127 | style2:; 128 | }` 129 | 130 | ex3 := `rule { 131 | style1: value1 132 | style2: value2; 133 | }` 134 | 135 | ex4 := `} 136 | rule { 137 | style1: value1; 138 | style2:; 139 | }` 140 | 141 | ex5 := ` 142 | body { 143 | style1:value1; 144 | */ 145 | }` 146 | 147 | cases := []struct { 148 | name string 149 | CSS string 150 | }{ 151 | {"Missing rule", ex1}, 152 | {"Missing style", ex2}, 153 | {"Statement Missing Semicolon", ex3}, 154 | {"BlockEndsWithoutBeginning", ex4}, 155 | {"Unexpected end of comment", ex5}, 156 | } 157 | 158 | for _, tt := range cases { 159 | t.Run(tt.name, func(t *testing.T) { 160 | if _, err := Unmarshal([]byte(tt.CSS)); err == nil { 161 | t.Fatal("Should return error!") 162 | } 163 | }) 164 | } 165 | } 166 | 167 | func BenchmarkParser(b *testing.B) { 168 | ex1 := "" 169 | for i := 0; i < 100; i++ { 170 | ex1 += fmt.Sprintf(`block%d { 171 | style%d: value%d; 172 | }`, i, i, i) 173 | } 174 | styleSheet := []byte(ex1) 175 | b.ResetTimer() 176 | for i := 0; i < b.N; i++ { 177 | _, err := Unmarshal(styleSheet) 178 | if err != nil { 179 | b.Fatal(err) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /styles.go: -------------------------------------------------------------------------------- 1 | package css 2 | 3 | import "fmt" 4 | 5 | type UnitValue float64 6 | 7 | type UnitType int 8 | 9 | const ( 10 | UnitNone UnitType = iota 11 | UnitPixels 12 | UnitEm 13 | UnitRem 14 | UnitPercent 15 | UnitPt 16 | UnitAuto 17 | ) 18 | 19 | type Style struct { 20 | Value interface{} 21 | unit UnitType 22 | } 23 | 24 | func (style Style) Unit() UnitType { 25 | return style.unit 26 | } 27 | 28 | func (style Style) String() string { 29 | return fmt.Sprintf("%v", style.Value) 30 | } 31 | 32 | // StyleHandler is a function that checks the style value for errors 33 | // and returns a Style 34 | type StyleHandler func(value string) (Style, error) 35 | 36 | // Common CSS styles. You can overwrite the handlers with your own. 37 | var StylesTable = map[string]StyleHandler{ 38 | "background": background, 39 | "background-attachment": backgroundAttachment, 40 | "background-color": backgroundColor, 41 | "background-image": backgroundImage, 42 | "background-position": backgroundPosition, 43 | "background-repeat": backgroundRepeat, 44 | "border": border, 45 | "border-bottom": borderBottom, 46 | "border-bottom-color": borderBottomColor, 47 | "border-bottom-style": borderBottomStyle, 48 | "border-bottom-width": borderBottomWidth, 49 | "border-color": borderColor, 50 | "border-left": borderLeft, 51 | "border-left-color": borderLeftColor, 52 | "border-left-style": borderLeftStyle, 53 | "border-left-width": borderLeftWidth, 54 | "border-right": borderRight, 55 | "border-right-color": borderRightColor, 56 | "border-right-style": borderRightStyle, 57 | "border-right-width": borderRightWidth, 58 | "border-style": borderStyle, 59 | "border-top": borderTop, 60 | "border-top-color": borderTopColor, 61 | "border-top-style": borderTopStyle, 62 | "border-top-width": borderTopWidth, 63 | "border-width": borderWidth, 64 | "clear": clear, 65 | "clip": clip, 66 | "color": color, 67 | "cursor": cursor, 68 | "display": display, 69 | "filter": filter, 70 | "font": font, 71 | "font-family": fontFamily, 72 | "font-size": fontSize, 73 | "font-variant": fontVariant, 74 | "font-weight": fontWeight, 75 | "height": height, 76 | "left": left, 77 | "letter-spacing": letterSpacing, 78 | "line-height": lineHeight, 79 | "list-style": listStyle, 80 | "list-style-image": listStyleImage, 81 | "list-style-position": listStylePosition, 82 | "list-style-type": listStyleType, 83 | "margin": margin, 84 | "margin-bottom": marginBottom, 85 | "margin-left": marginLeft, 86 | "margin-right": marginRight, 87 | "margin-top": marginTop, 88 | "overflow": overflow, 89 | "padding": padding, 90 | "padding-bottom": paddingBottom, 91 | "padding-left": paddingLeft, 92 | "padding-right": paddingRight, 93 | "padding-top": paddingTop, 94 | "page-break-after": pageBreakAfter, 95 | "page-break-before": pageBreakBefore, 96 | "position": position, 97 | "float": float, 98 | "text-align": textAlign, 99 | "text-decoration": textDecoration, 100 | "text-decoration: blink": textDecorationBlink, 101 | "text-decoration: line-through": textDecorationLineThrough, 102 | "text-decoration: none": textDecorationNone, 103 | "text-decoration: overline": textDecorationOverline, 104 | "text-decoration: underline": textDecorationUnderline, 105 | "text-indent": textIndent, 106 | "text-transform": textTransform, 107 | "top": top, 108 | "vertical-align": verticalAlign, 109 | "visibility": visibility, 110 | "width": width, 111 | "z-index": zIndex, 112 | } 113 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package css 2 | 3 | import ( 4 | "bytes" 5 | "container/list" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strings" 10 | "text/scanner" 11 | ) 12 | 13 | var InvalidCSSError = errors.New("invalid CSS") 14 | 15 | //go:generate stringer -type=tokenType 16 | 17 | type tokenType int 18 | 19 | const ( 20 | tokenFirstToken tokenType = iota - 1 21 | tokenBlockStart 22 | tokenBlockEnd 23 | tokenRuleName 24 | tokenValue 25 | tokenSelector 26 | tokenStyleSeparator 27 | tokenStatementEnd 28 | tokenCommentStart 29 | tokenCommentEnd 30 | ) 31 | 32 | type tokenEntry struct { 33 | value string 34 | pos scanner.Position 35 | } 36 | 37 | func newTokenType(typ string) tokenType { 38 | types := map[string]tokenType{ 39 | "{": tokenBlockStart, 40 | "}": tokenBlockEnd, 41 | ":": tokenStyleSeparator, 42 | ";": tokenStatementEnd, 43 | ".": tokenSelector, 44 | "#": tokenSelector, 45 | "/*": tokenCommentStart, 46 | "*/": tokenCommentEnd, 47 | } 48 | 49 | result, ok := types[typ] 50 | if ok { 51 | return result 52 | } 53 | 54 | return tokenValue 55 | } 56 | 57 | func (e tokenEntry) typ() tokenType { 58 | return newTokenType(e.value) 59 | } 60 | 61 | type tokenizer struct { 62 | s *scanner.Scanner 63 | } 64 | 65 | func newTokenizer(r io.Reader) *tokenizer { 66 | s := &scanner.Scanner{} 67 | s.Init(r) 68 | 69 | return &tokenizer{ 70 | s: s, 71 | } 72 | } 73 | 74 | func (t *tokenizer) next() (tokenEntry, error) { 75 | token := t.s.Scan() 76 | if token == scanner.EOF { 77 | return tokenEntry{}, errors.New("EOF") 78 | } 79 | value := t.s.TokenText() 80 | pos := t.s.Pos() 81 | if newTokenType(value) == tokenStyleSeparator { 82 | t.s.IsIdentRune = func(ch rune, i int) bool { // property value can contain spaces 83 | if ch == -1 || ch == '\n' || ch == '\r' || ch == '\t' || ch == ':' || ch == ';' { 84 | return false 85 | } 86 | return true 87 | } 88 | } else { 89 | t.s.IsIdentRune = func(ch rune, i int) bool { // other tokens can't contain spaces 90 | if ch == -1 || ch == '.' || ch == '#' || ch == '\n' || ch == '\r' || ch == ' ' || ch == '\t' || ch == ':' || ch == ';' { 91 | return false 92 | } 93 | return true 94 | } 95 | } 96 | return tokenEntry{ 97 | value: value, 98 | pos: pos, 99 | }, nil 100 | } 101 | 102 | // Rule is a string type that represents a CSS rule. 103 | type Rule string 104 | 105 | // Type returns the rule type, which can be a class, id or a tag. 106 | func (rule Rule) Type() string { 107 | if strings.HasPrefix(string(rule), ".") { 108 | return "class" 109 | } 110 | if strings.HasPrefix(string(rule), "#") { 111 | return "id" 112 | } 113 | return "tag" 114 | } 115 | 116 | func buildList(r io.Reader) *list.List { 117 | l := list.New() 118 | t := newTokenizer(r) 119 | for { 120 | token, err := t.next() 121 | if err != nil { 122 | break 123 | } 124 | l.PushBack(token) 125 | } 126 | return l 127 | } 128 | 129 | // TODO: rules can be comma separated 130 | func parse(l *list.List) (map[Rule]map[string]string, error) { 131 | var ( 132 | // Information about the current block that is parsed. 133 | rule = make([]string, 1) 134 | style string 135 | value string 136 | selector string 137 | 138 | isBlock bool 139 | isValue bool 140 | isComment bool 141 | 142 | // Parsed styles. 143 | css = make(map[Rule]map[string]string) 144 | styles = make(map[string]string) 145 | 146 | // Previous token for the state machine. 147 | prevToken = tokenType(tokenFirstToken) 148 | ) 149 | 150 | for e := l.Front(); e != nil; e = l.Front() { 151 | token := e.Value.(tokenEntry) 152 | l.Remove(e) 153 | 154 | // handle comment - we continue after this because we don't want to override prevToken 155 | switch token.typ() { 156 | case tokenCommentStart: 157 | isComment = true 158 | continue 159 | case tokenCommentEnd: 160 | // handle standalone endComment token 161 | if !isComment { 162 | return css, fmt.Errorf("line %d: unexpected end of comment: %w", token.pos.Line, InvalidCSSError) 163 | } 164 | 165 | isComment = false 166 | continue 167 | } 168 | 169 | if isComment { // skip everything regardless what it is if processing in comment mode 170 | continue 171 | } 172 | 173 | switch token.typ() { 174 | case tokenValue: 175 | switch prevToken { 176 | case tokenFirstToken, tokenBlockEnd: 177 | rule[len(rule)-1] += token.value 178 | case tokenSelector: 179 | // if not empty - we already added a part of a rule and this is a descendant selector for that rule 180 | if rule[len(rule)-1] != "" { 181 | rule[len(rule)-1] += " " 182 | } 183 | 184 | rule[len(rule)-1] += selector + token.value 185 | case tokenBlockStart, tokenStatementEnd: // { or ; 186 | style = token.value 187 | case tokenStyleSeparator: 188 | if isValue { // multiple separators without ; 189 | return css, fmt.Errorf("line %d: multiple style names before value: %w", token.pos.Line, InvalidCSSError) 190 | } 191 | 192 | isValue = true 193 | value = token.value 194 | case tokenValue: 195 | if !isBlock { // descendant selector 196 | rule[len(rule)-1] += " " + token.value 197 | } else { // technically, this could mean we put multiple style values. 198 | if !isValue { // want to parse multiple style names? denied. 199 | return css, fmt.Errorf("line %d: expected only one name before value: %w", token.pos.Line, InvalidCSSError) 200 | } 201 | 202 | value += " " + token.value 203 | } 204 | default: 205 | return css, fmt.Errorf("line %d: invalid syntax: %w", token.pos.Line, InvalidCSSError) 206 | } 207 | case tokenSelector: 208 | selector = token.value 209 | case tokenBlockStart: 210 | if prevToken != tokenValue { 211 | return css, fmt.Errorf("line %d: block is missing rule identifier: %w", token.pos.Line, InvalidCSSError) 212 | } 213 | isBlock = true 214 | isValue = false 215 | case tokenStatementEnd: 216 | if prevToken != tokenValue || style == "" || value == "" { 217 | return css, fmt.Errorf("line %d: expected style before semicolon: %w", token.pos.Line, InvalidCSSError) 218 | } 219 | styles[style] = value 220 | isValue = false 221 | case tokenBlockEnd: 222 | if !isBlock { 223 | return css, fmt.Errorf("line %d: rule block ends without a beginning: %w", token.pos.Line, InvalidCSSError) 224 | } 225 | 226 | for i := range rule { 227 | r := Rule(rule[i]) 228 | oldRule, ok := css[r] 229 | if ok { 230 | // merge rules 231 | for style, value := range oldRule { 232 | if _, ok := styles[style]; !ok { 233 | styles[style] = value 234 | } 235 | } 236 | } 237 | 238 | css[r] = styles 239 | 240 | } 241 | 242 | styles = map[string]string{} 243 | style, value = "", "" 244 | isBlock = false 245 | rule = make([]string, 1) 246 | } 247 | prevToken = token.typ() 248 | } 249 | 250 | return css, nil 251 | } 252 | 253 | // Unmarshal will take a byte slice, containing sylesheet rules and return 254 | // a map of a rules map. 255 | func Unmarshal(b []byte) (map[Rule]map[string]string, error) { 256 | return parse(buildList(bytes.NewReader(b))) 257 | } 258 | 259 | // CSSStyle returns an error-checked parsed style, or an error if the 260 | // style is unknown. Most of the styles are not supported yet. 261 | func CSSStyle(name string, styles map[string]string) (Style, error) { 262 | value := styles[name] 263 | styleFn, ok := StylesTable[name] 264 | if !ok { 265 | return Style{}, errors.New("unknown style") 266 | } 267 | return styleFn(value) 268 | } 269 | -------------------------------------------------------------------------------- /styles-handlers.go: -------------------------------------------------------------------------------- 1 | package css 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | func checkColor(color string) error { 10 | var errColor = errors.New("invalid color") 11 | if strings.HasPrefix(color, "#") { 12 | if len(color) == 0 || len(color) > 9 { 13 | return errColor 14 | } 15 | if _, err := strconv.ParseUint(color[1:], 16, 32); err != nil { 16 | return errColor 17 | } 18 | return nil 19 | } 20 | 21 | switch color { 22 | case 23 | "black", 24 | "silver", 25 | "gray", 26 | "white", 27 | "maroon", 28 | "red", 29 | "purple", 30 | "fuchsia", 31 | "green", 32 | "lime", 33 | "olive", 34 | "yellow", 35 | "navy", 36 | "blue", 37 | "teal", 38 | "aqua", 39 | "orange", 40 | "aliceblue", 41 | "antiquewhite", 42 | "aquamarine", 43 | "azure", 44 | "beige", 45 | "bisque", 46 | "blanchedalmond", 47 | "blueviolet", 48 | "brown", 49 | "burlywood", 50 | "cadetblue", 51 | "chartreuse", 52 | "chocolate", 53 | "coral", 54 | "cornflowerblue", 55 | "cornsilk", 56 | "crimson", 57 | "cyan", 58 | "darkblue", 59 | "darkcyan", 60 | "darkgoldenrod", 61 | "darkgray", 62 | "darkgreen ", 63 | "darkgrey", 64 | "darkkhaki", 65 | "darkmagenta", 66 | "darkolivegreen", 67 | "darkorange", 68 | "darkorchid", 69 | "darkred", 70 | "darksalmon", 71 | "darkseagreen", 72 | "darkslateblue", 73 | "darkslategray", 74 | "darkslategrey", 75 | "darkturquoise", 76 | "darkviolet", 77 | "deeppink", 78 | "deepskyblue", 79 | "dimgray", 80 | "dimgrey", 81 | "dodgerblue", 82 | "firebrick", 83 | "floralwhite", 84 | "forestgreen", 85 | "gainsboro", 86 | "ghostwhite", 87 | "gold", 88 | "goldenrod", 89 | "greenyellow", 90 | "grey", 91 | "honeydew", 92 | "hotpink", 93 | "indianred", 94 | "indigo", 95 | "ivory", 96 | "khaki", 97 | "lavender", 98 | "lavnderblush", 99 | "lawgreen", 100 | "lemonchiffon", 101 | "lightblue", 102 | "lightcoral", 103 | "lightcyan", 104 | "lightgoldenrodyellow", 105 | "lightgray", 106 | "lightgreen", 107 | "lightgrey", 108 | "lightpink", 109 | "lightsalmon", 110 | "lightseagreen", 111 | "lightskyblue", 112 | "lightslategray", 113 | "lightslategrey", 114 | "lightsteelblue", 115 | "lightyellow", 116 | "limegreen", 117 | "linen", 118 | "magenta", 119 | "mediumaquamarine", 120 | "mediumblue", 121 | "mediumorchid", 122 | "mediumpurple", 123 | "mediumseagreen", 124 | "mediumslateblue", 125 | "mediumspringgreen", 126 | "mediumturquoise", 127 | "mediumvioletred", 128 | "midnightblue", 129 | "mintcream", 130 | "mistyrose", 131 | "moccasin", 132 | "navajowhite", 133 | "oldlace", 134 | "olivedrab", 135 | "orangered", 136 | "orchid", 137 | "palegoldenrod", 138 | "palegreen", 139 | "paleturquoise", 140 | "palevioletred", 141 | "papayawhip", 142 | "peachpuff", 143 | "peru", 144 | "pink", 145 | "plum", 146 | "powderblue", 147 | "rosybrown", 148 | "royalblue", 149 | "saddlebrown", 150 | "salmon", 151 | "sandybrown", 152 | "seagreen", 153 | "seashell", 154 | "sienna", 155 | "skyblue", 156 | "slateblue", 157 | "slategray", 158 | "slategrey", 159 | "snow", 160 | "springgreen", 161 | "steelblue", 162 | "tan", 163 | "thistle", 164 | "tomato", 165 | "turquoise", 166 | "violet", 167 | "wheat", 168 | "whitesmoke", 169 | "yellowgreen": 170 | return nil 171 | 172 | } 173 | return errColor 174 | } 175 | 176 | func background(value string) (Style, error) { 177 | return Style{}, errors.New("not implemented") 178 | } 179 | func backgroundAttachment(value string) (Style, error) { 180 | return Style{}, errors.New("not implemented") 181 | } 182 | func backgroundColor(value string) (Style, error) { 183 | if err := checkColor(value); err != nil { 184 | return Style{}, err 185 | } 186 | 187 | style := Style{ 188 | Value: value, 189 | } 190 | return style, nil 191 | } 192 | func backgroundImage(value string) (Style, error) { 193 | return Style{}, errors.New("not implemented") 194 | } 195 | func backgroundPosition(value string) (Style, error) { 196 | return Style{}, errors.New("not implemented") 197 | } 198 | func backgroundRepeat(value string) (Style, error) { 199 | return Style{}, errors.New("not implemented") 200 | } 201 | func border(value string) (Style, error) { 202 | return Style{}, errors.New("not implemented") 203 | } 204 | func borderBottom(value string) (Style, error) { 205 | return Style{}, errors.New("not implemented") 206 | } 207 | func borderBottomColor(value string) (Style, error) { 208 | return Style{}, errors.New("not implemented") 209 | } 210 | func borderBottomStyle(value string) (Style, error) { 211 | return Style{}, errors.New("not implemented") 212 | } 213 | func borderBottomWidth(value string) (Style, error) { 214 | return Style{}, errors.New("not implemented") 215 | } 216 | func borderColor(value string) (Style, error) { 217 | return Style{}, errors.New("not implemented") 218 | } 219 | func borderLeft(value string) (Style, error) { 220 | return Style{}, errors.New("not implemented") 221 | } 222 | func borderLeftColor(value string) (Style, error) { 223 | return Style{}, errors.New("not implemented") 224 | } 225 | func borderLeftStyle(value string) (Style, error) { 226 | return Style{}, errors.New("not implemented") 227 | } 228 | func borderLeftWidth(value string) (Style, error) { 229 | return Style{}, errors.New("not implemented") 230 | } 231 | func borderRight(value string) (Style, error) { 232 | return Style{}, errors.New("not implemented") 233 | } 234 | func borderRightColor(value string) (Style, error) { 235 | return Style{}, errors.New("not implemented") 236 | } 237 | func borderRightStyle(value string) (Style, error) { 238 | return Style{}, errors.New("not implemented") 239 | } 240 | func borderRightWidth(value string) (Style, error) { 241 | return Style{}, errors.New("not implemented") 242 | } 243 | func borderStyle(value string) (Style, error) { 244 | return Style{}, errors.New("not implemented") 245 | } 246 | func borderTop(value string) (Style, error) { 247 | return Style{}, errors.New("not implemented") 248 | } 249 | func borderTopColor(value string) (Style, error) { 250 | return Style{}, errors.New("not implemented") 251 | } 252 | func borderTopStyle(value string) (Style, error) { 253 | return Style{}, errors.New("not implemented") 254 | } 255 | func borderTopWidth(value string) (Style, error) { 256 | return Style{}, errors.New("not implemented") 257 | } 258 | func borderWidth(value string) (Style, error) { 259 | return Style{}, errors.New("not implemented") 260 | } 261 | func clear(value string) (Style, error) { 262 | return Style{}, errors.New("not implemented") 263 | } 264 | func clip(value string) (Style, error) { 265 | return Style{}, errors.New("not implemented") 266 | } 267 | func color(value string) (Style, error) { 268 | return Style{}, errors.New("not implemented") 269 | } 270 | func cursor(value string) (Style, error) { 271 | return Style{}, errors.New("not implemented") 272 | } 273 | func display(value string) (Style, error) { 274 | return Style{}, errors.New("not implemented") 275 | } 276 | func filter(value string) (Style, error) { 277 | return Style{}, errors.New("not implemented") 278 | } 279 | func font(value string) (Style, error) { 280 | return Style{}, errors.New("not implemented") 281 | } 282 | func fontFamily(value string) (Style, error) { 283 | return Style{}, errors.New("not implemented") 284 | } 285 | func fontSize(value string) (Style, error) { 286 | return Style{}, errors.New("not implemented") 287 | } 288 | func fontVariant(value string) (Style, error) { 289 | return Style{}, errors.New("not implemented") 290 | } 291 | func fontWeight(value string) (Style, error) { 292 | return Style{}, errors.New("not implemented") 293 | } 294 | func height(value string) (Style, error) { 295 | return Style{}, errors.New("not implemented") 296 | } 297 | func left(value string) (Style, error) { 298 | return Style{}, errors.New("not implemented") 299 | } 300 | func letterSpacing(value string) (Style, error) { 301 | return Style{}, errors.New("not implemented") 302 | } 303 | func lineHeight(value string) (Style, error) { 304 | return Style{}, errors.New("not implemented") 305 | } 306 | func listStyle(value string) (Style, error) { 307 | return Style{}, errors.New("not implemented") 308 | } 309 | func listStyleImage(value string) (Style, error) { 310 | return Style{}, errors.New("not implemented") 311 | } 312 | func listStylePosition(value string) (Style, error) { 313 | return Style{}, errors.New("not implemented") 314 | } 315 | func listStyleType(value string) (Style, error) { 316 | return Style{}, errors.New("not implemented") 317 | } 318 | func margin(value string) (Style, error) { 319 | return Style{}, errors.New("not implemented") 320 | } 321 | func marginBottom(value string) (Style, error) { 322 | return Style{}, errors.New("not implemented") 323 | } 324 | func marginLeft(value string) (Style, error) { 325 | return Style{}, errors.New("not implemented") 326 | } 327 | func marginRight(value string) (Style, error) { 328 | return Style{}, errors.New("not implemented") 329 | } 330 | func marginTop(value string) (Style, error) { 331 | return Style{}, errors.New("not implemented") 332 | } 333 | func overflow(value string) (Style, error) { 334 | return Style{}, errors.New("not implemented") 335 | } 336 | func padding(value string) (Style, error) { 337 | return Style{}, errors.New("not implemented") 338 | } 339 | func paddingBottom(value string) (Style, error) { 340 | return Style{}, errors.New("not implemented") 341 | } 342 | func paddingLeft(value string) (Style, error) { 343 | return Style{}, errors.New("not implemented") 344 | } 345 | func paddingRight(value string) (Style, error) { 346 | return Style{}, errors.New("not implemented") 347 | } 348 | func paddingTop(value string) (Style, error) { 349 | return Style{}, errors.New("not implemented") 350 | } 351 | func pageBreakAfter(value string) (Style, error) { 352 | return Style{}, errors.New("not implemented") 353 | } 354 | func pageBreakBefore(value string) (Style, error) { 355 | return Style{}, errors.New("not implemented") 356 | } 357 | func position(value string) (Style, error) { 358 | return Style{}, errors.New("not implemented") 359 | } 360 | func float(value string) (Style, error) { 361 | return Style{}, errors.New("not implemented") 362 | } 363 | func textAlign(value string) (Style, error) { 364 | return Style{}, errors.New("not implemented") 365 | } 366 | func textDecoration(value string) (Style, error) { 367 | return Style{}, errors.New("not implemented") 368 | } 369 | func textDecorationBlink(value string) (Style, error) { 370 | return Style{}, errors.New("not implemented") 371 | } 372 | func textDecorationLineThrough(value string) (Style, error) { 373 | return Style{}, errors.New("not implemented") 374 | } 375 | func textDecorationNone(value string) (Style, error) { 376 | return Style{}, errors.New("not implemented") 377 | } 378 | func textDecorationOverline(value string) (Style, error) { 379 | return Style{}, errors.New("not implemented") 380 | } 381 | func textDecorationUnderline(value string) (Style, error) { 382 | return Style{}, errors.New("not implemented") 383 | } 384 | func textIndent(value string) (Style, error) { 385 | return Style{}, errors.New("not implemented") 386 | } 387 | func textTransform(value string) (Style, error) { 388 | return Style{}, errors.New("not implemented") 389 | } 390 | func top(value string) (Style, error) { 391 | return Style{}, errors.New("not implemented") 392 | } 393 | func verticalAlign(value string) (Style, error) { 394 | return Style{}, errors.New("not implemented") 395 | } 396 | func visibility(value string) (Style, error) { 397 | return Style{}, errors.New("not implemented") 398 | } 399 | func width(value string) (Style, error) { 400 | return Style{}, errors.New("not implemented") 401 | } 402 | func zIndex(value string) (Style, error) { 403 | return Style{}, errors.New("not implemented") 404 | } 405 | --------------------------------------------------------------------------------