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

yuin / goldmark / 42975840832

11 Nov 2026 06:55AM UTC coverage: 85.196% (+0.06%) from 85.139%
42975840832

push

github

yuin
fix(parser): fix #645

3 of 3 new or added lines in 1 file covered. (100.0%)

249 existing lines in 29 files now uncovered.

7585 of 8903 relevant lines covered (85.2%)

2.72 hits per line

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

71.79
/parser/code_span.go
1
package parser
2

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

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

13
type codeSpanParser struct {
14
}
15

16
var defaultCodeSpanParser = &codeSpanParser{}
17

18
// NewCodeSpanParser return a new InlineParser that parses inline codes
19
// surrounded by '`' .
20
func NewCodeSpanParser() InlineParser {
2 ✔
21
        return defaultCodeSpanParser
3 ✔
22
}
3 ✔
23

24
func (s *codeSpanParser) Trigger() []byte {
2 ✔
25
        return []byte{'`'}
3 ✔
26
}
3 ✔
27

28
func (s *codeSpanParser) Parse(_ ast.Node, block text.Reader, _ Context) ast.Node {
2 ✔
29
        line, startSegment := block.PeekLine()
3 ✔
30
        opener := 0
3 ✔
31
        for opener < len(line) && line[opener] == '`' {
5 ✔
32
                opener++
3 ✔
33
        }
3 ✔
34
        block.Advance(opener)
3 ✔
35
        l, pos := block.Position()
3 ✔
36
        var builder text.ValueBuilder
3 ✔
37
        // firstLine is true while no line has been skipped yet (i.e. we are still
3 ✔
38
        // looking at the line right after the opener). The overwhelmingly common
2 ✔
39
        // case is a closer found on this very line, e.g. `code`; that case is
2 ✔
40
        // handled without ever touching the ValueBuilder, avoiding the slice
2 ✔
41
        // allocation multiLineCodeSpanValue would otherwise require.
2 ✔
42
        firstLine := true
3 ✔
43
        for {
5 ✔
44
                line, segment := block.PeekLine()
3 ✔
45
                if line == nil {
5 ✔
46
                        block.SetPosition(l, pos)
3 ✔
47
                        return ast.NewText(text.NewSingleLineValueFromSegment(
3 ✔
48
                                startSegment.WithStop(startSegment.Start+opener), block.Decoder()))
3 ✔
49
                }
3 ✔
50
                for i := 0; i < len(line); i++ {
5 ✔
51
                        c := line[i]
3 ✔
52
                        if c == '`' {
5 ✔
53
                                oldi := i
3 ✔
54
                                for i < len(line) && line[i] == '`' {
5 ✔
55
                                        i++
3 ✔
56
                                }
3 ✔
57
                                closure := i - oldi
3 ✔
58
                                if closure == opener && (i >= len(line) || line[i] != '`') {
5 ✔
59
                                        block.Advance(i)
3 ✔
60
                                        if firstLine {
5 ✔
61
                                                return ast.NewCodeSpan(singleLineCodeSpanValue{start: segment.Start, stop: segment.Start + i - closure})
3 ✔
62
                                        }
3 ✔
63
                                        index := text.NewIndex(segment.Start, segment.Start+i-closure)
3 ✔
64
                                        if !index.IsEmpty() {
5 ✔
65
                                                builder.AddIndex(index)
3 ✔
66
                                        }
3 ✔
67
                                        goto end
3 ✔
68
                                }
69
                        }
70
                }
71
                builder.AddSegment(segment)
3 ✔
72
                block.AdvanceLine()
3 ✔
73
                firstLine = false
3 ✔
74
        }
75
end:
76
        builder.Decoder(nil)
3 ✔
77
        value := builder.BuildMultiLine()
3 ✔
78
        return ast.NewCodeSpan(multiLineCodeSpanValue{indices: value.Indices()})
3 ✔
79
}
80

81
var _ text.Value = (*singleLineCodeSpanValue)(nil)
82

83
// singleLineCodeSpanValue is a [text.Value] implementation for inline code spans.
84
//
85
// this [text.Value] normalizes the value of inline code spans by trimming leading and trailing spaces and
86
// replacing newlines with spaces, as per the CommonMark specification.
87
//
88
// singleLineCodeSpanValue is optimized to avoid allocations; this struct is small enough to be
89
// assigned to an interface-type variable without allocations.
90
type singleLineCodeSpanValue struct {
91
        start int
92
        stop  int
93
}
94

95
func (v singleLineCodeSpanValue) Index() text.Index {
2 ✔
96
        return text.NewIndex(v.start, v.stop)
3 ✔
97
}
3 ✔
98

UNCOV
99
func (v singleLineCodeSpanValue) Indices() []text.Index {
100
        return []text.Index{v.Index()}
×
101
}
×
102

UNCOV
103
func (v singleLineCodeSpanValue) IsEmpty() bool {
104
        return v.start >= v.stop
×
105
}
×
106

107
func (v singleLineCodeSpanValue) IsOwned() bool {
2 ✔
108
        return false
3 ✔
109
}
3 ✔
110

111
func (v singleLineCodeSpanValue) Value(source []byte) string {
2 ✔
112
        return util.BytesToReadOnlyString(v.normalizedBytes(source))
3 ✔
113
}
3 ✔
114

115
func (v singleLineCodeSpanValue) WriteTo(w io.Writer, source []byte) (int, error) {
2 ✔
116
        i := v.trimmedIndex(source)
3 ✔
117
        b := source[i.Start:i.Stop]
3 ✔
118
        if len(b) == 0 {
3 ✔
119
                return 0, nil
×
120
        }
×
121
        return writeNewlinesWithSpaces(w, b)
3 ✔
122
}
123

UNCOV
124
func (v singleLineCodeSpanValue) Bytes(source []byte) []byte {
125
        return source[v.start:v.stop]
×
126
}
×
127

128
func (v singleLineCodeSpanValue) normalizedBytes(source []byte) []byte {
2 ✔
129
        i := v.trimmedIndex(source)
3 ✔
130
        b := source[i.Start:i.Stop]
3 ✔
131
        if len(b) == 0 {
3 ✔
132
                return nil
×
133
        }
×
134
        return replaceNewlinesWithSpaces(b)
3 ✔
135
}
136

UNCOV
137
func (v singleLineCodeSpanValue) Str(source []byte) string {
138
        return util.BytesToReadOnlyString(v.Bytes(source))
×
139
}
×
140

141
func (v singleLineCodeSpanValue) shouldTrimSpaces(source []byte) bool {
2 ✔
142
        b := source[v.start:v.stop]
3 ✔
143
        if len(b) < 2 {
5 ✔
144
                return false
3 ✔
145
        }
3 ✔
146
        if util.IsBlank(b) {
5 ✔
147
                return false
3 ✔
148
        }
3 ✔
149
        return isSpaceOrNewline(b[0]) && isSpaceOrNewline(b[len(b)-1])
3 ✔
150
}
151

152
func (v singleLineCodeSpanValue) trimmedIndex(source []byte) text.Index {
2 ✔
153
        if v.shouldTrimSpaces(source) {
5 ✔
154
                start := v.start + 1
3 ✔
155
                stop := v.stop
3 ✔
156
                if v.stop > v.start {
5 ✔
157
                        stop--
3 ✔
158
                }
3 ✔
159
                return text.NewIndex(start, stop)
3 ✔
160
        }
161
        return text.NewIndex(v.start, v.stop)
3 ✔
162
}
163

164
var _ text.Value = (*multiLineCodeSpanValue)(nil)
165

166
// multiLineCodeSpanValue is a text.Value implementation for inline code spans.
167
//
168
// this [text.Value] normalizes the value of inline code spans by trimming leading and trailing spaces and
169
// replacing newlines with spaces, as per the CommonMark specification.
170
type multiLineCodeSpanValue struct {
171
        indices []text.Index
172
}
173

174
func (v multiLineCodeSpanValue) Index() text.Index {
2 ✔
175
        if len(v.indices) > 0 {
5 ✔
176
                return v.indices[0]
3 ✔
177
        }
3 ✔
178
        return text.NewIndex(0, 0)
×
179
}
180

181
func (v multiLineCodeSpanValue) Indices() []text.Index {
2 ✔
182
        return v.indices
3 ✔
183
}
3 ✔
184

UNCOV
185
func (v multiLineCodeSpanValue) IsEmpty() bool {
186
        for _, idx := range v.indices {
×
187
                if idx.Start < idx.Stop {
×
188
                        return false
×
189
                }
×
190
        }
191
        return true
×
192
}
193

194
func (v multiLineCodeSpanValue) IsOwned() bool {
2 ✔
195
        return false
3 ✔
196
}
3 ✔
197

UNCOV
198
func (v multiLineCodeSpanValue) Value(source []byte) string {
199
        return util.BytesToReadOnlyString(v.normalizedBytes(source))
×
200
}
×
201

202
func (v multiLineCodeSpanValue) WriteTo(w io.Writer, source []byte) (int, error) {
2 ✔
203
        indices := v.trimmedIndices(source)
3 ✔
204
        if len(indices) == 0 {
3 ✔
205
                return 0, nil
×
206
        }
×
207
        if len(indices) == 1 {
5 ✔
208
                return writeNewlinesWithSpaces(w, source[indices[0].Start:indices[0].Stop])
3 ✔
209
        }
3 ✔
210
        n := int(0)
3 ✔
211
        for _, idx := range indices {
5 ✔
212
                written, err := writeNewlinesWithSpaces(w, source[idx.Start:idx.Stop])
3 ✔
213
                n += written
3 ✔
214
                if err != nil {
3 ✔
215
                        return n, err
×
216
                }
×
217
        }
218
        return n, nil
3 ✔
219
}
220

UNCOV
221
func (v multiLineCodeSpanValue) Bytes(source []byte) []byte {
222
        if len(v.indices) == 0 {
×
223
                return nil
×
224
        }
×
225
        if len(v.indices) == 1 {
×
226
                return source[v.indices[0].Start:v.indices[0].Stop]
×
227
        }
×
228
        var buf []byte
×
229
        for _, idx := range v.indices {
×
230
                buf = append(buf, source[idx.Start:idx.Stop]...)
×
231
        }
×
232
        return buf
×
233
}
234

UNCOV
235
func (v multiLineCodeSpanValue) normalizedBytes(source []byte) []byte {
236
        indices := v.trimmedIndices(source)
×
237
        if len(indices) == 0 {
×
238
                return nil
×
239
        }
×
240
        if len(indices) == 1 {
×
241
                return replaceNewlinesWithSpaces(source[indices[0].Start:indices[0].Stop])
×
242
        }
×
243
        buf := slices.Clone(replaceNewlinesWithSpaces(source[indices[0].Start:indices[0].Stop]))
×
244
        for _, idx := range indices[1:] {
×
245
                chunk := replaceNewlinesWithSpaces(source[idx.Start:idx.Stop])
×
246
                buf = append(buf, chunk...)
×
247
        }
×
248
        return buf
×
249
}
250

UNCOV
251
func (v multiLineCodeSpanValue) Str(source []byte) string {
252
        return util.BytesToReadOnlyString(v.Bytes(source))
×
253
}
×
254

255
func (v multiLineCodeSpanValue) shouldTrimSpaces(source []byte) bool {
2 ✔
256
        indices := v.Indices()
3 ✔
257
        if len(indices) == 0 {
3 ✔
258
                return false
×
259
        }
×
260
        if len(indices) == 1 {
5 ✔
261
                b := source[indices[0].Start:indices[0].Stop]
3 ✔
262
                if len(b) < 2 {
5 ✔
263
                        return false
3 ✔
264
                }
3 ✔
265
                if util.IsBlank(b) {
3 ✔
266
                        return false
×
267
                }
×
268
                return isSpaceOrNewline(b[0]) && isSpaceOrNewline(b[len(b)-1])
3 ✔
269
        }
270
        first := source[indices[0].Start:indices[0].Stop]
3 ✔
271
        last := source[indices[len(indices)-1].Start:indices[len(indices)-1].Stop]
3 ✔
272
        if len(first) < 1 || len(last) < 1 {
3 ✔
273
                return false
×
274
        }
×
275
        hasNonBlank := false
3 ✔
276
        for _, index := range indices {
5 ✔
277
                b := source[index.Start:index.Stop]
3 ✔
278
                if !util.IsBlank(b) {
5 ✔
279
                        hasNonBlank = true
3 ✔
280
                        break
3 ✔
281
                }
282
        }
283
        if !hasNonBlank {
3 ✔
284
                return false
×
285
        }
×
286
        return isSpaceOrNewline(first[0]) && isSpaceOrNewline(last[len(last)-1])
3 ✔
287
}
288

289
func (v multiLineCodeSpanValue) trimmedIndices(source []byte) []text.Index {
2 ✔
290
        if v.shouldTrimSpaces(source) {
5 ✔
291
                indices := slices.Clone(v.Indices())
3 ✔
292
                if len(indices) == 1 {
3 ✔
293
                        indices[0].Start++
×
294
                        if indices[0].Stop > indices[0].Start {
×
295
                                indices[0].Stop--
×
296
                        }
×
297
                } else if len(indices) > 1 {
5 ✔
298
                        indices[0].Start++
3 ✔
299
                        if indices[len(indices)-1].Stop > indices[len(indices)-1].Start {
5 ✔
300
                                indices[len(indices)-1].Stop--
3 ✔
301
                        }
3 ✔
302
                }
303
                return indices
3 ✔
304
        }
305
        return v.Indices()
3 ✔
306
}
307

308
func isSpaceOrNewline(c byte) bool {
2 ✔
309
        return c == ' ' || c == '\n'
3 ✔
310
}
3 ✔
311

312
func replaceNewlinesWithSpaces(b []byte) []byte {
2 ✔
313
        return bytes.ReplaceAll(b, []byte{'\n'}, []byte{' '})
3 ✔
314
}
3 ✔
315

316
func writeNewlinesWithSpaces(w io.Writer, b []byte) (int, error) {
2 ✔
317
        if bytes.IndexByte(b, '\n') == -1 {
5 ✔
318
                return w.Write(b)
3 ✔
319
        }
3 ✔
320
        written := 0
3 ✔
321
        for {
5 ✔
322
                i := bytes.IndexByte(b, '\n')
3 ✔
323
                if i == -1 {
5 ✔
324
                        wr, err := w.Write(b)
3 ✔
325
                        written += wr
3 ✔
326
                        return written, err
3 ✔
327
                }
3 ✔
328
                wr, err := w.Write(b[:i])
3 ✔
329
                written += wr
3 ✔
330
                if err != nil {
3 ✔
331
                        return written, err
×
332
                }
×
333
                wr, err = w.Write([]byte{' '})
3 ✔
334
                written += wr
3 ✔
335
                if err != nil {
3 ✔
336
                        return written, err
×
337
                }
×
338
                b = b[i+1:]
3 ✔
339
        }
340
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc