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 |
package doc
import (
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
var md = goldmark.New(goldmark.WithExtensions(extension.GFM))
// plainText is what a reader sees of a block: link text without the URL,
// emphasis without asterisks. Word diffs run on it so a revision reads as
// prose, not as Markdown syntax.
func plainText(kind, src string) string {
switch kind {
case KindCode:
return codeText(src)
case KindBreak:
return ""
case KindHTML:
return collapse(src)
case KindTable:
return tableText(src)
}
source := []byte(src)
root := md.Parser().Parse(text.NewReader(source))
var b strings.Builder
_ = ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
if n.Type() == ast.TypeBlock {
b.WriteByte(' ')
}
return ast.WalkContinue, nil
}
switch t := n.(type) {
case *ast.Text:
v := t.Segment.Value(source)
if _, inCode := t.Parent().(*ast.CodeSpan); !inCode {
v = util.ResolveEntityNames(util.ResolveNumericReferences(util.UnescapePunctuations(v)))
}
b.Write(v)
if t.SoftLineBreak() || t.HardLineBreak() {
b.WriteByte(' ')
}
case *ast.String:
b.Write(t.Value)
case *ast.AutoLink:
b.Write(t.Label(source))
return ast.WalkSkipChildren, nil
case *ast.FencedCodeBlock, *ast.CodeBlock, *ast.HTMLBlock:
lines := n.Lines()
for i := 0; i < lines.Len(); i++ {
seg := lines.At(i)
b.Write(seg.Value(source))
}
return ast.WalkSkipChildren, nil
case *ast.RawHTML:
return ast.WalkSkipChildren, nil
}
return ast.WalkContinue, nil
})
return collapse(b.String())
}
// tableText keeps one line per row and " | " between cells, so a changed
// cell is still read in its row. The delimiter row is dropped.
func tableText(src string) string {
var rows []string
for i, line := range strings.Split(src, "\n") {
if i == 1 && isDelimiterRow(line) {
continue
}
t := strings.TrimSpace(line)
t = strings.TrimSuffix(strings.TrimPrefix(t, "|"), "|")
cells := strings.Split(t, "|")
for j, c := range cells {
cells[j] = plainText(KindParagraph, strings.TrimSpace(c))
}
rows = append(rows, strings.Join(cells, " | "))
}
return strings.Join(rows, "\n")
}
// codeText keeps a code block's lines and drops its fences.
func codeText(src string) string {
lines := strings.Split(src, "\n")
if f := fenceMarker(lines[0]); f != "" {
lines = lines[1:]
if len(lines) > 0 && closesFence(lines[len(lines)-1], f) {
lines = lines[:len(lines)-1]
}
}
return strings.Join(lines, "\n")
}
func collapse(s string) string { return strings.Join(strings.Fields(s), " ") }
|