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

yuin / goldmark / 23470262150

24 Mar 2026 02:27AM UTC coverage: 82.558% (-0.7%) from 83.229%
23470262150

push

github

yuin
feat: add position information to all nodes

- add position information to all nodes, including inline nodes and link
  reference definition nodes.
- Now link reference definition nodes are represented as a new node
  type.
- Link and image nodes have a new field Reference which is a pointer to the reference
  link if this link is a reference link. This field is nil for non-reference
  links.

182 of 283 new or added lines in 21 files covered. (64.31%)

12 existing lines in 1 file now uncovered.

6286 of 7614 relevant lines covered (82.56%)

470127.28 hits per line

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

53.87
/testutil/testutil.go
1
// Package testutil provides utilities for unit tests.
2
package testutil
3

4
import (
5
        "bufio"
6
        "bytes"
7
        "encoding/hex"
8
        "encoding/json"
9
        "fmt"
10
        "os"
11
        "regexp"
12
        "runtime/debug"
13
        "slices"
14
        "strconv"
15
        "strings"
16

17
        "github.com/yuin/goldmark"
18
        "github.com/yuin/goldmark/parser"
19
        "github.com/yuin/goldmark/util"
20
)
21

22
// TestingT is a subset of the functionality provided by testing.T.
23
type TestingT interface {
24
        Logf(string, ...any)
25
        Skipf(string, ...any)
26
        Errorf(string, ...any)
27
        FailNow()
28
}
29

30
// MarkdownTestCase represents a test case.
31
type MarkdownTestCase struct {
32
        No          int
33
        Description string
34
        Options     MarkdownTestCaseOptions
35
        Markdown    string
36
        Expected    string
37
}
38

39
func source(t *MarkdownTestCase) string {
2,493✔
40
        ret := t.Markdown
2,493✔
41
        if t.Options.Trim {
2,547✔
42
                ret = strings.TrimSpace(ret)
54✔
43
        }
54✔
44
        if t.Options.EnableEscape {
2,634✔
45
                return string(applyEscapeSequence([]byte(ret)))
141✔
46
        }
141✔
47
        return ret
2,352✔
48
}
49

50
func expected(t *MarkdownTestCase) string {
2,493✔
51
        ret := t.Expected
2,493✔
52
        if t.Options.Trim {
2,547✔
53
                ret = strings.TrimSpace(ret)
54✔
54
        }
54✔
55
        if t.Options.EnableEscape {
2,634✔
56
                return string(applyEscapeSequence([]byte(ret)))
141✔
57
        }
141✔
58
        return ret
2,352✔
59
}
60

61
// MarkdownTestCaseOptions represents options for each test case.
62
type MarkdownTestCaseOptions struct {
63
        EnableEscape bool
64
        Trim         bool
65
}
66

67
const attributeSeparator = "//- - - - - - - - -//"
68
const caseSeparator = "//= = = = = = = = = = = = = = = = = = = = = = = =//"
69

70
var optionsRegexp = regexp.MustCompile(`(?i)\s*options:(.*)`)
71

72
// ParseCliCaseArg parses -case command line args.
73
func ParseCliCaseArg() []int {
30✔
74
        ret := []int{}
30✔
75
        for _, a := range os.Args {
210✔
76
                if strings.HasPrefix(a, "case=") {
180✔
77
                        parts := strings.Split(a, "=")
×
78
                        for _, cas := range strings.Split(parts[1], ",") {
×
79
                                value, err := strconv.Atoi(strings.TrimSpace(cas))
×
80
                                if err == nil {
×
81
                                        ret = append(ret, value)
×
82
                                }
×
83
                        }
84
                }
85
        }
86
        return ret
30✔
87
}
88

89
// DoTestCaseFile runs test cases in a given file.
90
func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int) {
27✔
91
        fp, err := os.Open(filename)
27✔
92
        if err != nil {
27✔
93
                panic(err)
×
94
        }
95
        defer func() {
54✔
96
                _ = fp.Close()
27✔
97
        }()
27✔
98

99
        scanner := bufio.NewScanner(fp)
27✔
100
        c := MarkdownTestCase{
27✔
101
                No:          -1,
27✔
102
                Description: "",
27✔
103
                Options:     MarkdownTestCaseOptions{},
27✔
104
                Markdown:    "",
27✔
105
                Expected:    "",
27✔
106
        }
27✔
107
        cases := []MarkdownTestCase{}
27✔
108
        line := 0
27✔
109
        for scanner.Scan() {
1,203✔
110
                line++
1,176✔
111
                if util.IsBlank([]byte(scanner.Text())) {
1,905✔
112
                        continue
729✔
113
                }
114
                header := scanner.Text()
447✔
115
                c.Description = ""
447✔
116
                if strings.Contains(header, ":") {
705✔
117
                        parts := strings.Split(header, ":")
258✔
118
                        c.No, err = strconv.Atoi(strings.TrimSpace(parts[0]))
258✔
119
                        c.Description = strings.Join(parts[1:], ":")
258✔
120
                } else {
447✔
121
                        c.No, err = strconv.Atoi(scanner.Text())
189✔
122
                }
189✔
123
                if err != nil {
447✔
124
                        panic(fmt.Sprintf("%s: invalid case No at line %d", filename, line))
×
125
                }
126
                if !scanner.Scan() {
447✔
127
                        panic(fmt.Sprintf("%s: invalid case at line %d", filename, line))
×
128
                }
129
                line++
447✔
130
                matches := optionsRegexp.FindAllStringSubmatch(scanner.Text(), -1)
447✔
131
                if len(matches) != 0 {
483✔
132
                        err = json.Unmarshal([]byte(matches[0][1]), &c.Options)
36✔
133
                        if err != nil {
36✔
134
                                panic(fmt.Sprintf("%s: invalid options at line %d", filename, line))
×
135
                        }
136
                        scanner.Scan()
36✔
137
                        line++
36✔
138
                }
139
                if scanner.Text() != attributeSeparator {
447✔
140
                        panic(fmt.Sprintf("%s: invalid separator '%s' at line %d", filename, scanner.Text(), line))
×
141
                }
142
                buf := []string{}
447✔
143
                for scanner.Scan() {
2,226✔
144
                        line++
1,779✔
145
                        text := scanner.Text()
1,779✔
146
                        if text == attributeSeparator {
2,226✔
147
                                break
447✔
148
                        }
149
                        buf = append(buf, text)
1,332✔
150
                }
151
                c.Markdown = strings.Join(buf, "\n")
447✔
152
                buf = []string{}
447✔
153
                for scanner.Scan() {
2,799✔
154
                        line++
2,352✔
155
                        text := scanner.Text()
2,352✔
156
                        if text == caseSeparator {
2,799✔
157
                                break
447✔
158
                        }
159
                        buf = append(buf, text)
1,905✔
160
                }
161
                c.Expected = strings.Join(buf, "\n")
447✔
162
                if len(c.Expected) != 0 {
891✔
163
                        c.Expected = c.Expected + "\n"
444✔
164
                }
444✔
165
                shouldAdd := len(no) == 0
447✔
166
                if !shouldAdd {
447✔
NEW
167
                        shouldAdd = slices.Contains(no, c.No)
×
168
                }
×
169
                if shouldAdd {
894✔
170
                        cases = append(cases, c)
447✔
171
                }
447✔
172
        }
173
        DoTestCases(m, cases, t)
27✔
174
}
175

176
// DoTestCases runs a set of test cases.
177
func DoTestCases(m goldmark.Markdown, cases []MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
30✔
178
        for _, testCase := range cases {
2,433✔
179
                DoTestCase(m, testCase, t, opts...)
2,403✔
180
        }
2,403✔
181
}
182

183
// DoTestCase runs a test case.
184
func DoTestCase(m goldmark.Markdown, testCase MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
2,493✔
185
        var ok bool
2,493✔
186
        var out bytes.Buffer
2,493✔
187
        defer func() {
4,986✔
188
                description := ""
2,493✔
189
                if len(testCase.Description) != 0 {
2,832✔
190
                        description = ": " + testCase.Description
339✔
191
                }
339✔
192
                if err := recover(); err != nil {
2,493✔
193
                        format := `============= case %d%s ================
×
194
Markdown:
×
195
-----------
×
196
%s
×
197

×
198
Expected:
×
199
----------
×
200
%s
×
201

×
202
Actual
×
203
---------
×
204
%v
×
205
%s
×
206
`
×
207
                        t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), err, debug.Stack())
×
208
                } else if !ok {
2,493✔
209
                        format := `============= case %d%s ================
×
210
Markdown:
×
211
-----------
×
212
%s
×
213

×
214
Expected:
×
215
----------
×
216
%s
×
217

×
218
Actual
×
219
---------
×
220
%s
×
221

×
222
Diff
×
223
---------
×
224
%s
×
225
`
×
226
                        t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), out.Bytes(),
×
227
                                DiffPretty([]byte(expected(&testCase)), out.Bytes()))
×
228
                }
×
229
        }()
230

231
        if err := m.Convert([]byte(source(&testCase)), &out, opts...); err != nil {
2,493✔
232
                panic(err)
×
233
        }
234
        ok = bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(expected(&testCase))))
2,493✔
235
}
236

237
type diffType int
238

239
const (
240
        diffRemoved diffType = iota
241
        diffAdded
242
        diffNone
243
)
244

245
type diff struct {
246
        Type  diffType
247
        Lines [][]byte
248
}
249

250
func simpleDiff(v1, v2 []byte) []diff {
×
251
        return simpleDiffAux(
×
252
                bytes.Split(v1, []byte("\n")),
×
253
                bytes.Split(v2, []byte("\n")))
×
254
}
×
255

256
func simpleDiffAux(v1lines, v2lines [][]byte) []diff {
×
257
        v1index := map[string][]int{}
×
258
        for i, line := range v1lines {
×
259
                key := util.BytesToReadOnlyString(line)
×
260
                if _, ok := v1index[key]; !ok {
×
261
                        v1index[key] = []int{}
×
262
                }
×
263
                v1index[key] = append(v1index[key], i)
×
264
        }
265
        overlap := map[int]int{}
×
266
        v1start := 0
×
267
        v2start := 0
×
268
        length := 0
×
269
        for v2pos, line := range v2lines {
×
270
                newOverlap := map[int]int{}
×
271
                key := util.BytesToReadOnlyString(line)
×
272
                if _, ok := v1index[key]; !ok {
×
273
                        v1index[key] = []int{}
×
274
                }
×
275
                for _, v1pos := range v1index[key] {
×
276
                        value := 0
×
277
                        if v1pos != 0 {
×
278
                                if v, ok := overlap[v1pos-1]; ok {
×
279
                                        value = v
×
280
                                }
×
281
                        }
282
                        newOverlap[v1pos] = value + 1
×
283
                        if newOverlap[v1pos] > length {
×
284
                                length = newOverlap[v1pos]
×
285
                                v1start = v1pos - length + 1
×
286
                                v2start = v2pos - length + 1
×
287
                        }
×
288
                }
289
                overlap = newOverlap
×
290
        }
291
        if length == 0 {
×
292
                diffs := []diff{}
×
293
                if len(v1lines) != 0 {
×
294
                        diffs = append(diffs, diff{diffRemoved, v1lines})
×
295
                }
×
296
                if len(v2lines) != 0 {
×
297
                        diffs = append(diffs, diff{diffAdded, v2lines})
×
298
                }
×
299
                return diffs
×
300
        }
301
        diffs := simpleDiffAux(v1lines[:v1start], v2lines[:v2start])
×
302
        diffs = append(diffs, diff{diffNone, v2lines[v2start : v2start+length]})
×
303
        diffs = append(diffs, simpleDiffAux(v1lines[v1start+length:],
×
304
                v2lines[v2start+length:])...)
×
305
        return diffs
×
306
}
307

308
// DiffPretty returns pretty formatted diff between given bytes.
309
func DiffPretty(v1, v2 []byte) []byte {
×
310
        var b bytes.Buffer
×
311
        diffs := simpleDiff(v1, v2)
×
312
        for _, diff := range diffs {
×
313
                c := " "
×
314
                switch diff.Type {
×
315
                case diffAdded:
×
316
                        c = "+"
×
317
                case diffRemoved:
×
318
                        c = "-"
×
319
                case diffNone:
×
320
                        c = " "
×
321
                }
322
                for _, line := range diff.Lines {
×
323
                        if c != " " {
×
NEW
324
                                fmt.Fprintf(&b, "%s | %s\n", c, util.VisualizeSpaces(line))
×
325
                        } else {
×
NEW
326
                                fmt.Fprintf(&b, "%s | %s\n", c, line)
×
327
                        }
×
328
                }
329
        }
330
        return b.Bytes()
×
331
}
332

333
func applyEscapeSequence(b []byte) []byte {
282✔
334
        result := make([]byte, 0, len(b))
282✔
335
        for i := 0; i < len(b); i++ {
8,928✔
336
                if b[i] == '\\' && i != len(b)-1 {
8,820✔
337
                        switch b[i+1] {
174✔
338
                        case 'a':
×
339
                                result = append(result, '\a')
×
340
                                i++
×
341
                                continue
×
342
                        case 'b':
×
343
                                result = append(result, '\b')
×
344
                                i++
×
345
                                continue
×
346
                        case 'f':
12✔
347
                                result = append(result, '\f')
12✔
348
                                i++
12✔
349
                                continue
12✔
350
                        case 'n':
39✔
351
                                result = append(result, '\n')
39✔
352
                                i++
39✔
353
                                continue
39✔
354
                        case 'r':
24✔
355
                                result = append(result, '\r')
24✔
356
                                i++
24✔
357
                                continue
24✔
358
                        case 't':
21✔
359
                                result = append(result, '\t')
21✔
360
                                i++
21✔
361
                                continue
21✔
362
                        case 'v':
6✔
363
                                result = append(result, '\v')
6✔
364
                                i++
6✔
365
                                continue
6✔
366
                        case '\\':
12✔
367
                                result = append(result, '\\')
12✔
368
                                i++
12✔
369
                                continue
12✔
370
                        case 'x':
12✔
371
                                if len(b) >= i+3 && util.IsHexDecimal(b[i+2]) && util.IsHexDecimal(b[i+3]) {
24✔
372
                                        v, _ := hex.DecodeString(string(b[i+2 : i+4]))
12✔
373
                                        result = append(result, v[0])
12✔
374
                                        i += 3
12✔
375
                                        continue
12✔
376
                                }
377
                        case 'u', 'U':
12✔
378
                                if len(b) > i+2 {
24✔
379
                                        num := []byte{}
12✔
380
                                        for j := i + 2; j < len(b); j++ {
72✔
381
                                                if util.IsHexDecimal(b[j]) {
108✔
382
                                                        num = append(num, b[j])
48✔
383
                                                        continue
48✔
384
                                                }
385
                                                break
12✔
386
                                        }
387
                                        if len(num) >= 4 && len(num) < 8 {
24✔
388
                                                v, _ := strconv.ParseInt(string(num[:4]), 16, 32)
12✔
389
                                                result = append(result, []byte(string(rune(v)))...)
12✔
390
                                                i += 5
12✔
391
                                                continue
12✔
392
                                        }
393
                                        if len(num) >= 8 {
×
394
                                                v, _ := strconv.ParseInt(string(num[:8]), 16, 32)
×
395
                                                result = append(result, []byte(string(rune(v)))...)
×
396
                                                i += 9
×
397
                                                continue
×
398
                                        }
399
                                }
400
                        }
401
                }
402
                result = append(result, b[i])
8,508✔
403
        }
404
        return result
282✔
405
}
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