• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yuin / goldmark / 20690504462

04 Jan 2026 08:59AM UTC coverage: 82.984% (+0.2%) from 82.777%
20690504462

push

github

yuin
fix: #537

44 of 45 new or added lines in 1 file covered. (97.78%)

10 existing lines in 4 files now uncovered.

6218 of 7493 relevant lines covered (82.98%)

476947.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.05
/parser/attribute.go
1
package parser
2

3
import (
4
        "bytes"
5
        "io"
6
        "strconv"
7

8
        "github.com/yuin/goldmark/text"
9
        "github.com/yuin/goldmark/util"
10
)
11

12
var attrNameID = []byte("id")
13
var attrNameClass = []byte("class")
14

15
// An Attribute is an attribute of the markdown elements.
16
type Attribute struct {
17
        Name  []byte
18
        Value interface{}
19
}
20

21
// An Attributes is a collection of attributes.
22
type Attributes []Attribute
23

24
// Find returns a (value, true) if an attribute correspond with given name is found, otherwise (nil, false).
25
func (as Attributes) Find(name []byte) (interface{}, bool) {
3✔
26
        for _, a := range as {
6✔
27
                if bytes.Equal(a.Name, name) {
6✔
28
                        return a.Value, true
3✔
29
                }
3✔
30
        }
31
        return nil, false
×
32
}
33

34
func (as Attributes) findUpdate(name []byte, cb func(v interface{}) interface{}) bool {
36✔
35
        for i, a := range as {
87✔
36
                if bytes.Equal(a.Name, name) {
66✔
37
                        as[i].Value = cb(a.Value)
15✔
38
                        return true
15✔
39
                }
15✔
40
        }
41
        return false
21✔
42
}
43

44
// ParseAttributes parses attributes into a map.
45
// ParseAttributes returns a parsed attributes and true if could parse
46
// attributes, otherwise nil and false.
47
func ParseAttributes(reader text.Reader) (Attributes, bool) {
45✔
48
        savedLine, savedPosition := reader.Position()
45✔
49
        reader.SkipSpaces()
45✔
50
        if reader.Peek() != '{' {
45✔
UNCOV
51
                reader.SetPosition(savedLine, savedPosition)
×
UNCOV
52
                return nil, false
×
UNCOV
53
        }
×
54
        reader.Advance(1)
45✔
55
        attrs := Attributes{}
45✔
56
        for {
177✔
57
                if reader.Peek() == '}' {
174✔
58
                        reader.Advance(1)
42✔
59
                        return attrs, true
42✔
60
                }
42✔
61
                attr, ok := parseAttribute(reader)
90✔
62
                if !ok {
93✔
63
                        reader.SetPosition(savedLine, savedPosition)
3✔
64
                        return nil, false
3✔
65
                }
3✔
66
                if bytes.Equal(attr.Name, attrNameClass) {
123✔
67
                        if !attrs.findUpdate(attrNameClass, func(v interface{}) interface{} {
51✔
68
                                ret := make([]byte, 0, len(v.([]byte))+1+len(attr.Value.([]byte)))
15✔
69
                                ret = append(ret, v.([]byte)...)
15✔
70
                                return append(append(ret, ' '), attr.Value.([]byte)...)
15✔
71
                        }) {
36✔
72
                                attrs = append(attrs, attr)
21✔
73
                        }
21✔
74
                } else {
51✔
75
                        attrs = append(attrs, attr)
51✔
76
                }
51✔
77
                reader.SkipSpaces()
87✔
78
                if reader.Peek() == ',' {
99✔
79
                        reader.Advance(1)
12✔
80
                        reader.SkipSpaces()
12✔
81
                }
12✔
82
        }
83
}
84

85
func parseAttribute(reader text.Reader) (Attribute, bool) {
90✔
86
        reader.SkipSpaces()
90✔
87
        c := reader.Peek()
90✔
88
        if c == '#' || c == '.' {
141✔
89
                reader.Advance(1)
51✔
90
                line, _ := reader.PeekLine()
51✔
91
                i := 0
51✔
92
                // HTML5 allows any kind of characters as id, but XHTML restricts characters for id.
51✔
93
                // CommonMark is basically defined for XHTML(even though it is legacy).
51✔
94
                // So we restrict id characters.
51✔
95
                for ; i < len(line) && !util.IsSpace(line[i]) &&
51✔
96
                        (!util.IsPunct(line[i]) || line[i] == '_' ||
51✔
97
                                line[i] == '-' || line[i] == ':' || line[i] == '.'); i++ {
330✔
98
                }
279✔
99
                name := attrNameClass
51✔
100
                if c == '#' {
81✔
101
                        name = attrNameID
30✔
102
                }
30✔
103
                reader.Advance(i)
51✔
104
                return Attribute{Name: name, Value: line[0:i]}, true
51✔
105
        }
106
        line, _ := reader.PeekLine()
39✔
107
        if len(line) == 0 {
39✔
108
                return Attribute{}, false
×
109
        }
×
110
        c = line[0]
39✔
111
        if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
39✔
112
                c == '_' || c == ':') {
39✔
113
                return Attribute{}, false
×
114
        }
×
115
        i := 0
39✔
116
        for ; i < len(line); i++ {
351✔
117
                c = line[i]
312✔
118
                if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
312✔
119
                        (c >= '0' && c <= '9') ||
312✔
120
                        c == '_' || c == ':' || c == '.' || c == '-') {
351✔
121
                        break
39✔
122
                }
123
        }
124
        name := line[:i]
39✔
125
        reader.Advance(i)
39✔
126
        reader.SkipSpaces()
39✔
127
        c = reader.Peek()
39✔
128
        if c != '=' {
39✔
129
                return Attribute{}, false
×
130
        }
×
131
        reader.Advance(1)
39✔
132
        reader.SkipSpaces()
39✔
133
        value, ok := parseAttributeValue(reader)
39✔
134
        if !ok {
39✔
135
                return Attribute{}, false
×
136
        }
×
137
        if bytes.Equal(name, attrNameClass) {
57✔
138
                if _, ok = value.([]byte); !ok {
21✔
139
                        return Attribute{}, false
3✔
140
                }
3✔
141
        }
142
        return Attribute{Name: name, Value: value}, true
36✔
143
}
144

145
func parseAttributeValue(reader text.Reader) (interface{}, bool) {
51✔
146
        reader.SkipSpaces()
51✔
147
        c := reader.Peek()
51✔
148
        var value interface{}
51✔
149
        var ok bool
51✔
150
        switch c {
51✔
151
        case text.EOF:
×
152
                return Attribute{}, false
×
153
        case '{':
3✔
154
                value, ok = ParseAttributes(reader)
3✔
155
        case '[':
3✔
156
                value, ok = parseAttributeArray(reader)
3✔
157
        case '"':
12✔
158
                value, ok = parseAttributeString(reader)
12✔
159
        default:
33✔
160
                if c == '-' || c == '+' || util.IsNumeric(c) {
36✔
161
                        value, ok = parseAttributeNumber(reader)
3✔
162
                } else {
33✔
163
                        value, ok = parseAttributeOthers(reader)
30✔
164
                }
30✔
165
        }
166
        if !ok {
51✔
167
                return nil, false
×
168
        }
×
169
        return value, true
51✔
170
}
171

172
func parseAttributeArray(reader text.Reader) ([]interface{}, bool) {
3✔
173
        reader.Advance(1) // skip [
3✔
174
        ret := []interface{}{}
3✔
175
        for i := 0; ; i++ {
18✔
176
                c := reader.Peek()
15✔
177
                comma := false
15✔
178
                if i != 0 && c == ',' {
24✔
179
                        reader.Advance(1)
9✔
180
                        comma = true
9✔
181
                }
9✔
182
                if c == ']' {
18✔
183
                        if !comma {
6✔
184
                                reader.Advance(1)
3✔
185
                                return ret, true
3✔
186
                        }
3✔
187
                        return nil, false
×
188
                }
189
                reader.SkipSpaces()
12✔
190
                value, ok := parseAttributeValue(reader)
12✔
191
                if !ok {
12✔
192
                        return nil, false
×
193
                }
×
194
                ret = append(ret, value)
12✔
195
                reader.SkipSpaces()
12✔
196
        }
197
}
198

199
func parseAttributeString(reader text.Reader) ([]byte, bool) {
12✔
200
        reader.Advance(1) // skip "
12✔
201
        line, _ := reader.PeekLine()
12✔
202
        i := 0
12✔
203
        l := len(line)
12✔
204
        var buf bytes.Buffer
12✔
205
        for i < l {
153✔
206
                c := line[i]
141✔
207
                if c == '\\' && i != l-1 {
144✔
208
                        n := line[i+1]
3✔
209
                        switch n {
3✔
210
                        case '"', '/', '\\':
3✔
211
                                buf.WriteByte(n)
3✔
212
                                i += 2
3✔
213
                        case 'b':
×
214
                                buf.WriteString("\b")
×
215
                                i += 2
×
216
                        case 'f':
×
217
                                buf.WriteString("\f")
×
218
                                i += 2
×
219
                        case 'n':
×
220
                                buf.WriteString("\n")
×
221
                                i += 2
×
222
                        case 'r':
×
223
                                buf.WriteString("\r")
×
224
                                i += 2
×
225
                        case 't':
×
226
                                buf.WriteString("\t")
×
227
                                i += 2
×
228
                        default:
×
229
                                buf.WriteByte('\\')
×
230
                                i++
×
231
                        }
232
                        continue
3✔
233
                }
234
                if c == '"' {
150✔
235
                        reader.Advance(i + 1)
12✔
236
                        return buf.Bytes(), true
12✔
237
                }
12✔
238
                buf.WriteByte(c)
126✔
239
                i++
126✔
240
        }
241
        return nil, false
×
242
}
243

244
func scanAttributeDecimal(reader text.Reader, w io.ByteWriter) {
3✔
245
        for {
9✔
246
                c := reader.Peek()
6✔
247
                if util.IsNumeric(c) {
9✔
248
                        _ = w.WriteByte(c)
3✔
249
                } else {
6✔
250
                        return
3✔
251
                }
3✔
252
                reader.Advance(1)
3✔
253
        }
254
}
255

256
func parseAttributeNumber(reader text.Reader) (float64, bool) {
3✔
257
        sign := 1
3✔
258
        c := reader.Peek()
3✔
259
        if c == '-' {
3✔
260
                sign = -1
×
261
                reader.Advance(1)
×
262
        } else if c == '+' {
3✔
263
                reader.Advance(1)
×
264
        }
×
265
        var buf bytes.Buffer
3✔
266
        if !util.IsNumeric(reader.Peek()) {
3✔
267
                return 0, false
×
268
        }
×
269
        scanAttributeDecimal(reader, &buf)
3✔
270
        if buf.Len() == 0 {
3✔
271
                return 0, false
×
272
        }
×
273
        c = reader.Peek()
3✔
274
        if c == '.' {
3✔
275
                buf.WriteByte(c)
×
276
                reader.Advance(1)
×
277
                scanAttributeDecimal(reader, &buf)
×
278
        }
×
279
        c = reader.Peek()
3✔
280
        if c == 'e' || c == 'E' {
3✔
281
                buf.WriteByte(c)
×
282
                reader.Advance(1)
×
283
                c = reader.Peek()
×
284
                if c == '-' || c == '+' {
×
285
                        buf.WriteByte(c)
×
286
                        reader.Advance(1)
×
287
                }
×
288
                scanAttributeDecimal(reader, &buf)
×
289
        }
290
        f, err := strconv.ParseFloat(buf.String(), 64)
3✔
291
        if err != nil {
3✔
292
                return 0, false
×
293
        }
×
294
        return float64(sign) * f, true
3✔
295
}
296

297
var bytesTrue = []byte("true")
298
var bytesFalse = []byte("false")
299
var bytesNull = []byte("null")
300

301
func parseAttributeOthers(reader text.Reader) (interface{}, bool) {
30✔
302
        line, _ := reader.PeekLine()
30✔
303
        c := line[0]
30✔
304
        if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
30✔
305
                c == '_' || c == ':') {
30✔
306
                return nil, false
×
307
        }
×
308
        i := 0
30✔
309
        for ; i < len(line); i++ {
216✔
310
                c := line[i]
186✔
311
                if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
186✔
312
                        (c >= '0' && c <= '9') ||
186✔
313
                        c == '_' || c == ':' || c == '.' || c == '-') {
216✔
314
                        break
30✔
315
                }
316
        }
317
        value := line[:i]
30✔
318
        reader.Advance(i)
30✔
319
        if bytes.Equal(value, bytesTrue) {
30✔
320
                return true, true
×
321
        }
×
322
        if bytes.Equal(value, bytesFalse) {
30✔
323
                return false, true
×
324
        }
×
325
        if bytes.Equal(value, bytesNull) {
30✔
326
                return nil, true
×
327
        }
×
328
        return value, true
30✔
329
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc