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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
|
// Package render turns raw repository bytes into HTML: syntax-highlighted code
// via chroma and rendered Markdown via goldmark. It holds no git knowledge.
package render
import (
"bytes"
"html/template"
"path"
"regexp"
"strings"
"unicode/utf8"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/extension"
"gopkg.in/yaml.v3"
)
// FMPair is one ordered key/value from a Markdown file's YAML frontmatter.
type FMPair struct {
Key string
Value string
}
// SplitFrontmatter separates a leading `---`-delimited YAML block from the body.
// No frontmatter → fm is nil and body is the whole input.
func SplitFrontmatter(raw []byte) (fm, body []byte) {
s := string(raw)
if !strings.HasPrefix(s, "---") {
return nil, raw
}
nl := strings.IndexByte(s, '\n')
if nl < 0 {
return nil, raw
}
rest := s[nl+1:]
end := strings.Index(rest, "\n---")
if end < 0 {
return nil, raw
}
fm = []byte(rest[:end])
after := rest[end+1:] // starts at closing "---"
if nl2 := strings.IndexByte(after, '\n'); nl2 >= 0 {
body = []byte(after[nl2+1:])
}
return fm, body
}
// ParseFrontmatter parses a YAML frontmatter block into ordered key/value pairs
// for display. Sequence values are comma-joined; nested maps are flattened to a
// compact YAML string. Returns nil on empty or invalid input.
func ParseFrontmatter(fm []byte) []FMPair {
if len(bytes.TrimSpace(fm)) == 0 {
return nil
}
var doc yaml.Node
if err := yaml.Unmarshal(fm, &doc); err != nil || len(doc.Content) == 0 {
return nil
}
root := doc.Content[0]
if root.Kind != yaml.MappingNode {
return nil
}
var pairs []FMPair
for i := 0; i+1 < len(root.Content); i += 2 {
pairs = append(pairs, FMPair{
Key: root.Content[i].Value,
Value: nodeString(root.Content[i+1]),
})
}
return pairs
}
func nodeString(n *yaml.Node) string {
switch n.Kind {
case yaml.ScalarNode:
return n.Value
case yaml.SequenceNode:
parts := make([]string, 0, len(n.Content))
for _, c := range n.Content {
parts = append(parts, nodeString(c))
}
return strings.Join(parts, ", ")
default:
var b bytes.Buffer
enc := yaml.NewEncoder(&b)
_ = enc.Encode(n)
_ = enc.Close()
return strings.TrimSpace(b.String())
}
}
// md renders GFM and highlights fenced code blocks with chroma classes, so
// code in READMEs/issue bodies is colored by the same theme tokens as blobs.
var md = goldmark.New(goldmark.WithExtensions(
extension.GFM,
highlighting.NewHighlighting(
highlighting.WithFormatOptions(html.WithClasses(true)),
),
))
// chroma formatters emit classes (not inline styles) so themes drive colors.
// formatter is for source files (with a line-number gutter); diffFormatter omits
// line numbers, which would fight a patch's own +/- columns.
var formatter = html.New(html.WithClasses(true), html.WithLineNumbers(true), html.LineNumbersInTable(true))
var diffFormatter = html.New(html.WithClasses(true))
// classStyle is irrelevant to class-based output but required by the API.
var classStyle = styles.Get("github")
// MD is the context for resolving a Markdown file's relative image paths to the
// repo's raw endpoint. A zero Repo disables rewriting.
type MD struct {
Repo string // repo name
Ref string // ref the file is viewed at
Dir string // directory of the Markdown file (for relative resolution)
}
// Markdown renders GitHub-flavored Markdown to HTML, rewriting relative <img>
// sources to /r/<repo>/raw/<ref>/<path> so embedded images resolve on the web.
func Markdown(src []byte, ctx MD) (template.HTML, error) {
var buf bytes.Buffer
if err := md.Convert(src, &buf); err != nil {
return "", err
}
out := buf.String()
if ctx.Repo != "" {
out = rewriteRelImages(out, ctx)
}
return template.HTML(out), nil // #nosec G203 -- trusted repo content, rendered by goldmark
}
var imgSrcRe = regexp.MustCompile(`(<img\b[^>]*?\bsrc=")([^"]*)(")`)
// rewriteRelImages points relative <img src> at the repo's raw endpoint.
func rewriteRelImages(htmlStr string, ctx MD) string {
return imgSrcRe.ReplaceAllStringFunc(htmlStr, func(m string) string {
g := imgSrcRe.FindStringSubmatch(m)
dest := g[2]
if !isRelativeURL(dest) {
return m
}
clean := path.Join(ctx.Dir, dest) // resolves ./ and ../ against the file's dir
return g[1] + "/r/" + ctx.Repo + "/raw/" + ctx.Ref + "/" + clean + g[3]
})
}
func isRelativeURL(u string) bool {
if u == "" {
return false
}
for _, p := range []string{"http://", "https://", "//", "/", "data:", "#", "mailto:"} {
if strings.HasPrefix(u, p) {
return false
}
}
return true
}
// Highlight returns syntax-highlighted HTML for code, choosing a lexer by
// filename then content. Binary content yields ok=false so callers can show a
// "binary file" notice instead.
func Highlight(filename string, code []byte) (out template.HTML, ok bool) {
if isBinary(code) {
return "", false
}
lexer := lexers.Match(filename)
if lexer == nil {
lexer = lexers.Analyse(string(code))
}
if lexer == nil {
lexer = lexers.Fallback
}
lexer = chroma.Coalesce(lexer)
iterator, err := lexer.Tokenise(nil, string(code))
if err != nil {
return "", false
}
var buf bytes.Buffer
if err := formatter.Format(&buf, classStyle, iterator); err != nil {
return "", false
}
return template.HTML(buf.String()), true // #nosec G203 -- escaped by chroma formatter
}
// FileDiff is one file's slice of a commit diff: its name, highlighted hunks,
// and add/delete line counts.
type FileDiff struct {
Name string
HTML template.HTML
Added int
Deleted int
Binary bool
}
// SplitDiff breaks a unified diff into per-file sections. Each section's git
// metadata (diff --git / index / ---/+++ lines) is dropped — the filename is
// surfaced separately — and the hunks are highlighted on their own.
func SplitDiff(diff string) []FileDiff {
if strings.TrimSpace(diff) == "" {
return nil
}
lines := strings.Split(diff, "\n")
var files []FileDiff
var cur []string
flush := func() {
if len(cur) == 0 {
return
}
files = append(files, buildFileDiff(cur))
cur = nil
}
for _, ln := range lines {
if strings.HasPrefix(ln, "diff --git ") {
flush()
}
cur = append(cur, ln)
}
flush()
return files
}
func buildFileDiff(block []string) FileDiff {
fd := FileDiff{Name: diffName(block)}
hunk := -1
for i, ln := range block {
switch {
case strings.HasPrefix(ln, "@@") && hunk < 0:
hunk = i
case strings.HasPrefix(ln, "Binary files "), strings.HasPrefix(ln, "GIT binary patch"):
fd.Binary = true
case strings.HasPrefix(ln, "+") && !strings.HasPrefix(ln, "+++"):
fd.Added++
case strings.HasPrefix(ln, "-") && !strings.HasPrefix(ln, "---"):
fd.Deleted++
}
}
if hunk >= 0 {
fd.HTML = HighlightDiff(strings.Join(block[hunk:], "\n"))
}
return fd
}
// diffName extracts a display name from a file block's git header, formatting
// renames as "old → new".
func diffName(block []string) string {
for _, ln := range block {
if strings.HasPrefix(ln, "diff --git ") {
fields := strings.Fields(ln)
if len(fields) >= 4 {
a := strings.TrimPrefix(fields[len(fields)-2], "a/")
b := strings.TrimPrefix(fields[len(fields)-1], "b/")
if a != b {
return a + " → " + b
}
return b
}
}
}
for _, ln := range block {
if strings.HasPrefix(ln, "+++ b/") {
return strings.TrimPrefix(ln, "+++ b/")
}
}
return "diff"
}
// HighlightDiff renders a unified diff with chroma's diff lexer (class-based:
// .gi inserts, .gd deletes, .gu/.gh hunk headers). Falls back to escaped text.
func HighlightDiff(diff string) template.HTML {
lexer := lexers.Get("diff")
if lexer == nil {
return template.HTML("<pre class=\"chroma\">" + template.HTMLEscapeString(diff) + "</pre>") // #nosec G203 -- escaped
}
iterator, err := lexer.Tokenise(nil, diff)
if err != nil {
return template.HTML("<pre class=\"chroma\">" + template.HTMLEscapeString(diff) + "</pre>") // #nosec G203 -- escaped
}
var buf bytes.Buffer
if err := diffFormatter.Format(&buf, classStyle, iterator); err != nil {
return template.HTML("<pre class=\"chroma\">" + template.HTMLEscapeString(diff) + "</pre>") // #nosec G203 -- escaped
}
return template.HTML(buf.String()) // #nosec G203 -- escaped by chroma formatter
}
// IsMarkdown reports whether a filename should be rendered as Markdown.
func IsMarkdown(name string) bool {
switch strings.ToLower(path.Ext(name)) {
case ".md", ".markdown", ".mdown":
return true
}
return false
}
// isBinary uses a NUL-byte heuristic over the first chunk, like git.
func isBinary(b []byte) bool {
const sniff = 8000
if len(b) > sniff {
b = b[:sniff]
}
if bytes.IndexByte(b, 0) >= 0 {
return true
}
return !utf8.Valid(b)
}
|