code.pablomurad.com

patchwork

Publishes the revisions of your pages as a feed, with notes and readable diffs, written in Go

internal/doc/plaintext.go raw
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), " ") }