main
diff_render.go
Eric Bower
·
2026-09-14
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "html"
7 "strconv"
8 "strings"
9
10 "github.com/alecthomas/chroma/v2"
11 formatterHtml "github.com/alecthomas/chroma/v2/formatters/html"
12 "github.com/alecthomas/chroma/v2/lexers"
13 "github.com/alecthomas/chroma/v2/styles"
14 git "github.com/gogs/git-module"
15)
16
17var inlineChromaFormatter = formatterHtml.New(
18 formatterHtml.WithClasses(true),
19 formatterHtml.PreventSurroundingPre(true),
20)
21
22func resolveLexer(fileName string) chroma.Lexer {
23 lexer := lexers.Match(fileName)
24 if lexer == nil {
25 lexer = lexers.Fallback
26 }
27 return chroma.Coalesce(lexer)
28}
29
30// FormatDiffHunk formats a git.DiffSection as a syntax-highlighted HTML table
31// with diff overlay classes and clickable anchor links on line numbers.
32func FormatDiffHunk(theme *chroma.Style, fileName string, section *git.DiffSection, hunkAnchor string) (string, error) {
33 if section == nil {
34 return "", nil
35 }
36 if theme == nil {
37 theme = styles.Fallback
38 }
39 lexer := resolveLexer(fileName)
40
41 var buf bytes.Buffer
42 buf.WriteString("<div class=\"chroma diff-container\"><table class=\"diff-table\"><tbody class=\"diff-hunk\">")
43
44 var hunkHeader string
45 var diffLines []*git.DiffLine
46
47 if len(section.Lines) > 0 && section.Lines[0].Type == git.DiffLineSection {
48 hunkHeader = strings.TrimSpace(section.Lines[0].Content)
49 diffLines = section.Lines[1:]
50 } else {
51 diffLines = section.Lines
52 }
53
54 if hunkHeader != "" {
55 buf.WriteString("<tr class=\"diff-line diff-line-hunk\">")
56 buf.WriteString("<td class=\"diff-num diff-num-old\">...</td>")
57 buf.WriteString("<td class=\"diff-num diff-num-new\">...</td>")
58 buf.WriteString("<td class=\"diff-gutter\"></td>")
59 buf.WriteString("<td class=\"diff-code\"><span class=\"gu\">")
60 buf.WriteString(html.EscapeString(hunkHeader))
61 buf.WriteString("</span></td></tr>\n")
62 }
63
64 for _, line := range diffLines {
65 var oldNumStr, newNumStr, gutter, rowClass string
66 var oldAnchorID, newAnchorID string
67
68 switch line.Type {
69 case git.DiffLinePlain:
70 if line.LeftLine > 0 {
71 oldNumStr = strconv.Itoa(line.LeftLine)
72 }
73 if line.RightLine > 0 {
74 newNumStr = strconv.Itoa(line.RightLine)
75 }
76 if hunkAnchor != "" {
77 if oldNumStr != "" {
78 oldAnchorID = fmt.Sprintf("%s-L%s", hunkAnchor, oldNumStr)
79 }
80 if newNumStr != "" {
81 newAnchorID = fmt.Sprintf("%s-R%s", hunkAnchor, newNumStr)
82 }
83 }
84 gutter = " "
85 rowClass = "diff-line diff-line-context"
86 case git.DiffLineDelete:
87 if line.LeftLine > 0 {
88 oldNumStr = strconv.Itoa(line.LeftLine)
89 }
90 newNumStr = ""
91 if hunkAnchor != "" && oldNumStr != "" {
92 oldAnchorID = fmt.Sprintf("%s-L%s", hunkAnchor, oldNumStr)
93 }
94 gutter = "-"
95 rowClass = "diff-line diff-line-delete"
96 case git.DiffLineAdd:
97 oldNumStr = ""
98 if line.RightLine > 0 {
99 newNumStr = strconv.Itoa(line.RightLine)
100 }
101 if hunkAnchor != "" && newNumStr != "" {
102 newAnchorID = fmt.Sprintf("%s-R%s", hunkAnchor, newNumStr)
103 }
104 gutter = "+"
105 rowClass = "diff-line diff-line-add"
106 default:
107 gutter = " "
108 rowClass = "diff-line diff-line-context"
109 }
110
111 buf.WriteString("<tr class=\"")
112 buf.WriteString(rowClass)
113 buf.WriteString("\">")
114
115 // Old line number column with anchor link
116 buf.WriteString("<td class=\"diff-num diff-num-old\"")
117 if oldAnchorID != "" {
118 buf.WriteString(" id=\"")
119 buf.WriteString(html.EscapeString(oldAnchorID))
120 buf.WriteString("\"")
121 }
122 if oldNumStr != "" {
123 buf.WriteString(" data-line-number=\"")
124 buf.WriteString(oldNumStr)
125 buf.WriteString("\">")
126 if oldAnchorID != "" {
127 buf.WriteString("<a href=\"#")
128 buf.WriteString(html.EscapeString(oldAnchorID))
129 buf.WriteString("\">")
130 buf.WriteString(oldNumStr)
131 buf.WriteString("</a>")
132 } else {
133 buf.WriteString(oldNumStr)
134 }
135 } else {
136 buf.WriteString(">")
137 }
138 buf.WriteString("</td>")
139
140 // New line number column with anchor link
141 buf.WriteString("<td class=\"diff-num diff-num-new\"")
142 if newAnchorID != "" {
143 buf.WriteString(" id=\"")
144 buf.WriteString(html.EscapeString(newAnchorID))
145 buf.WriteString("\"")
146 }
147 if newNumStr != "" {
148 buf.WriteString(" data-line-number=\"")
149 buf.WriteString(newNumStr)
150 buf.WriteString("\">")
151 if newAnchorID != "" {
152 buf.WriteString("<a href=\"#")
153 buf.WriteString(html.EscapeString(newAnchorID))
154 buf.WriteString("\">")
155 buf.WriteString(newNumStr)
156 buf.WriteString("</a>")
157 } else {
158 buf.WriteString(newNumStr)
159 }
160 } else {
161 buf.WriteString(">")
162 }
163 buf.WriteString("</td>")
164
165 // Gutter column
166 buf.WriteString("<td class=\"diff-gutter\">")
167 buf.WriteString(html.EscapeString(gutter))
168 buf.WriteString("</td>")
169
170 // Code cell
171 buf.WriteString("<td class=\"diff-code\">")
172 rawLine := line.Content
173 var lineContent string
174 if len(rawLine) > 0 && (rawLine[0] == ' ' || rawLine[0] == '+' || rawLine[0] == '-') {
175 lineContent = rawLine[1:]
176 } else {
177 lineContent = rawLine
178 }
179 lineContent = strings.TrimRight(lineContent, "\r\n")
180 if lineContent == "" {
181 buf.WriteString("\n")
182 } else {
183 it, err := lexer.Tokenise(nil, lineContent)
184 if err != nil {
185 buf.WriteString(html.EscapeString(lineContent))
186 } else {
187 if err := inlineChromaFormatter.Format(&buf, theme, it); err != nil {
188 buf.WriteString(html.EscapeString(lineContent))
189 }
190 }
191 }
192 buf.WriteString("</td></tr>\n")
193 }
194
195 buf.WriteString("</tbody></table></div>")
196 return buf.String(), nil
197}
198
199// FormatDiffFile formats all hunks in a git.DiffFile into syntax-highlighted HTML.
200func FormatDiffFile(theme *chroma.Style, file *git.DiffFile) (string, error) {
201 if file == nil {
202 return "", nil
203 }
204 if file.IsBinary() {
205 return "<div class=\"chroma diff-container\"><pre style=\"padding: var(--grid-height);\">Binaries are not rendered as diffs.</pre></div>", nil
206 }
207 if len(file.Sections) == 0 {
208 return "", nil
209 }
210
211 fileName := file.Name
212 if fileName == "" {
213 fileName = file.OldName()
214 }
215
216 var buf bytes.Buffer
217 for hunkIdx, section := range file.Sections {
218 hunkAnchor := fmt.Sprintf("diff-%s-hunk-%d", fileName, hunkIdx)
219 hunkHtml, err := FormatDiffHunk(theme, fileName, section, hunkAnchor)
220 if err != nil {
221 return "", err
222 }
223 buf.WriteString(hunkHtml)
224 }
225 return buf.String(), nil
226}