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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140 |
package render
import (
"fmt"
"html"
"strings"
"git.pablomurad.com/pablo/patchwork/internal/diff"
"git.pablomurad.com/pablo/patchwork/internal/doc"
)
// Kind labels shown to readers.
var kindLabel = map[string]string{
"correction": "Correction",
"expansion": "Expansion",
"position": "Change of position",
"reorganization": "Reorganization",
}
// EntryTitle is a feed entry's title: what kind of revision, of which page.
func EntryTitle(r Revision) string {
if l, ok := kindLabel[r.Kind]; ok {
return l + ": " + r.Page.Title
}
return "Revised: " + r.Page.Title
}
// EntryHTML is a feed entry's content: the note, a summary line, the
// excerpt, and links. Feed readers strip styles, so the diff relies on what
// <del> and <ins> mean, and every block is a plain paragraph.
func EntryHTML(r Revision) string {
var b strings.Builder
if r.Note != "" {
fmt.Fprintf(&b, "<p><strong>%s</strong></p>\n", esc(r.Note))
}
var facts []string
if l, ok := kindLabel[r.Kind]; ok {
facts = append(facts, l)
}
if r.Maturity != "" {
facts = append(facts, "maturity: "+r.Maturity)
}
facts = append(facts, fmt.Sprintf("+%d / −%d words", r.Stats.WordsAdded, r.Stats.WordsRemoved))
fmt.Fprintf(&b, "<p><small>%s</small></p>\n", esc(strings.Join(facts, " · ")))
for _, m := range r.MetaChanges {
if m.Key == "maturity" {
continue // already in the summary line
}
fmt.Fprintf(&b, "<p>%s: <del>%s</del> <ins>%s</ins></p>\n", esc(m.Key), esc(metaText(m.Old)), esc(metaText(m.New)))
}
b.WriteString("<hr>\n")
for _, op := range r.Excerpt.Blocks {
b.WriteString(opHTML(op))
}
if r.Excerpt.Truncated {
b.WriteString("<p>…</p>\n")
}
fmt.Fprintf(&b, "<p><a href=\"%s\">Read version %d</a> · <a href=\"%s\">Current page</a> · <a href=\"%s\">History</a></p>\n",
esc(r.VersionURL), r.Number, esc(r.Page.URL), esc(r.HistoryURL))
return b.String()
}
func opHTML(op diff.Op) string {
wrap := func(inner string) string {
switch op.Kind {
case doc.KindCode, doc.KindTable:
return "<pre>" + inner + "</pre>\n"
case doc.KindHeading:
return "<h3>" + inner + "</h3>\n"
case doc.KindQuote:
return "<blockquote>" + inner + "</blockquote>\n"
case doc.KindListItem:
return "<ul><li>" + inner + "</li></ul>\n"
}
return "<p>" + inner + "</p>\n"
}
switch op.Op {
case diff.OpGap:
if op.Removed {
return fmt.Sprintf("<p><del><small>[%d more removed]</small></del></p>\n", op.Count)
}
return fmt.Sprintf("<p><small>[%d unchanged]</small></p>\n", op.Count)
case diff.OpEq:
return wrap(esc(op.Text))
case diff.OpInsert:
return wrap("<ins>" + esc(op.Text) + "</ins>")
case diff.OpDelete:
return wrap("<del>" + esc(op.Text) + "</del>")
case diff.OpMove:
return "<p><small>[moved]</small></p>\n" + wrap(esc(op.Text))
case diff.OpModify:
if op.Replaced {
// Rewritten block: old and new side by side would run together.
var out strings.Builder
for _, s := range op.Segments {
tag := "ins"
if s.T == diff.SegDel {
tag = "del"
}
out.WriteString(wrap("<" + tag + ">" + esc(s.S) + "</" + tag + ">"))
}
return out.String()
}
var in strings.Builder
for i, s := range op.Segments {
switch s.T {
case diff.SegDel:
in.WriteString("<del>" + esc(s.S) + "</del>")
case diff.SegIns:
// Removed and inserted words side by side would read as one
// word in a reader that strips styles.
if i > 0 && op.Segments[i-1].T == diff.SegDel {
in.WriteString(" ")
}
in.WriteString("<ins>" + esc(s.S) + "</ins>")
default:
in.WriteString(esc(s.S))
}
}
return wrap(in.String())
}
return ""
}
func esc(s string) string { return html.EscapeString(s) }
func metaText(v any) string {
switch t := v.(type) {
case nil:
return "(none)"
case []any:
parts := make([]string, len(t))
for i, x := range t {
parts[i] = fmt.Sprint(x)
}
return strings.Join(parts, ", ")
}
return fmt.Sprint(v)
}
|