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}