├── .gitignore ├── LICENSE ├── README.md ├── alpha.go ├── benchmark_colours.txt ├── benchmark_web_colours.txt ├── boundary.go ├── converters.go ├── core.go ├── core_test.go ├── foo ├── go.mod ├── hex.go ├── hex_test.go ├── hsl.go ├── hsl_test.go ├── hsla.go ├── hsla_test.go ├── main.go ├── models.go ├── output.go ├── palette.go ├── palette_test.go ├── patterns.go ├── rgb.go ├── rgb_test.go ├── rgba.go ├── rgba_test.go ├── sample_palette.json ├── test_colours_2.txt ├── testhelpers_test.go ├── webcolours.go └── webcolours_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | hexokinase 2 | main 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Adam P. Regasz-Rethy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hexokinase 2 | 3 | Fast text parser to scrape and convert colours in the form of rgb, rgb, hsl, hsla functions, three and six digit hex values, web colours names, and custom patterns into hex values. 4 | 5 | ## Installation 6 | 7 | ``` 8 | go get github.com/RRethy/hexokinase 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | Usage of ./hexokinase: 15 | -dp string 16 | disabled patterns which will not be parsed for. Comma separated list 17 | with possible values of hex, rgb, rgba, hsl, hsla, names. The "names" 18 | argument refers to web colour names. 19 | -extended 20 | print results in the format "filename:lnum:colstart-colend:hex:line" (default true) 21 | -files string 22 | files to parse (or stdout to parse stdout) (default "stdout") 23 | -palettes string 24 | 25 | palette file names. 26 | This has the ability to define additional patterns to match and how to convert 27 | them into hex values. The file must be a valid json file that looks like this: 28 | 29 | { 30 | "regex_pattern": "foo[0-9]bar[0-9]baz[0-9]", 31 | "colour_table": { 32 | "foo1bar1baz1": "#eb00ff", 33 | "foo2bar2baz2": "#ffeb00", 34 | "foo3bar3baz3": "#00ffeb" 35 | } 36 | } 37 | 38 | The "regex_pattern" key is optional. If omitted, every key in the 39 | "colour_table" map will be matched instead of using the regex. 40 | Any key in the "colour_table" map will be matched and have the associated hex 41 | string that is provided outputted. 42 | If the regex matches a string which is not a key in the "colour_table" map, it 43 | will be discarded as a false positive. 44 | No checking is done on the hex strings so technically they can be any string. 45 | 46 | -simplified 47 | same as -extended but don't print the full line. Overrides -extended. 48 | ``` 49 | 50 | ### Examples 51 | 52 | ```sh 53 | # Parse foo.json and bar.json for all colours except hsl and hsl functions 54 | # Output the results in simplified format 55 | hexokinase -dp=hsl,hsla -files=foo.json,bar.json -s 56 | 57 | # Parse stdin all colours 58 | # Output the results in extended format 59 | hexokinase 60 | 61 | # Parse foo.json and bar.json for all colours and for the colour patterns 62 | # specified in the palettes p1.json and p2.json. 63 | # Output the results in extended format. 64 | hexokinase -files=foo.json,bar.json -palettes=p1.json,p2.json 65 | ``` 66 | 67 | ## Integrations 68 | 69 | * [vim-hexokinase](https://github.com/RRethy/vim-hexokinase) - WIP 70 | 71 | # TODO 72 | 73 | * Cleanup public API 74 | * Improve README 75 | 76 | ## License 77 | 78 | `mit` 79 | -------------------------------------------------------------------------------- /alpha.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | var ( 10 | bg = rgb{255, 255, 255} 11 | 12 | // ErrInvalidBgHex indicates that a bg value does not contain a valid six 13 | // digit hex 14 | ErrInvalidBgHex = errors.New("invalid hex value") 15 | 16 | // ErrInvalidBgRGB indicates that a bg value does not contain valid values 17 | // for r, g, b 18 | ErrInvalidBgRGB = errors.New("invalid r,g,b value") 19 | ) 20 | 21 | // A BgError records an unsuccessful attempt to change the bg colour used to 22 | // adjust colours with an alpha. 23 | type BgError struct { 24 | Func string // the failing function (SetBgHex, SetBgRGB, etc.) 25 | Value string // the bg value (either a hex or r:g:b) 26 | Err error // the reason for failure (e.g. ErrInvalidBgHex, ErrInvalidBgRGB, etc.) 27 | } 28 | 29 | func (e *BgError) Error() string { 30 | return "hexokinase." + e.Func + ": " + "setting bg " + e.Value + ": " + e.Err.Error() 31 | } 32 | 33 | // SetBgHex sets the bg hex to be used as the background for parsed colours. 34 | // This affects rgba and hsla functions which are mixed with the bg based on 35 | // their alpha. 36 | func SetBgHex(hex string) error { 37 | const fnSetBgHex = "SetBgHex" 38 | 39 | bgHex := hexColour.FindString(hex) 40 | if len(bgHex) > 0 { 41 | bg = hexToRGB(bgHex) 42 | return nil 43 | } 44 | 45 | return &BgError{fnSetBgHex, hex, ErrInvalidBgHex} 46 | } 47 | 48 | // SetBgRGB sets the bg r, g, b to be used as the background for parsed 49 | // colours. This affects rgba and hsla functions which are mixed with the bg 50 | // based on their alpha. 51 | func SetBgRGB(r, g, b int) error { 52 | const fnSetBgRGB = "SetBgRGB" 53 | 54 | if r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255 { 55 | return ErrInvalidBgRGB 56 | } 57 | 58 | bg = rgb{r, g, b} 59 | return nil 60 | } 61 | 62 | // withAlpha mixes r, g, b with bg based on alpha and returns the new r, g, b 63 | // values 64 | func withAlpha(r, g, b int, alpha float64) (int, int, int) { 65 | if alpha > 1.0 { 66 | alpha = 0 67 | } 68 | if alpha < 0.0 { 69 | alpha = 0 70 | } 71 | newR := int(float64(r)*alpha + float64(bg.r)*(1-alpha)) 72 | newG := int(float64(g)*alpha + float64(bg.g)*(1-alpha)) 73 | newB := int(float64(b)*alpha + float64(bg.b)*(1-alpha)) 74 | return newR, newG, newB 75 | } 76 | 77 | func hexWithAlpha(hex string) string { 78 | hexAlpha, err := strconv.ParseInt(hex[6:8], 16, 32) 79 | alpha := 1.0 80 | if err != nil { 81 | alpha = 1 82 | } else { 83 | alpha = float64(hexAlpha) / 255 84 | } 85 | c := hexToRGB(fmt.Sprintf("#%s", hex[0:6])) 86 | return rgbToHex(withAlpha(c.r, c.g, c.b, alpha)) 87 | } 88 | -------------------------------------------------------------------------------- /boundary.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var checkBoundary = false 4 | 5 | // poor mans vimscript "\<\>" 6 | func isWord(line string, start int, end int) bool { 7 | return (start == 0 || !isKeyword(line[start-1])) && (end == len(line) || !isKeyword(line[end])) 8 | } 9 | 10 | // TODO do a better job with utf-8 11 | func isKeyword(c byte) bool { 12 | return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-' 13 | } 14 | -------------------------------------------------------------------------------- /converters.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // toFullHex returns str if it is a six digit hex (#ffffff | 0xffffff), 9 | // otherwise it will treat str as a three digit hex and convert it to a six 10 | // digit hex. 11 | // str MUST be either [#|0x]\x{3} or [#|0x]\x{6}. 12 | func toFullHex(str string) string { 13 | switch len(str) { 14 | case 10: 15 | return hexWithAlpha(str[2:10]) 16 | case 9: 17 | return hexWithAlpha(str[1:9]) 18 | case 8: 19 | return fmt.Sprintf("#%c%c%c%c%c%c", 20 | str[2], str[3], str[4], str[5], str[6], str[7]) 21 | case 7: 22 | return str 23 | case 5: 24 | return fmt.Sprintf("#%c%c%c%c%c%c", 25 | str[2], str[2], str[3], str[3], str[4], str[4]) 26 | case 4: 27 | return fmt.Sprintf("#%c%c%c%c%c%c", 28 | str[1], str[1], str[2], str[2], str[3], str[3]) 29 | default: 30 | return str 31 | } 32 | } 33 | 34 | // percentageStrToInt converts a str formatted as a percentage ("45.5%") into 35 | // an int in [0, 100] where decimals are rounded down. 36 | func percentageStrToInt(perStr string) (int, error) { 37 | num, err := strconv.ParseFloat(perStr[:len(perStr)-1], 64) 38 | return int(num), err 39 | } 40 | 41 | // strToDec converts a str formatted as either a percentage ("45.5%") or as an 42 | // int ("35") into an int in [0, 255]. 43 | // "45.5%" would return 45 * 255 / 100 which is 114 44 | // "35" would return 35 45 | func strToDec(str string) (int, error) { 46 | if str[len(str)-1] == '%' { 47 | num, err := strconv.ParseFloat(str[:len(str)-1], 64) 48 | if err != nil { 49 | return 0, err 50 | } 51 | return int(num) * 255 / 100, nil 52 | } 53 | num, err := strconv.ParseFloat(str, 64) 54 | return int(num), err 55 | } 56 | 57 | // rgbaToHex returns a six digit hex value which represents r, g, b after it 58 | // has been mixed with alpha. 59 | // rgbaToHex(0, 0, 0, 0.0) returns "#000000" 60 | // r, g, b must be in [0, 255] 61 | // alpha must be in [0.0...1.0] 62 | func rgbaToHex(r, g, b int, alpha float64) string { 63 | return rgbToHex(withAlpha(r, g, b, alpha)) 64 | } 65 | 66 | // rgbToHex returns a six digit hex value which represents r, g, b 67 | // rgbToHex(0, 0, 0) returns "#000000" 68 | // r, g, b must be in [0, 255] 69 | func rgbToHex(r, g, b int) string { 70 | return fmt.Sprintf("#%02s%02s%02s", 71 | strconv.FormatInt(int64(r), 16), 72 | strconv.FormatInt(int64(g), 16), 73 | strconv.FormatInt(int64(b), 16)) 74 | } 75 | 76 | // hexToRGB returns the rgb representation of hex. 77 | // hex MUST be #\x{6} 78 | func hexToRGB(hex string) rgb { 79 | r, err := strconv.ParseInt(hex[1:3], 16, 32) 80 | g, err := strconv.ParseInt(hex[3:5], 16, 32) 81 | b, err := strconv.ParseInt(hex[5:7], 16, 32) 82 | if err != nil { 83 | return bg 84 | } 85 | return rgb{int(r), int(g), int(b)} 86 | } 87 | 88 | // hslaToHex returns a six digit hex value which represents h, s, l after it 89 | // has been mixed with alpha. 90 | // h must be [0, 359] 91 | // s, l, alpha must be in [0.0...1.0] 92 | func hslaToHex(h, s, l, alpha float64) string { 93 | r, g, b := hslToRGB(h, s, l) 94 | return rgbaToHex(r, g, b, alpha) 95 | } 96 | 97 | // hslToHex returns a six digit hex value which represents h, s, l 98 | // h must be [0, 359] 99 | // s, l must be in [0.0...1.0] 100 | func hslToHex(h, s, l float64) string { 101 | r, g, b := hslToRGB(h, s, l) 102 | return rgbToHex(r, g, b) 103 | } 104 | 105 | // hslToRGB returns the rgb representation of h, s, l. 106 | // h must be [0, 359] 107 | // s, l must be in [0.0...1.0] 108 | // https://github.com/lucasb-eyer/go-colorful/blob/30298f24079860c4dee452fdef6519b362a4a026/colors.go#L229 109 | func hslToRGB(h, s, l float64) (int, int, int) { 110 | if s == 0 { 111 | lInt := int(l * 255) 112 | return lInt, lInt, lInt 113 | } 114 | 115 | var r, g, b float64 116 | var t1 float64 117 | var t2 float64 118 | var tr float64 119 | var tg float64 120 | var tb float64 121 | 122 | if l < 0.5 { 123 | t1 = l * (1.0 + s) 124 | } else { 125 | t1 = l + s - l*s 126 | } 127 | 128 | t2 = 2*l - t1 129 | h = h / 360 130 | tr = h + 1.0/3.0 131 | tg = h 132 | tb = h - 1.0/3.0 133 | 134 | if tr < 0 { 135 | tr++ 136 | } 137 | if tr > 1 { 138 | tr-- 139 | } 140 | if tg < 0 { 141 | tg++ 142 | } 143 | if tg > 1 { 144 | tg-- 145 | } 146 | if tb < 0 { 147 | tb++ 148 | } 149 | if tb > 1 { 150 | tb-- 151 | } 152 | 153 | // Red 154 | if 6*tr < 1 { 155 | r = t2 + (t1-t2)*6*tr 156 | } else if 2*tr < 1 { 157 | r = t1 158 | } else if 3*tr < 2 { 159 | r = t2 + (t1-t2)*(2.0/3.0-tr)*6 160 | } else { 161 | r = t2 162 | } 163 | 164 | // Green 165 | if 6*tg < 1 { 166 | g = t2 + (t1-t2)*6*tg 167 | } else if 2*tg < 1 { 168 | g = t1 169 | } else if 3*tg < 2 { 170 | g = t2 + (t1-t2)*(2.0/3.0-tg)*6 171 | } else { 172 | g = t2 173 | } 174 | 175 | // Blue 176 | if 6*tb < 1 { 177 | b = t2 + (t1-t2)*6*tb 178 | } else if 2*tb < 1 { 179 | b = t1 180 | } else if 3*tb < 2 { 181 | b = t2 + (t1-t2)*(2.0/3.0-tb)*6 182 | } else { 183 | b = t2 184 | } 185 | 186 | rInt := int(r * 255) 187 | gInt := int(g * 255) 188 | bInt := int(b * 255) 189 | return rInt, gInt, bInt 190 | } 191 | -------------------------------------------------------------------------------- /core.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "sort" 7 | ) 8 | 9 | // parseFile returns all colours matched when parsing in. 10 | // tag is attached to each colour. 11 | func parseFile(in *os.File, tag string, max int) colours { 12 | scanner := bufio.NewScanner(in) 13 | clrs := make(colours, 0, 4) 14 | parsers := []parser{ 15 | parseHex, 16 | parseRGB, 17 | parseRGBA, 18 | parseHSL, 19 | parseHSLA, 20 | parsePalettes, 21 | parseWebColours, 22 | } 23 | 24 | lnum := 0 25 | for scanner.Scan() { 26 | lnum++ 27 | for _, parser := range parsers { 28 | lineColours := parser(scanner.Text()) 29 | for _, colour := range lineColours { 30 | colour.Lnum = lnum 31 | colour.Tag = tag 32 | } 33 | clrs = append(clrs, lineColours...) 34 | } 35 | if max != -1 && len(clrs) >= max { 36 | break 37 | } 38 | } 39 | 40 | sort.Sort(clrs) 41 | if max != -1 && len(clrs) > max { 42 | clrs = clrs[:max] 43 | } 44 | 45 | return clrs 46 | } 47 | -------------------------------------------------------------------------------- /core_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkParseFile(b *testing.B) { 9 | for i := 0; i < b.N; i++ { 10 | file, err := os.Open("./benchmark_colours.txt") 11 | if err != nil { 12 | b.Errorf("%v\n", err) 13 | continue 14 | } 15 | parseFile(file, "", -1) 16 | file.Close() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /foo: -------------------------------------------------------------------------------- 1 | blue red green 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rrethy/hexokinase 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /hex.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | hexDisabled = false 11 | hexColour = regexp.MustCompile(fmt.Sprintf("(?:#|0x)(?:%s{8}|%[1]s{6}|%[1]s{3})", hexDigit)) 12 | ) 13 | 14 | func setTripleHexDisabled(disabled bool) { 15 | if disabled { 16 | hexColour = regexp.MustCompile(fmt.Sprintf("(?:#|0x)(?:%s{8}|%[1]s{6})", hexDigit)) 17 | } else { 18 | hexColour = regexp.MustCompile(fmt.Sprintf("(?:#|0x)(?:%s{8}|%[1]s{6}|%[1]s{3})", hexDigit)) 19 | } 20 | } 21 | 22 | func parseHex(line string) colours { 23 | var clrs colours 24 | if hexDisabled { 25 | return clrs 26 | } 27 | 28 | matches := hexColour.FindAllStringIndex(line, -1) 29 | for _, match := range matches { 30 | if !checkBoundary || isWord(line, match[0], match[1]) { 31 | colour := &Colour{ 32 | ColStart: match[0] + 1, 33 | ColEnd: match[1], 34 | Hex: strings.ToLower(toFullHex(line[match[0]:match[1]])), 35 | Line: line, 36 | } 37 | clrs = append(clrs, colour) 38 | } 39 | } 40 | return clrs 41 | } 42 | -------------------------------------------------------------------------------- /hex_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseHex(t *testing.T) { 8 | var tests = []testData{ 9 | // test various values 10 | {" #fff ", colours{ 11 | &Colour{ColStart: 2, ColEnd: 5, Hex: "#ffffff"}, 12 | }}, 13 | {" #fff ", colours{ 14 | &Colour{ColStart: 2, ColEnd: 5, Hex: "#ffffff"}, 15 | }}, 16 | {" 0xfff ", colours{ 17 | &Colour{ColStart: 2, ColEnd: 6, Hex: "#ffffff"}, 18 | }}, 19 | {" #FFF ", colours{ 20 | &Colour{ColStart: 2, ColEnd: 5, Hex: "#ffffff"}, 21 | }}, 22 | {" #ffffff ", colours{ 23 | &Colour{ColStart: 2, ColEnd: 8, Hex: "#ffffff"}, 24 | }}, 25 | {" #FFFFFF ", colours{ 26 | &Colour{ColStart: 2, ColEnd: 8, Hex: "#ffffff"}, 27 | }}, 28 | {" #FFFFFF ", colours{ 29 | &Colour{ColStart: 2, ColEnd: 8, Hex: "#ffffff"}, 30 | }}, 31 | {" #FFFFFFFF ", colours{ 32 | &Colour{ColStart: 2, ColEnd: 10, Hex: "#ffffff"}, 33 | }}, 34 | {" #B0FD23E6 ", colours{ 35 | &Colour{ColStart: 2, ColEnd: 10, Hex: "#b7fd38"}, 36 | }}, 37 | {" #A23f23 ", colours{ 38 | &Colour{ColStart: 2, ColEnd: 8, Hex: "#a23f23"}, 39 | }}, 40 | {"#a8f9e9", colours{ 41 | &Colour{ColStart: 1, ColEnd: 7, Hex: "#a8f9e9"}, 42 | }}, 43 | {"0xa8f9e9", colours{ 44 | &Colour{ColStart: 1, ColEnd: 8, Hex: "#a8f9e9"}, 45 | }}, 46 | 47 | // test invalid values 48 | {" # fff ", colours{}}, 49 | {" #gggggg ", colours{}}, 50 | {" #banana ", colours{}}, 51 | {" banana ", colours{}}, 52 | {" #ggg ", colours{}}, 53 | 54 | // test multiple values 55 | {" #ae9 #A23f23 ", colours{ 56 | &Colour{ColStart: 2, ColEnd: 5, Lnum: 0, Hex: "#aaee99"}, 57 | &Colour{ColStart: 7, ColEnd: 13, Lnum: 0, Hex: "#a23f23"}, 58 | }}, 59 | {"#ae9 #A23f23", colours{ 60 | &Colour{ColStart: 1, ColEnd: 4, Lnum: 0, Hex: "#aaee99"}, 61 | &Colour{ColStart: 6, ColEnd: 12, Lnum: 0, Hex: "#a23f23"}, 62 | }}, 63 | {"#ae9 #A23f23 #000 #ae9#A23f23#000", colours{ 64 | &Colour{ColStart: 1, ColEnd: 4, Lnum: 0, Hex: "#aaee99"}, 65 | &Colour{ColStart: 6, ColEnd: 12, Lnum: 0, Hex: "#a23f23"}, 66 | &Colour{ColStart: 14, ColEnd: 17, Lnum: 0, Hex: "#000000"}, 67 | &Colour{ColStart: 19, ColEnd: 22, Lnum: 0, Hex: "#aaee99"}, 68 | &Colour{ColStart: 23, ColEnd: 29, Lnum: 0, Hex: "#a23f23"}, 69 | &Colour{ColStart: 30, ColEnd: 33, Lnum: 0, Hex: "#000000"}, 70 | }}, 71 | } 72 | runTests("TestParseHex", t, tests, parseHex) 73 | } 74 | 75 | func TestParseHexWithBoundary(t *testing.T) { 76 | checkBoundary = true 77 | var tests = []testData{ 78 | // test various values 79 | {" #fff ", colours{ 80 | &Colour{ColStart: 2, ColEnd: 5, Hex: "#ffffff"}, 81 | }}, 82 | {" #fff ", colours{ 83 | &Colour{ColStart: 2, ColEnd: 5, Hex: "#ffffff"}, 84 | }}, 85 | {" #FFF ", colours{ 86 | &Colour{ColStart: 2, ColEnd: 5, Hex: "#ffffff"}, 87 | }}, 88 | {" #ffffff ", colours{ 89 | &Colour{ColStart: 2, ColEnd: 8, Hex: "#ffffff"}, 90 | }}, 91 | {" #FFFFFF ", colours{ 92 | &Colour{ColStart: 2, ColEnd: 8, Hex: "#ffffff"}, 93 | }}, 94 | {" #FFFFFF ", colours{ 95 | &Colour{ColStart: 2, ColEnd: 8, Hex: "#ffffff"}, 96 | }}, 97 | {" #A23f23 ", colours{ 98 | &Colour{ColStart: 2, ColEnd: 8, Hex: "#a23f23"}, 99 | }}, 100 | {"#a8f9e9", colours{ 101 | &Colour{ColStart: 1, ColEnd: 7, Hex: "#a8f9e9"}, 102 | }}, 103 | 104 | // test invalid values 105 | {" # fff ", colours{}}, 106 | {" #gggggg ", colours{}}, 107 | {" #banana ", colours{}}, 108 | {" banana ", colours{}}, 109 | {" #ggg ", colours{}}, 110 | 111 | // test multiple values 112 | {" #ae9 #A23f23 ", colours{ 113 | &Colour{ColStart: 2, ColEnd: 5, Lnum: 0, Hex: "#aaee99"}, 114 | &Colour{ColStart: 7, ColEnd: 13, Lnum: 0, Hex: "#a23f23"}, 115 | }}, 116 | {"#ae9 #A23f23", colours{ 117 | &Colour{ColStart: 1, ColEnd: 4, Lnum: 0, Hex: "#aaee99"}, 118 | &Colour{ColStart: 6, ColEnd: 12, Lnum: 0, Hex: "#a23f23"}, 119 | }}, 120 | {"#ae9 #A23f23 #000 #ae9#A23f23#000", colours{ 121 | &Colour{ColStart: 1, ColEnd: 4, Lnum: 0, Hex: "#aaee99"}, 122 | &Colour{ColStart: 6, ColEnd: 12, Lnum: 0, Hex: "#a23f23"}, 123 | &Colour{ColStart: 14, ColEnd: 17, Lnum: 0, Hex: "#000000"}, 124 | &Colour{ColStart: 19, ColEnd: 22, Lnum: 0, Hex: "#aaee99"}, 125 | }}, 126 | } 127 | runTests("TestParseHex", t, tests, parseHex) 128 | } 129 | -------------------------------------------------------------------------------- /hsl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | var ( 10 | hslFunc = regexp.MustCompile(fmt.Sprintf(`hsl\(\s*(%s)\s*,\s*(%s)\s*,\s*(%[2]s)\s*\)`, validHue, percentage)) 11 | hslDisabled = false 12 | ) 13 | 14 | func parseHSL(line string) colours { 15 | var clrs colours 16 | if hslDisabled { 17 | return clrs 18 | } 19 | 20 | matches := hslFunc.FindAllStringSubmatchIndex(line, -1) 21 | for _, match := range matches { 22 | h, err := strconv.ParseFloat(line[match[2]:match[3]], 64) 23 | s, err := percentageStrToInt(line[match[4]:match[5]]) 24 | l, err := percentageStrToInt(line[match[6]:match[7]]) 25 | if err != nil { 26 | continue 27 | } 28 | colour := &Colour{ 29 | ColStart: match[0] + 1, 30 | ColEnd: match[1], 31 | Hex: hslToHex(float64(int(h)%360), float64(s)/100, float64(l)/100), 32 | Line: line, 33 | } 34 | clrs = append(clrs, colour) 35 | } 36 | return clrs 37 | } 38 | -------------------------------------------------------------------------------- /hsl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseHSL(t *testing.T) { 8 | var tests = []testData{ 9 | {" hsl(195, 75.7%, 50.4%) ", colours{ 10 | &Colour{ColStart: 2, ColEnd: 23, Hex: "#1fafdf"}, 11 | }}, 12 | {" hsl(195, 75%, 50%) ", colours{ 13 | &Colour{ColStart: 2, ColEnd: 19, Hex: "#1fafdf"}, 14 | }}, 15 | {" hsl(195, 100%, 50%) ", colours{ 16 | &Colour{ColStart: 2, ColEnd: 20, Hex: "#00bfff"}, 17 | }}, 18 | {" hsl(195.5, 100%, 50%) ", colours{ 19 | &Colour{ColStart: 2, ColEnd: 22, Hex: "#00bfff"}, 20 | }}, 21 | {" hsl(195.53, 100%, 50%) ", colours{ 22 | &Colour{ColStart: 2, ColEnd: 23, Hex: "#00bfff"}, 23 | }}, 24 | {" hsl(0, 0%, 100%) ", colours{ 25 | &Colour{ColStart: 2, ColEnd: 17, Hex: "#ffffff"}, 26 | }}, 27 | {" hsl(0, 0%, 0%) ", colours{ 28 | &Colour{ColStart: 2, ColEnd: 15, Hex: "#000000"}, 29 | }}, 30 | {" hsl( 0 , 0% , 0% ) ", colours{ 31 | &Colour{ColStart: 2, ColEnd: 19, Hex: "#000000"}, 32 | }}, 33 | {"hsl(0,0%,0%)", colours{ 34 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#000000"}, 35 | }}, 36 | {"hsl(360,50%,50%)", colours{ 37 | &Colour{ColStart: 1, ColEnd: 16, Hex: "#bf3f3f"}, 38 | }}, 39 | {"hsl(500,50%,50%)", colours{ 40 | &Colour{ColStart: 1, ColEnd: 16, Hex: "#3fbf6a"}, 41 | }}, 42 | {"hsl(-500,50%,50%)", colours{}}, 43 | {"hsl(-500,500%,50%)", colours{}}, 44 | {"hsl(-500,50%,500%)", colours{}}, 45 | } 46 | 47 | runTests("TestParseHSL", t, tests, parseHSL) 48 | } 49 | -------------------------------------------------------------------------------- /hsla.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | var ( 10 | hslaFunc = regexp.MustCompile(fmt.Sprintf(`hsla\(\s*(%s)\s*,\s*(%s)\s*,\s*(%[2]s)\s*,\s*(%s)\s*\)`, validHue, percentage, alphaPat)) 11 | hslaDisabled = false 12 | ) 13 | 14 | func parseHSLA(line string) colours { 15 | var clrs colours 16 | if hslaDisabled { 17 | return clrs 18 | } 19 | 20 | matches := hslaFunc.FindAllStringSubmatchIndex(line, -1) 21 | for _, match := range matches { 22 | h, err := strconv.ParseFloat(line[match[2]:match[3]], 64) 23 | s, err := percentageStrToInt(line[match[4]:match[5]]) 24 | l, err := percentageStrToInt(line[match[6]:match[7]]) 25 | alpha, err := strconv.ParseFloat(line[match[8]:match[9]], 64) 26 | if err != nil { 27 | continue 28 | } 29 | colour := &Colour{ 30 | ColStart: match[0] + 1, 31 | ColEnd: match[1], 32 | Hex: hslaToHex(float64(int(h)%360), float64(s)/100, float64(l)/100, alpha), 33 | Line: line, 34 | } 35 | clrs = append(clrs, colour) 36 | } 37 | return clrs 38 | } 39 | -------------------------------------------------------------------------------- /hsla_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseHSLA(t *testing.T) { 8 | var tests = []testData{ 9 | {" hsla(195, 100%, 50%, 0) ", colours{ 10 | &Colour{ColStart: 2, ColEnd: 24, Hex: "#ffffff"}, 11 | }}, 12 | {" hsla(195.5, 100%, 50%, 0) ", colours{ 13 | &Colour{ColStart: 2, ColEnd: 26, Hex: "#ffffff"}, 14 | }}, 15 | {" hsla(195.53, 100%, 50%, 0) ", colours{ 16 | &Colour{ColStart: 2, ColEnd: 27, Hex: "#ffffff"}, 17 | }}, 18 | {" hsla(195, 100%, 50%, 0.5) ", colours{ 19 | &Colour{ColStart: 2, ColEnd: 26, Hex: "#7fdfff"}, 20 | }}, 21 | {" hsla(195, 100%, 50%, 1) ", colours{ 22 | &Colour{ColStart: 2, ColEnd: 24, Hex: "#00bfff"}, 23 | }}, 24 | {" hsla(0, 0%, 100%, 1) ", colours{ 25 | &Colour{ColStart: 2, ColEnd: 21, Hex: "#ffffff"}, 26 | }}, 27 | {" hsla(0, 0%, 0%, 1) ", colours{ 28 | &Colour{ColStart: 2, ColEnd: 19, Hex: "#000000"}, 29 | }}, 30 | {" hsla( 0 , 0% , 0% , 1 ) ", colours{ 31 | &Colour{ColStart: 2, ColEnd: 24, Hex: "#000000"}, 32 | }}, 33 | {"hsla(0,0%,0%, 1)", colours{ 34 | &Colour{ColStart: 1, ColEnd: 16, Hex: "#000000"}, 35 | }}, 36 | {"hsla(360,50%,50%, 1)", colours{ 37 | &Colour{ColStart: 1, ColEnd: 20, Hex: "#bf3f3f"}, 38 | }}, 39 | {"hsla(500,50%,50%, 1)", colours{ 40 | &Colour{ColStart: 1, ColEnd: 20, Hex: "#3fbf6a"}, 41 | }}, 42 | {"hsla(-500,50%,50%, 1)", colours{}}, 43 | {"hsla(-500,500%,50%, 1)", colours{}}, 44 | {"hsla(-500,50%,500%, 1)", colours{}}, 45 | {"hsla(195.531, 100%, 50%, 0)", colours{}}, 46 | } 47 | 48 | runTests("TestParseHSLA", t, tests, parseHSLA) 49 | } 50 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | paletteUsage = ` 12 | palette file names. 13 | This has the ability to define additional patterns to match and how to convert 14 | them into hex values. The file must be a valid json file that looks like this: 15 | 16 | { 17 | "regex_pattern": "foo[0-9]bar[0-9]baz[0-9]", 18 | "colour_table": { 19 | "foo1bar1baz1": "#eb00ff", 20 | "foo2bar2baz2": "#ffeb00", 21 | "foo3bar3baz3": "#00ffeb" 22 | } 23 | } 24 | 25 | The "regex_pattern" key is optional. If omitted, every key in the 26 | "colour_table" map will be matched instead of using the regex. 27 | Any key in the "colour_table" map will be matched and have the associated hex 28 | string that is provided outputted. 29 | If the regex matches a string which is not a key in the "colour_table" map, it 30 | will be discarded as a false positive. 31 | No checking is done on the hex strings so technically they can be any string. 32 | ` 33 | ) 34 | 35 | var ( 36 | paletteFnames = flag.String("palettes", "", paletteUsage) 37 | fmtShort = flag.Bool("simplified", false, "same as -extended but don't print the full line. Overrides -extended.") 38 | fmtExtended = flag.Bool("extended", true, `print results in the format "filename:lnum:colstart-colend:hex:line"`) 39 | disabledPatterns = flag.String("dp", "", "disabled patterns which will not be parsed for. Comma separated list\nwith possible values of full_hex, triple_hex, rgb, rgba, hsl, hsla, colour_names. The \"names\"\nargument refers to web colour names.") 40 | enabledPatterns = flag.String("ep", "", "enabled patterns which will be parsed for. Comma separated list\nwith possible values of full_hex, triple_hex, rgb, rgba, hsl, hsla, colour_names. The \"names\"\nargument refers to web colour names.") 41 | fnames = flag.String("files", "stdin", "files to parse (or stdin to parse stdin)") 42 | reverse = flag.Bool("r", false, "reverse output") 43 | checkForColour = flag.String("check", "", "file to check if it contains colour patterns. This will override -fnames. A non-zero exit status indicates no colours found.") 44 | bgHex = flag.String("bg", "#ffffff", "background colour used for alpha calculations with rgba and hsla functions.") 45 | useBoundaries = flag.Bool("boundary", false, "TODO") 46 | ) 47 | 48 | func main() { 49 | flag.Parse() 50 | 51 | SetBgHex(*bgHex) 52 | checkBoundary = *useBoundaries 53 | 54 | if len(*paletteFnames) > 0 { 55 | errs := LoadPalettes(strings.Split(*paletteFnames, ",")...) 56 | if len(errs) > 0 { 57 | for _, err := range errs { 58 | fmt.Fprintf(os.Stderr, "%v\n", err) 59 | } 60 | } 61 | } 62 | 63 | if len(*disabledPatterns) > 0 { 64 | for _, pattern := range strings.Split(*disabledPatterns, ",") { 65 | if len(pattern) == 0 { 66 | continue 67 | } 68 | 69 | switch pattern { 70 | case "full_hex": 71 | hexDisabled = true 72 | case "rgb": 73 | rgbDisabled = true 74 | case "rgba": 75 | rgbaDisabled = true 76 | case "hsl": 77 | hslDisabled = true 78 | case "hsla": 79 | hslaDisabled = true 80 | case "colour_names": 81 | webColoursDisabled = true 82 | case "triple_hex": 83 | setTripleHexDisabled(true) 84 | default: 85 | fmt.Fprintf(os.Stderr, "Unknown argument to flag -dp: %s", pattern) 86 | } 87 | } 88 | } else if len(*enabledPatterns) > 0 { 89 | hexDisabled = true 90 | rgbDisabled = true 91 | rgbaDisabled = true 92 | hslDisabled = true 93 | hslaDisabled = true 94 | webColoursDisabled = true 95 | setTripleHexDisabled(true) 96 | for _, pattern := range strings.Split(*enabledPatterns, ",") { 97 | if len(pattern) == 0 { 98 | continue 99 | } 100 | 101 | switch pattern { 102 | case "full_hex": 103 | hexDisabled = false 104 | case "rgb": 105 | rgbDisabled = false 106 | case "rgba": 107 | rgbaDisabled = false 108 | case "hsl": 109 | hslDisabled = false 110 | case "hsla": 111 | hslaDisabled = false 112 | case "colour_names": 113 | webColoursDisabled = false 114 | case "triple_hex": 115 | setTripleHexDisabled(false) 116 | default: 117 | fmt.Fprintf(os.Stderr, "Unknown argument to flag -ep: %s", pattern) 118 | } 119 | } 120 | } 121 | 122 | if *fmtShort { 123 | SetOutputFmt(ShortFmt) 124 | } else { 125 | SetOutputFmt(ExtendedFmt) 126 | } 127 | 128 | if len(*checkForColour) > 0 { 129 | file, err := os.Open(*checkForColour) 130 | defer file.Close() 131 | if err != nil { 132 | fmt.Fprintf(os.Stderr, "%v\n", err) 133 | os.Exit(1) 134 | } 135 | clrs := parseFile(file, *checkForColour, 1) 136 | if len(clrs) > 0 { 137 | os.Exit(0) 138 | } else { 139 | os.Exit(1) 140 | } 141 | } else { 142 | for _, fname := range strings.Split(*fnames, ",") { 143 | var file *os.File 144 | var err error 145 | 146 | if fname == "stdin" { 147 | file = os.Stdin 148 | } else { 149 | file, err = os.Open(fname) 150 | defer file.Close() 151 | } 152 | if err != nil { 153 | fmt.Fprintf(os.Stderr, "%v\n", err) 154 | continue 155 | } 156 | 157 | clrs := parseFile(file, fname, -1) 158 | PrintColours(clrs, os.Stdout, *reverse) 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type rgb struct { 8 | r, g, b int 9 | } 10 | 11 | type parser (func(line string) colours) 12 | 13 | type colours []*Colour 14 | 15 | func (clrs colours) Len() int { 16 | return len(clrs) 17 | } 18 | 19 | func (clrs colours) Less(i, j int) bool { 20 | clr1 := clrs[i] 21 | clr2 := clrs[j] 22 | if clr1.Lnum < clr2.Lnum || 23 | clr1.Lnum == clr2.Lnum && clr1.ColStart <= clr2.ColStart || 24 | clr1.Lnum == clr2.Lnum && clr1.ColStart == clr2.ColStart && clr1.ColEnd <= clr2.ColEnd { 25 | return true 26 | } 27 | return false 28 | } 29 | 30 | func (clrs colours) Swap(i, j int) { 31 | clrs[i], clrs[j] = clrs[j], clrs[i] 32 | } 33 | 34 | // Colour represents a colour that was parsed and recognized as a valid colour. 35 | type Colour struct { 36 | ColStart, ColEnd int 37 | Lnum int 38 | Line string 39 | Hex string 40 | Tag string 41 | } 42 | 43 | func (c *Colour) String() string { 44 | return fmt.Sprintf("%s:%d:%d-%d:%s:%s", c.Tag, c.Lnum, c.ColStart, c.ColEnd, c.Hex, c.Line) 45 | } 46 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // FMT is the colour format which will be printed to stdout 9 | type FMT int 10 | 11 | const ( 12 | // ExtendedFmt will print the format "filename:lnum:colstart-colend:hex:line" 13 | ExtendedFmt FMT = iota 14 | // ShortFmt will print the format "filename:lnum:colstart-colend:hex" 15 | ShortFmt 16 | 17 | extendedFmt = "%s:%d:%d-%d:%s:%s\n" 18 | shortFmt = "%s:%d:%d-%d:%s\n" 19 | ) 20 | 21 | var outputFmt = ExtendedFmt 22 | 23 | // SetOutputFmt sets the format to print the colour 24 | // fmt can be hexokinase.ExtendedFmt or hexokinase.ShortFmt 25 | func SetOutputFmt(fmt FMT) { 26 | outputFmt = fmt 27 | } 28 | 29 | // PrintColours prints clrs to out based on the formatting specificed by 30 | // SetOutputFmt. 31 | func PrintColours(clrs colours, out *os.File, reverse bool) { 32 | if reverse { 33 | for i := len(clrs) - 1; i >= 0; i-- { 34 | printColour(clrs[i], out) 35 | } 36 | } else { 37 | for _, colour := range clrs { 38 | printColour(colour, out) 39 | } 40 | } 41 | } 42 | 43 | func printColour(colour *Colour, out *os.File) { 44 | if outputFmt == ExtendedFmt { 45 | fmt.Fprintf(out, extendedFmt, colour.Tag, colour.Lnum, colour.ColStart, colour.ColEnd, colour.Hex, colour.Line) 46 | } else { 47 | fmt.Fprintf(out, shortFmt, colour.Tag, colour.Lnum, colour.ColStart, colour.ColEnd, colour.Hex) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /palette.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | type palette struct { 12 | Regex string `json:"regex_pattern"` 13 | ColourPairs map[string]string `json:"colour_table"` 14 | compiledRegex *regexp.Regexp 15 | } 16 | 17 | func (p *palette) compileRegex() { 18 | if len(p.Regex) != 0 { 19 | p.compiledRegex = regexp.MustCompile(p.Regex) 20 | } 21 | } 22 | 23 | var ( 24 | palettes []*palette 25 | 26 | // ErrIncompletePalette indicates that a palette file does not have the 27 | // required key "colour_table" 28 | ErrIncompletePalette = errors.New(`palette missing required "colour_table" key`) 29 | ) 30 | 31 | // A PaletteError records an unsuccessful attempt to load a palette file 32 | type PaletteError struct { 33 | Func string // the failing function (LoadPalettes, etc.) 34 | Fname string //the file name of the palette 35 | Err error // the reason for failure 36 | } 37 | 38 | func (e *PaletteError) Error() string { 39 | return "hexokinase." + e.Func + ": " + "loading palette " + e.Fname + ": " + e.Err.Error() 40 | } 41 | 42 | // LoadPalettes reads the files in fnames and loads them as a palette if they are valid. 43 | func LoadPalettes(fnames ...string) []error { 44 | const fnLoadPalettes = "LoadPalettes" 45 | 46 | var errs []error 47 | for _, fname := range fnames { 48 | file, err := ioutil.ReadFile(fname) 49 | if err != nil { 50 | errs = append(errs, &PaletteError{fnLoadPalettes, fname, err}) 51 | continue 52 | } 53 | 54 | p := &palette{} 55 | err = json.Unmarshal(file, &p) 56 | if err != nil { 57 | errs = append(errs, &PaletteError{fnLoadPalettes, fname, err}) 58 | continue 59 | } 60 | 61 | if len(p.ColourPairs) == 0 { 62 | errs = append(errs, &PaletteError{fnLoadPalettes, fname, ErrIncompletePalette}) 63 | continue 64 | } 65 | 66 | p.compileRegex() 67 | palettes = append(palettes, p) 68 | } 69 | return errs 70 | } 71 | 72 | func parsePalettes(line string) colours { 73 | var clrs colours 74 | if len(palettes) == 0 { 75 | return clrs 76 | } 77 | 78 | for _, p := range palettes { 79 | clrs = append(clrs, parsePalette(line, p)...) 80 | } 81 | 82 | return clrs 83 | } 84 | 85 | func parsePalette(line string, p *palette) colours { 86 | var clrs colours 87 | 88 | if p.compiledRegex != nil { 89 | matches := p.compiledRegex.FindAllStringIndex(line, -1) 90 | for _, match := range matches { 91 | name := line[match[0]:match[1]] 92 | if hex, ok := p.ColourPairs[name]; ok { 93 | if !checkBoundary || isWord(line, match[0], match[1]) { 94 | colour := &Colour{ 95 | ColStart: match[0] + 1, 96 | ColEnd: match[1], 97 | Hex: hex, 98 | Line: line, 99 | } 100 | clrs = append(clrs, colour) 101 | } 102 | } 103 | } 104 | } else { 105 | for name, hex := range p.ColourPairs { 106 | curLine := line 107 | for len(curLine) > 0 { 108 | offset := len(line) - len(curLine) 109 | index := strings.Index(curLine, name) 110 | if index != -1 { 111 | if !checkBoundary || isWord(line, offset+index, offset+index+len(name)) { 112 | colour := &Colour{ 113 | ColStart: offset + index + 1, 114 | ColEnd: offset + index + len(name), 115 | Hex: hex, 116 | Line: line, 117 | } 118 | clrs = append(clrs, colour) 119 | } 120 | curLine = curLine[index+len(name):] 121 | } else { 122 | break 123 | } 124 | } 125 | } 126 | } 127 | 128 | return clrs 129 | } 130 | -------------------------------------------------------------------------------- /palette_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | func TestParsePalettes(t *testing.T) { 9 | p1 := &palette{ 10 | ColourPairs: map[string]string{"foo": "bar", "BING": "BONG"}, 11 | } 12 | p2 := &palette{ 13 | ColourPairs: map[string]string{"one": "1", "two": "2"}, 14 | compiledRegex: regexp.MustCompile("one|two"), 15 | } 16 | p3 := &palette{ 17 | ColourPairs: map[string]string{}, 18 | compiledRegex: regexp.MustCompile("three"), 19 | } 20 | palettes = append(palettes, p1, p2, p3) 21 | var tests = []testData{ 22 | {" nothing to see here", colours{}}, 23 | {" three ", colours{}}, 24 | {"foo", colours{ 25 | &Colour{ColStart: 1, ColEnd: 3, Hex: "bar"}, 26 | }}, 27 | {"BING", colours{ 28 | &Colour{ColStart: 1, ColEnd: 4, Hex: "BONG"}, 29 | }}, 30 | {"one", colours{ 31 | &Colour{ColStart: 1, ColEnd: 3, Hex: "1"}, 32 | }}, 33 | {"two", colours{ 34 | &Colour{ColStart: 1, ColEnd: 3, Hex: "2"}, 35 | }}, 36 | {" two ", colours{ 37 | &Colour{ColStart: 2, ColEnd: 4, Hex: "2"}, 38 | }}, 39 | {" foo ", colours{ 40 | &Colour{ColStart: 2, ColEnd: 4, Hex: "bar"}, 41 | }}, 42 | {" foo two ", colours{ 43 | &Colour{ColStart: 2, ColEnd: 4, Hex: "bar"}, 44 | &Colour{ColStart: 6, ColEnd: 8, Hex: "2"}, 45 | }}, 46 | } 47 | runTests("TestPalette", t, tests, parsePalettes) 48 | } 49 | -------------------------------------------------------------------------------- /patterns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | validHue = `[0-9]{1,4}(?:\.[0-9]{1,2})?` 5 | num0to255 = `(?:2(?:[0-4][0-9]|5[0-5])|1?[0-9]?[0-9])(?:\.[0-9]{1,2})?` 6 | percentage = `1?[0-9]{1,2}(?:\.[0-9]{1,2})?%` 7 | alphaPat = `(?:0|1)?(?:\.[0-9]{1,2})?` 8 | hexDigit = `[0-9a-fA-F]` 9 | rgbFuncParam = `(?:(?:` + num0to255 + `)|(?:` + percentage + `))` 10 | ) 11 | -------------------------------------------------------------------------------- /rgb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | var ( 9 | rgbFunc = regexp.MustCompile(fmt.Sprintf(`rgb\(\s*(%s)\s*,\s*(%[1]s)\s*,\s*(%[1]s)\s*\)`, rgbFuncParam)) 10 | rgbDisabled = false 11 | ) 12 | 13 | func parseRGB(line string) colours { 14 | var clrs colours 15 | if rgbDisabled { 16 | return clrs 17 | } 18 | 19 | matches := rgbFunc.FindAllStringSubmatchIndex(line, -1) 20 | for _, match := range matches { 21 | r, err := strToDec(line[match[2]:match[3]]) 22 | g, err := strToDec(line[match[4]:match[5]]) 23 | b, err := strToDec(line[match[6]:match[7]]) 24 | if err != nil { 25 | continue 26 | } 27 | colour := &Colour{ 28 | ColStart: match[0] + 1, 29 | ColEnd: match[1], 30 | Hex: rgbToHex(r, g, b), 31 | Line: line, 32 | } 33 | clrs = append(clrs, colour) 34 | } 35 | return clrs 36 | } 37 | -------------------------------------------------------------------------------- /rgb_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseRGB(t *testing.T) { 8 | var tests = []testData{ 9 | {"rgb(0,0,0)", colours{ 10 | &Colour{ColStart: 1, ColEnd: 10, Hex: "#000000"}, 11 | }}, 12 | {"…rgb(0,0,0)", colours{ 13 | &Colour{ColStart: 4, ColEnd: 13, Hex: "#000000"}, 14 | }}, 15 | {"rgb(176,253,35)", colours{ 16 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#b0fd23"}, 17 | }}, 18 | {"rgb(176.99,253.12,35.99)", colours{ 19 | &Colour{ColStart: 1, ColEnd: 24, Hex: "#b0fd23"}, 20 | }}, 21 | 22 | // test percentages 23 | {"rgb(0%,253,35)", colours{ 24 | &Colour{ColStart: 1, ColEnd: 14, Hex: "#00fd23"}, 25 | }}, 26 | {"rgb(100%,253,35)", colours{ 27 | &Colour{ColStart: 1, ColEnd: 16, Hex: "#fffd23"}, 28 | }}, 29 | {"rgb(25%,253,35)", colours{ 30 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#3ffd23"}, 31 | }}, 32 | {"rgb(253,25%,35)", colours{ 33 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#fd3f23"}, 34 | }}, 35 | {"rgb(35,253,25%)", colours{ 36 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#23fd3f"}, 37 | }}, 38 | {"rgb(0%,25%,35%)", colours{ 39 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#003f59"}, 40 | }}, 41 | 42 | // test red value 43 | {"rgb(35,0,0)", colours{ 44 | &Colour{ColStart: 1, ColEnd: 11, Hex: "#230000"}, 45 | }}, 46 | {"rgb(176,0,0)", colours{ 47 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#b00000"}, 48 | }}, 49 | {"rgb(215,0,0)", colours{ 50 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#d70000"}, 51 | }}, 52 | {"rgb(253,0,0)", colours{ 53 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#fd0000"}, 54 | }}, 55 | {"rgb(255,0,0)", colours{ 56 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#ff0000"}, 57 | }}, 58 | 59 | // test green value 60 | {"rgb(0,35,0)", colours{ 61 | &Colour{ColStart: 1, ColEnd: 11, Hex: "#002300"}, 62 | }}, 63 | {"rgb(0,176,0)", colours{ 64 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#00b000"}, 65 | }}, 66 | {"rgb(0,215,0)", colours{ 67 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#00d700"}, 68 | }}, 69 | {"rgb(0,253,0)", colours{ 70 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#00fd00"}, 71 | }}, 72 | {"rgb(0,255,0)", colours{ 73 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#00ff00"}, 74 | }}, 75 | 76 | // test blue value 77 | {"rgb(0,0,35)", colours{ 78 | &Colour{ColStart: 1, ColEnd: 11, Hex: "#000023"}, 79 | }}, 80 | {"rgb(0,0,176)", colours{ 81 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#0000b0"}, 82 | }}, 83 | {"rgb(0,0,215)", colours{ 84 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#0000d7"}, 85 | }}, 86 | {"rgb(0,0,253)", colours{ 87 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#0000fd"}, 88 | }}, 89 | {"rgb(0,0,255)", colours{ 90 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#0000ff"}, 91 | }}, 92 | 93 | // test multiple values 94 | {"rgb(0,0,255)rgb(176,253,35) rgb(176,253,35)", colours{ 95 | &Colour{ColStart: 1, ColEnd: 12, Hex: "#0000ff"}, 96 | &Colour{ColStart: 13, ColEnd: 27, Hex: "#b0fd23"}, 97 | &Colour{ColStart: 30, ColEnd: 44, Hex: "#b0fd23"}, 98 | }}, 99 | 100 | // tests invalid values 101 | {"rgb(256,0,0)", colours{}}, 102 | {"rgb(0,0,256)", colours{}}, 103 | {"rgb(0,0,256)", colours{}}, 104 | {"rgb(1000,1000,1000)", colours{}}, 105 | 106 | // test handling of whitespace 107 | {" rgb(0,0,0) ", colours{ 108 | &Colour{ColStart: 2, ColEnd: 11, Hex: "#000000"}, 109 | }}, 110 | {" rgb(0,0,0) rgb(0,0,0) ", colours{ 111 | &Colour{ColStart: 2, ColEnd: 11, Hex: "#000000"}, 112 | &Colour{ColStart: 13, ColEnd: 22, Hex: "#000000"}, 113 | }}, 114 | {"rgb( 0 , 0 , 0 )", colours{ 115 | &Colour{ColStart: 1, ColEnd: 16, Hex: "#000000"}, 116 | }}, 117 | {"rgb( 0 , 0 , 0 )", colours{ 118 | &Colour{ColStart: 1, ColEnd: 22, Hex: "#000000"}, 119 | }}, 120 | {"rgb( 0 , 0 , 0 )", colours{ 121 | &Colour{ColStart: 1, ColEnd: 16, Hex: "#000000"}, 122 | }}, 123 | } 124 | runTests("TestParseRGB", t, tests, parseRGB) 125 | } 126 | -------------------------------------------------------------------------------- /rgba.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | var ( 10 | rgbaFunc = regexp.MustCompile(fmt.Sprintf(`rgba\(\s*(%s)\s*,\s*(%[1]s)\s*,\s*(%[1]s)\s*,\s*(%s)\s*\)`, rgbFuncParam, alphaPat)) 11 | rgbaDisabled = false 12 | ) 13 | 14 | func parseRGBA(line string) colours { 15 | var clrs colours 16 | if rgbaDisabled { 17 | return clrs 18 | } 19 | 20 | matches := rgbaFunc.FindAllStringSubmatchIndex(line, -1) 21 | for _, match := range matches { 22 | r, err := strToDec(line[match[2]:match[3]]) 23 | g, err := strToDec(line[match[4]:match[5]]) 24 | b, err := strToDec(line[match[6]:match[7]]) 25 | alpha, err := strconv.ParseFloat(line[match[8]:match[9]], 64) 26 | if err != nil { 27 | continue 28 | } 29 | colour := &Colour{ 30 | ColStart: match[0] + 1, 31 | ColEnd: match[1], 32 | Hex: rgbaToHex(r, g, b, alpha), 33 | Line: line, 34 | } 35 | clrs = append(clrs, colour) 36 | } 37 | return clrs 38 | } 39 | -------------------------------------------------------------------------------- /rgba_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseRGBA(t *testing.T) { 8 | var tests = []testData{ 9 | {"rgba(0,0,0,1)", colours{ 10 | &Colour{ColStart: 1, ColEnd: 13, Hex: "#000000"}, 11 | }}, 12 | {"rgba(176,253,35,1)", colours{ 13 | &Colour{ColStart: 1, ColEnd: 18, Hex: "#b0fd23"}, 14 | }}, 15 | {"rgba(176,253,35,1)", colours{ 16 | &Colour{ColStart: 1, ColEnd: 18, Hex: "#b0fd23"}, 17 | }}, 18 | 19 | // test various alphas 20 | {"rgba(176,253,35,1.0)", colours{ 21 | &Colour{ColStart: 1, ColEnd: 20, Hex: "#b0fd23"}, 22 | }}, 23 | {"rgba(176,253,35,0)", colours{ 24 | &Colour{ColStart: 1, ColEnd: 18, Hex: "#ffffff"}, 25 | }}, 26 | {"rgba(176,253,35,0.0)", colours{ 27 | &Colour{ColStart: 1, ColEnd: 20, Hex: "#ffffff"}, 28 | }}, 29 | {"rgba(176,253,35,0.1)", colours{ 30 | &Colour{ColStart: 1, ColEnd: 20, Hex: "#f7fee9"}, 31 | }}, 32 | {"rgba(176,253,35,0.9)", colours{ 33 | &Colour{ColStart: 1, ColEnd: 20, Hex: "#b7fd38"}, 34 | }}, 35 | 36 | // test percentages 37 | {"rgba(0%,253,35,1)", colours{ 38 | &Colour{ColStart: 1, ColEnd: 17, Hex: "#00fd23"}, 39 | }}, 40 | {"rgba(100%,253,35,1)", colours{ 41 | &Colour{ColStart: 1, ColEnd: 19, Hex: "#fffd23"}, 42 | }}, 43 | {"rgba(25%,253,35,1)", colours{ 44 | &Colour{ColStart: 1, ColEnd: 18, Hex: "#3ffd23"}, 45 | }}, 46 | {"rgba(253,25%,35,1)", colours{ 47 | &Colour{ColStart: 1, ColEnd: 18, Hex: "#fd3f23"}, 48 | }}, 49 | {"rgba(35,253,25%,1)", colours{ 50 | &Colour{ColStart: 1, ColEnd: 18, Hex: "#23fd3f"}, 51 | }}, 52 | {"rgba(0%,25%,35%,1)", colours{ 53 | &Colour{ColStart: 1, ColEnd: 18, Hex: "#003f59"}, 54 | }}, 55 | 56 | // test red value 57 | {"rgba(35,0,0,1)", colours{ 58 | &Colour{ColStart: 1, ColEnd: 14, Hex: "#230000"}, 59 | }}, 60 | {"rgba(176,0,0,1)", colours{ 61 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#b00000"}, 62 | }}, 63 | {"rgba(215,0,0,1)", colours{ 64 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#d70000"}, 65 | }}, 66 | {"rgba(253,0,0,1)", colours{ 67 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#fd0000"}, 68 | }}, 69 | {"rgba(255,0,0,1)", colours{ 70 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#ff0000"}, 71 | }}, 72 | 73 | // test green value 74 | {"rgba(0,35,0,1)", colours{ 75 | &Colour{ColStart: 1, ColEnd: 14, Hex: "#002300"}, 76 | }}, 77 | {"rgba(0,176,0,1)", colours{ 78 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#00b000"}, 79 | }}, 80 | {"rgba(0,215,0,1)", colours{ 81 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#00d700"}, 82 | }}, 83 | {"rgba(0,253,0,1)", colours{ 84 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#00fd00"}, 85 | }}, 86 | {"rgba(0,255,0,1)", colours{ 87 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#00ff00"}, 88 | }}, 89 | 90 | // test blue value 91 | {"rgba(0,0,35,1)", colours{ 92 | &Colour{ColStart: 1, ColEnd: 14, Hex: "#000023"}, 93 | }}, 94 | {"rgba(0,0,176,1)", colours{ 95 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#0000b0"}, 96 | }}, 97 | {"rgba(0,0,215,1)", colours{ 98 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#0000d7"}, 99 | }}, 100 | {"rgba(0,0,253,1)", colours{ 101 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#0000fd"}, 102 | }}, 103 | {"rgba(0,0,255,1)", colours{ 104 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#0000ff"}, 105 | }}, 106 | 107 | // test multiple values 108 | {"rgba(0,0,255,1)rgba(176,253,35,1) rgba(176,253,35,1)", colours{ 109 | &Colour{ColStart: 1, ColEnd: 15, Hex: "#0000ff"}, 110 | &Colour{ColStart: 16, ColEnd: 33, Hex: "#b0fd23"}, 111 | &Colour{ColStart: 36, ColEnd: 53, Hex: "#b0fd23"}, 112 | }}, 113 | 114 | // tests invalid values 115 | {"rgba(256,0,0,1)", colours{}}, 116 | {"rgba(0,0,256,1)", colours{}}, 117 | {"rgba(0,0,256,1)", colours{}}, 118 | {"rgba(1000,1000,1000,1)", colours{}}, 119 | 120 | // test handling of whitespace 121 | {" rgba(0,0,0,1) ", colours{ 122 | &Colour{ColStart: 2, ColEnd: 14, Hex: "#000000"}, 123 | }}, 124 | {" rgba(0,0,0,1) rgba(0,0,0,1) ", colours{ 125 | &Colour{ColStart: 2, ColEnd: 14, Hex: "#000000"}, 126 | &Colour{ColStart: 16, ColEnd: 28, Hex: "#000000"}, 127 | }}, 128 | {"rgba( 0 , 0 , 0 , 1 )", colours{ 129 | &Colour{ColStart: 1, ColEnd: 21, Hex: "#000000"}, 130 | }}, 131 | {"rgba( 0 , 0 , 0 , 1 )", colours{ 132 | &Colour{ColStart: 1, ColEnd: 29, Hex: "#000000"}, 133 | }}, 134 | {"rgba( 0 , 0 , 0 , 1 )", colours{ 135 | &Colour{ColStart: 1, ColEnd: 21, Hex: "#000000"}, 136 | }}, 137 | } 138 | runTests("TestParseRGBA", t, tests, parseRGBA) 139 | } 140 | -------------------------------------------------------------------------------- /sample_palette.json: -------------------------------------------------------------------------------- 1 | { 2 | "regex_pattern": "foo[0-9]bar[0-9]baz[0-9]", 3 | "colour_table": { 4 | "foo1bar1baz1": "#eb00ff", 5 | "foo2bar2baz2": "#ffeb00", 6 | "foo3bar3baz3": "#00ffeb" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testhelpers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type testData struct { 8 | line string 9 | want colours 10 | } 11 | 12 | func runTests(tag string, t *testing.T, tests []testData, fun func(string) colours) { 13 | for _, test := range tests { 14 | if got := fun(test.line); !areSameColours(got, test.want) { 15 | t.Errorf(` 16 | Func: %+v 17 | Input: %s 18 | Got: %+v 19 | Wanted: %+v`, tag, test.line, got, test.want) 20 | } 21 | } 22 | } 23 | 24 | func areSameColours(colours1 colours, colours2 colours) bool { 25 | if len(colours1) != len(colours2) { 26 | return false 27 | } 28 | 29 | for i, colour1 := range colours1 { 30 | colour2 := colours2[i] 31 | if colour1.ColStart != colour2.ColStart || 32 | colour1.ColEnd != colour2.ColEnd || 33 | colour1.Hex != colour2.Hex { 34 | return false 35 | } 36 | } 37 | 38 | return true 39 | } 40 | -------------------------------------------------------------------------------- /webcolours.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | var ( 8 | // This need to be a list and not a map so it is ordered. This ordering 9 | // allows us to not match "blue" in "aliceblue" since "aliceblue" is a 10 | // colour that will already be matched. 11 | // https://www.w3schools.com/colors/colors_names.asp 12 | webColours = [][2]string{ 13 | [2]string{"lightgoldenrodyellow", "#fafad2"}, 14 | [2]string{"mediumspringgreen", "#00fa9a"}, 15 | [2]string{"mediumaquamarine", "#66cdaa"}, 16 | [2]string{"mediumslateblue", "#7b68ee"}, 17 | [2]string{"mediumvioletred", "#c71585"}, 18 | [2]string{"mediumturquoise", "#48d1cc"}, 19 | [2]string{"lightslategray", "#778899"}, 20 | [2]string{"lightslategrey", "#778899"}, 21 | [2]string{"mediumseagreen", "#3cb371"}, 22 | [2]string{"blanchedalmond", "#ffebcd"}, 23 | [2]string{"lightsteelblue", "#b0c4de"}, 24 | [2]string{"cornflowerblue", "#6495ed"}, 25 | [2]string{"darkolivegreen", "#556b2f"}, 26 | [2]string{"darkgoldenrod", "#b8860b"}, 27 | [2]string{"palegoldenrod", "#eee8aa"}, 28 | [2]string{"paleturquoise", "#afeeee"}, 29 | [2]string{"lavenderblush", "#fff0f5"}, 30 | [2]string{"rebeccapurple", "#663399"}, 31 | [2]string{"darkslategray", "#2f4f4f"}, 32 | [2]string{"darkslategrey", "#2f4f4f"}, 33 | [2]string{"lightseagreen", "#20b2aa"}, 34 | [2]string{"palevioletred", "#db7093"}, 35 | [2]string{"darkturquoise", "#00ced1"}, 36 | [2]string{"darkslateblue", "#483d8b"}, 37 | [2]string{"antiquewhite", "#faebd7"}, 38 | [2]string{"darkseagreen", "#8fbc8f"}, 39 | [2]string{"lightskyblue", "#87cefa"}, 40 | [2]string{"mediumorchid", "#ba55d3"}, 41 | [2]string{"lemonchiffon", "#fffacd"}, 42 | [2]string{"mediumpurple", "#9370db"}, 43 | [2]string{"midnightblue", "#191970"}, 44 | [2]string{"greenyellow", "#adff2f"}, 45 | [2]string{"darkmagenta", "#8b008b"}, 46 | [2]string{"lightsalmon", "#ffa07a"}, 47 | [2]string{"lightyellow", "#ffffe0"}, 48 | [2]string{"deepskyblue", "#00bfff"}, 49 | [2]string{"navajowhite", "#ffdead"}, 50 | [2]string{"saddlebrown", "#8b4513"}, 51 | [2]string{"springgreen", "#00ff7f"}, 52 | [2]string{"forestgreen", "#228b22"}, 53 | [2]string{"floralwhite", "#fffaf0"}, 54 | [2]string{"yellowgreen", "#9acd32"}, 55 | [2]string{"papayawhip", "#ffefd5"}, 56 | [2]string{"aquamarine", "#7fffd4"}, 57 | [2]string{"dodgerblue", "#1e90ff"}, 58 | [2]string{"chartreuse", "#7fff00"}, 59 | [2]string{"blueviolet", "#8a2be2"}, 60 | [2]string{"darkviolet", "#9400d3"}, 61 | [2]string{"darkorange", "#ff8c00"}, 62 | [2]string{"lightgreen", "#90ee90"}, 63 | [2]string{"ghostwhite", "#f8f8ff"}, 64 | [2]string{"whitesmoke", "#f5f5f5"}, 65 | [2]string{"darkorchid", "#9932cc"}, 66 | [2]string{"mediumblue", "#0000cd"}, 67 | [2]string{"powderblue", "#b0e0e6"}, 68 | [2]string{"lightcoral", "#f08080"}, 69 | [2]string{"darksalmon", "#e9967a"}, 70 | [2]string{"sandybrown", "#f4a460"}, 71 | [2]string{"indianred", "#cd5c5c"}, 72 | [2]string{"royalblue", "#4169e1"}, 73 | [2]string{"steelblue", "#4682b4"}, 74 | [2]string{"aliceblue", "#f0f8ff"}, 75 | [2]string{"slategrey", "#708090"}, 76 | [2]string{"mistyrose", "#ffe4e1"}, 77 | [2]string{"turquoise", "#40e0d0"}, 78 | [2]string{"lawngreen", "#7cfc00"}, 79 | [2]string{"mintcream", "#f5fffa"}, 80 | [2]string{"lightblue", "#add8e6"}, 81 | [2]string{"slategray", "#708090"}, 82 | [2]string{"lightcyan", "#e0ffff"}, 83 | [2]string{"goldenrod", "#daa520"}, 84 | [2]string{"lightgray", "#d3d3d3"}, 85 | [2]string{"lightgrey", "#d3d3d3"}, 86 | [2]string{"gainsboro", "#dcdcdc"}, 87 | [2]string{"olivedrab", "#6b8e23"}, 88 | [2]string{"chocolate", "#d2691e"}, 89 | [2]string{"darkgreen", "#006400"}, 90 | [2]string{"peachpuff", "#ffdab9"}, 91 | [2]string{"rosybrown", "#bc8f8f"}, 92 | [2]string{"burlywood", "#deb887"}, 93 | [2]string{"firebrick", "#b22222"}, 94 | [2]string{"slateblue", "#6a5acd"}, 95 | [2]string{"lightpink", "#ffb6c1"}, 96 | [2]string{"limegreen", "#32cd32"}, 97 | [2]string{"orangered", "#ff4500"}, 98 | [2]string{"cadetblue", "#5f9ea0"}, 99 | [2]string{"darkkhaki", "#bdb76b"}, 100 | [2]string{"palegreen", "#98fb98"}, 101 | [2]string{"honeydew", "#f0fff0"}, 102 | [2]string{"seashell", "#fff5ee"}, 103 | [2]string{"seagreen", "#2e8b57"}, 104 | [2]string{"deeppink", "#ff1493"}, 105 | [2]string{"cornsilk", "#fff8dc"}, 106 | [2]string{"darkblue", "#00008b"}, 107 | [2]string{"darkcyan", "#008b8b"}, 108 | [2]string{"darkgray", "#a9a9a9"}, 109 | [2]string{"darkgrey", "#a9a9a9"}, 110 | [2]string{"moccasin", "#ffe4b5"}, 111 | [2]string{"lavender", "#e6e6fa"}, 112 | [2]string{"darkred", "#8b0000"}, 113 | [2]string{"hotpink", "#ff69b4"}, 114 | [2]string{"skyblue", "#87ceeb"}, 115 | [2]string{"oldlace", "#fdf5e6"}, 116 | [2]string{"thistle", "#d8bfd8"}, 117 | [2]string{"fuchsia", "#ff00ff"}, 118 | [2]string{"magenta", "#ff00ff"}, 119 | [2]string{"dimgrey", "#696969"}, 120 | [2]string{"crimson", "#dc143c"}, 121 | [2]string{"dimgray", "#696969"}, 122 | [2]string{"tomato", "#ff6347"}, 123 | [2]string{"bisque", "#ffe4c4"}, 124 | [2]string{"silver", "#c0c0c0"}, 125 | [2]string{"orchid", "#da70d6"}, 126 | [2]string{"orange", "#ffa500"}, 127 | [2]string{"yellow", "#ffff00"}, 128 | [2]string{"sienna", "#a0522d"}, 129 | [2]string{"maroon", "#800000"}, 130 | [2]string{"salmon", "#fa8072"}, 131 | [2]string{"purple", "#800080"}, 132 | [2]string{"indigo", "#4b0082"}, 133 | [2]string{"violet", "#ee82ee"}, 134 | [2]string{"green", "#008000"}, 135 | [2]string{"beige", "#f5f5dc"}, 136 | [2]string{"azure", "#f0ffff"}, 137 | [2]string{"olive", "#808000"}, 138 | [2]string{"ivory", "#fffff0"}, 139 | [2]string{"coral", "#ff7f50"}, 140 | [2]string{"wheat", "#f5deb3"}, 141 | [2]string{"white", "#ffffff"}, 142 | [2]string{"linen", "#faf0e6"}, 143 | [2]string{"brown", "#a52a2a"}, 144 | [2]string{"khaki", "#f0e68c"}, 145 | [2]string{"black", "#000000"}, 146 | [2]string{"cyan", "#00ffff"}, 147 | [2]string{"blue", "#0000ff"}, 148 | [2]string{"aqua", "#00ffff"}, 149 | [2]string{"navy", "#000080"}, 150 | [2]string{"peru", "#cd853f"}, 151 | [2]string{"teal", "#008080"}, 152 | [2]string{"grey", "#808080"}, 153 | [2]string{"snow", "#fffafa"}, 154 | [2]string{"gray", "#808080"}, 155 | [2]string{"gold", "#ffd700"}, 156 | [2]string{"plum", "#dda0dd"}, 157 | [2]string{"pink", "#ffc0cb"}, 158 | [2]string{"lime", "#00ff00"}, 159 | [2]string{"red", "#ff0000"}, 160 | [2]string{"tan", "#d2b48c"}, 161 | } 162 | webColoursDisabled = false 163 | ) 164 | 165 | func parseWebColours(line string) colours { 166 | var clrs colours 167 | if webColoursDisabled { 168 | return clrs 169 | } 170 | 171 | used := make([]bool, len(line)) 172 | line = strings.ToLower(line) 173 | for _, tuple := range webColours { 174 | curLine := line 175 | for len(curLine) > 0 { 176 | offset := len(line) - len(curLine) 177 | index := strings.Index(curLine, tuple[0]) 178 | if index != -1 { 179 | if !used[offset+index] { 180 | if !checkBoundary || isWord(line, offset+index, offset+index+len(tuple[0])) { 181 | colour := &Colour{ 182 | ColStart: offset + index + 1, 183 | ColEnd: offset + index + len(tuple[0]), 184 | Hex: tuple[1], 185 | Line: line, 186 | } 187 | clrs = append(clrs, colour) 188 | for i := offset + index; i < offset+index+len(tuple[0]); i++ { 189 | used[i] = true 190 | } 191 | } 192 | } 193 | curLine = curLine[index+len(tuple[0]):] 194 | } else { 195 | break 196 | } 197 | } 198 | } 199 | return clrs 200 | } 201 | -------------------------------------------------------------------------------- /webcolours_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestParseWebColours(t *testing.T) { 10 | checkBoundary = false 11 | var tests = []testData{ 12 | // test various values 13 | {"aliceblue", colours{ 14 | &Colour{ColStart: 1, ColEnd: 9, Hex: "#f0f8ff"}, 15 | }}, 16 | {"mediumseagreen", colours{ 17 | &Colour{ColStart: 1, ColEnd: 14, Hex: "#3cb371"}, 18 | }}, 19 | {"darkolivegreen", colours{ 20 | &Colour{ColStart: 1, ColEnd: 14, Hex: "#556b2f"}, 21 | }}, 22 | {"cyan", colours{ 23 | &Colour{ColStart: 1, ColEnd: 4, Hex: "#00ffff"}, 24 | }}, 25 | {"blue", colours{ 26 | &Colour{ColStart: 1, ColEnd: 4, Hex: "#0000ff"}, 27 | }}, 28 | {"grey", colours{ 29 | &Colour{ColStart: 1, ColEnd: 4, Hex: "#808080"}, 30 | }}, 31 | {"gray", colours{ 32 | &Colour{ColStart: 1, ColEnd: 4, Hex: "#808080"}, 33 | }}, 34 | {" gre y ", colours{}}, 35 | {" seven ", colours{}}, 36 | {" …grey… ", colours{ 37 | &Colour{ColStart: 6, ColEnd: 9, Hex: "#808080"}, 38 | }}, 39 | {"grey graygrey", colours{ 40 | &Colour{ColStart: 1, ColEnd: 4, Hex: "#808080"}, 41 | &Colour{ColStart: 10, ColEnd: 13, Hex: "#808080"}, 42 | &Colour{ColStart: 6, ColEnd: 9, Hex: "#808080"}, 43 | }}, 44 | {"grey aliceblue blue", colours{ 45 | &Colour{ColStart: 6, ColEnd: 14, Hex: "#f0f8ff"}, 46 | &Colour{ColStart: 16, ColEnd: 19, Hex: "#0000ff"}, 47 | &Colour{ColStart: 1, ColEnd: 4, Hex: "#808080"}, 48 | }}, 49 | {" grey aliceblue blue", colours{ 50 | &Colour{ColStart: 11, ColEnd: 19, Hex: "#f0f8ff"}, 51 | &Colour{ColStart: 23, ColEnd: 26, Hex: "#0000ff"}, 52 | &Colour{ColStart: 6, ColEnd: 9, Hex: "#808080"}, 53 | }}, 54 | } 55 | runTests("TestParseWebColours", t, tests, parseWebColours) 56 | } 57 | 58 | func BenchmarkParseWebColours(b *testing.B) { 59 | for i := 0; i < b.N; i++ { 60 | file, err := os.Open("./benchmark_web_colours.txt") 61 | if err != nil { 62 | b.Errorf("%v\n", err) 63 | continue 64 | } 65 | scanner := bufio.NewScanner(file) 66 | if scanner.Scan() { 67 | parseWebColours(scanner.Text()) 68 | } 69 | file.Close() 70 | } 71 | } 72 | --------------------------------------------------------------------------------