-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathstyle.go
279 lines (246 loc) · 7.19 KB
/
style.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
package html5tag
import (
"errors"
"fmt"
"math"
"regexp"
"sort"
"strconv"
"strings"
)
const numericMatch = `-?[\d]*(\.[\d]+)?`
var numericReplacer, _ = regexp.Compile(numericMatch)
var numericMatcher, _ = regexp.Compile("^" + numericMatch + "$")
// keys for style attributes that take a number that is not a length
var nonLengthNumerics = map[string]bool{
"volume": true,
"speech-rate": true,
"orphans": true,
"widows": true,
"pitch-range": true,
"font-weight": true,
"z-index": true,
"counter-increment": true,
"counter-reset": true,
}
// Style makes it easy to add and manipulate individual properties in a generated style sheet.
//
// Its main use is for generating a style attribute in an HTML tag.
// It implements the String interface to get the style properties as an HTML embeddable string.
type Style map[string]string
// NewStyle initializes an empty Style object.
func NewStyle() Style {
return make(map[string]string)
}
// Copy copies the given style. It also turns a map[string]string into a Style.
func (s Style) Copy() Style {
s2 := NewStyle()
s2.Merge(s)
return s2
}
// Merge merges the styles from one style to another. Conflicts will overwrite the current style.
func (s Style) Merge(m Style) {
for k, v := range m {
s[k] = v
}
}
// Len returns the number of properties in the style.
func (s Style) Len() int {
if s == nil {
return 0
}
return len(s)
}
// Has returns true if the given property is in the style.
func (s Style) Has(property string) bool {
if s == nil {
return false
}
_, ok := s[property]
return ok
}
// Get returns the property.
func (s Style) Get(property string) string {
return s[property]
}
// Remove removes the property.
func (s Style) Remove(property string) {
delete(s, property)
}
// SetString receives a style encoded "style" attribute into the Style structure (e.g. "width: 4px; border: 1px solid black")
func (s Style) SetString(text string) (changed bool, err error) {
s.RemoveAll()
a := strings.Split(text, ";") // break apart into pairs
changed = false
err = nil
for _, value := range a {
b := strings.Split(value, ":")
if len(b) != 2 {
err = errors.New("Css must be a name/value pair separated by a colon. '" + string(text) + "' was given.")
return
}
newChange, newErr := s.SetChanged(strings.TrimSpace(b[0]), strings.TrimSpace(b[1]))
if newErr != nil {
err = newErr
return
}
changed = changed || newChange
}
return
}
// SetChanged sets the given property to the given value.
//
// If the value is prefixed with a plus, minus, multiply or divide, and then a space,
// it assumes that a number will follow, and the specified operation will be performed in place on the current value
// For example, Set ("height", "* 2") will double the height value without changing the unit specifier
// When referring to a value that can be a length, you can use numeric values. In this case, "0" will be passed unchanged,
// but any other number will automatically get a "px" suffix.
func (s Style) SetChanged(property string, value string) (changed bool, err error) {
if strings.Contains(property, " ") {
err = errors.New("attribute names cannot contain spaces")
return
}
if strings.HasPrefix(value, "+ ") ||
strings.HasPrefix(value, "- ") || // the space here distinguishes between a math operation and a negative value
strings.HasPrefix(value, "* ") ||
strings.HasPrefix(value, "/ ") {
return s.mathOp(property, value[0:1], value[2:])
}
if value == "0" {
changed = s.set(property, value)
return
}
isNumeric := numericMatcher.MatchString(value)
if isNumeric {
if !nonLengthNumerics[property] {
value = value + "px"
}
changed = s.set(property, value)
return
}
changed = s.set(property, value)
return
}
// Set is like SetChanged, but returns the Style for chaining.
func (s Style) Set(property string, value string) Style {
_, err := s.SetChanged(property, value)
if err != nil {
panic(err)
}
return s
}
// opReplacer is used in the regular expression replacement function below
func opReplacer(op string, v float64) func(string) string {
return func(cur string) string {
if cur == "" {
return ""
} // bug workaround
//fmt.Println(cur)
f, err := strconv.ParseFloat(cur, 0)
if err != nil {
panic("The number detector is broken on " + cur) // this is coming directly from the regular expression match
}
var newVal float64
switch op {
case "+":
newVal = f + v
case "-":
newVal = f - v
case "*":
newVal = f * v
case "/":
newVal = f / v
default:
panic("Unexpected operation")
}
// floating point operations sometimes are not accurate. This is an attempt to correct epsilons.
return fmt.Sprint(roundFloat(newVal, 6))
}
}
// mathOp applies the given math operation and value to all the numeric values found in the given property.
// Bug(r) If the operation is working on a zero, and the result is not a zero, we may get a raw number with no unit. Not a big deal, but result will use default unit of browser, which is not always px
func (s Style) mathOp(property string, op string, val string) (changed bool, err error) {
cur := s.Get(property)
if cur == "" {
cur = "0"
}
f, err := strconv.ParseFloat(val, 0)
if err != nil {
return
}
newStr := numericReplacer.ReplaceAllStringFunc(cur, opReplacer(op, f))
changed = s.set(property, newStr)
return
}
// RemoveAll resets the style to contain no styles
func (s Style) RemoveAll() {
for k := range s {
delete(s, k)
}
}
// String returns the string version of the style attribute, suitable for inclusion in an HTML style tag
func (s Style) String() string {
return s.encode()
}
// set is a raw set and return true if changed
func (s Style) set(k string, v string) bool {
oldVal, existed := s[k]
s[k] = v
return !existed || oldVal != v
}
// roundFloat takes out rounding errors when doing length math
func roundFloat(f float64, digits int) float64 {
f = f * math.Pow10(digits)
if math.Abs(f) < 0.5 {
return 0
}
v := int(f + math.Copysign(0.5, f))
f = float64(v) / math.Pow10(digits)
return f
}
// encode will output a text version of the style, suitable for inclusion in an HTML "style" attribute.
// it will sort the keys so that they are presented in a consistent and testable way.
func (s Style) encode() (text string) {
var keys []string
for k := range s {
keys = append(keys, k)
}
sort.Strings(keys)
for i, k := range keys {
if i > 0 {
text += ";"
}
text += k + ":" + s.Get(k)
}
return text
}
// StyleString converts an interface type that is being used to set a style value to a string that can be fed into
// the SetStyle* functions
func StyleString(i interface{}) string {
var sValue string
switch v := i.(type) {
case int:
sValue = fmt.Sprintf("%dpx", v)
case float32:
sValue = fmt.Sprintf("%gpx", v)
case float64:
sValue = fmt.Sprintf("%gpx", v)
case string:
sValue = v
case fmt.Stringer:
sValue = v.String()
default:
sValue = fmt.Sprint(v)
}
return sValue
}
// MergeStyleStrings merges the styles found in the two style strings.
// s2 wins conflicts.
func MergeStyleStrings(s1, s2 string) string {
style1 := NewStyle()
_, _ = style1.SetString(s1)
style2 := NewStyle()
_, _ = style2.SetString(s2)
style1.Merge(style2)
return style1.String()
}