Commit a630835
Eric Bower
·
2026-09-14 14:56:24 -0400 EDT
parent 4e9469b
feat: commit diff renderer
4 files changed,
+513,
-8
+226,
-0
| ... | ... | @@ -0,0 +1,226 @@ | |
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 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 | + | ||
| 17 | + | var inlineChromaFormatter = formatterHtml.New( | |
| 18 | + | formatterHtml.WithClasses(true), | |
| 19 | + | formatterHtml.PreventSurroundingPre(true), | |
| 20 | + | ) | |
| 21 | + | ||
| 22 | + | func 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. | |
| 32 | + | func 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. | |
| 200 | + | func 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 | + | } |
+160,
-0
| ... | ... | @@ -0,0 +1,160 @@ | |
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/alecthomas/chroma/v2/styles" | |
| 8 | + | git "github.com/gogs/git-module" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestFormatDiffHunkGo(t *testing.T) { | |
| 12 | + | section := &git.DiffSection{ | |
| 13 | + | Lines: []*git.DiffLine{ | |
| 14 | + | {Type: git.DiffLineSection, Content: "@@ -1,5 +1,6 @@"}, | |
| 15 | + | {Type: git.DiffLinePlain, Content: " package main", LeftLine: 1, RightLine: 1}, | |
| 16 | + | {Type: git.DiffLinePlain, Content: " ", LeftLine: 2, RightLine: 2}, | |
| 17 | + | {Type: git.DiffLineDelete, Content: "-func OldFunction() int {", LeftLine: 3}, | |
| 18 | + | {Type: git.DiffLineAdd, Content: "+func NewFunction() string {", RightLine: 3}, | |
| 19 | + | {Type: git.DiffLineAdd, Content: "+\t// added comment", RightLine: 4}, | |
| 20 | + | {Type: git.DiffLinePlain, Content: " \treturn 42", LeftLine: 4, RightLine: 5}, | |
| 21 | + | {Type: git.DiffLinePlain, Content: " }", LeftLine: 5, RightLine: 6}, | |
| 22 | + | }, | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | theme := styles.Get("dracula") | |
| 26 | + | anchor := "diff-main.go-hunk-0" | |
| 27 | + | htmlOut, err := FormatDiffHunk(theme, "main.go", section, anchor) | |
| 28 | + | if err != nil { | |
| 29 | + | t.Fatalf("FormatDiffHunk: %v", err) | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // Verify table structure | |
| 33 | + | if !strings.Contains(htmlOut, "diff-table") { | |
| 34 | + | t.Errorf("expected diff-table class in output") | |
| 35 | + | } | |
| 36 | + | if !strings.Contains(htmlOut, "diff-line-hunk") { | |
| 37 | + | t.Errorf("expected diff-line-hunk class in output") | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // Verify line number and diff overlays | |
| 41 | + | if !strings.Contains(htmlOut, "diff-line-add") { | |
| 42 | + | t.Errorf("expected diff-line-add class in output") | |
| 43 | + | } | |
| 44 | + | if !strings.Contains(htmlOut, "diff-line-delete") { | |
| 45 | + | t.Errorf("expected diff-line-delete class in output") | |
| 46 | + | } | |
| 47 | + | if !strings.Contains(htmlOut, "diff-line-context") { | |
| 48 | + | t.Errorf("expected diff-line-context class in output") | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // Verify gutter markers | |
| 52 | + | if !strings.Contains(htmlOut, "<td class=\"diff-gutter\">+</td>") { | |
| 53 | + | t.Errorf("expected '+' in gutter") | |
| 54 | + | } | |
| 55 | + | if !strings.Contains(htmlOut, "<td class=\"diff-gutter\">-</td>") { | |
| 56 | + | t.Errorf("expected '-' in gutter") | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // Verify anchor links in line numbers | |
| 60 | + | if !strings.Contains(htmlOut, `id="diff-main.go-hunk-0-L3"`) { | |
| 61 | + | t.Errorf("expected deleted line anchor id in output, got:\n%s", htmlOut) | |
| 62 | + | } | |
| 63 | + | if !strings.Contains(htmlOut, `href="#diff-main.go-hunk-0-L3"`) { | |
| 64 | + | t.Errorf("expected deleted line anchor href in output, got:\n%s", htmlOut) | |
| 65 | + | } | |
| 66 | + | if !strings.Contains(htmlOut, `id="diff-main.go-hunk-0-R3"`) { | |
| 67 | + | t.Errorf("expected added line anchor id in output, got:\n%s", htmlOut) | |
| 68 | + | } | |
| 69 | + | if !strings.Contains(htmlOut, `href="#diff-main.go-hunk-0-R3"`) { | |
| 70 | + | t.Errorf("expected added line anchor href in output, got:\n%s", htmlOut) | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // Verify syntax highlighting (Chroma classes for Go keywords / comments) | |
| 74 | + | if !strings.Contains(htmlOut, "class=\"kd\"") && !strings.Contains(htmlOut, "class=\"k\"") { | |
| 75 | + | t.Errorf("expected keyword chroma class (kd or k) for 'func' or 'package', got:\n%s", htmlOut) | |
| 76 | + | } | |
| 77 | + | if !strings.Contains(htmlOut, "class=\"c1\"") && !strings.Contains(htmlOut, "class=\"c\"") { | |
| 78 | + | t.Errorf("expected comment chroma class (c1 or c) for '// added comment', got:\n%s", htmlOut) | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func TestFormatDiffHunkZig(t *testing.T) { | |
| 83 | + | section := &git.DiffSection{ | |
| 84 | + | Lines: []*git.DiffLine{ | |
| 85 | + | {Type: git.DiffLineSection, Content: "@@ -1,4 +1,5 @@"}, | |
| 86 | + | {Type: git.DiffLinePlain, Content: " const std = @import(\"std\");", LeftLine: 1, RightLine: 1}, | |
| 87 | + | {Type: git.DiffLinePlain, Content: " ", LeftLine: 2, RightLine: 2}, | |
| 88 | + | {Type: git.DiffLineDelete, Content: "-pub fn main() void {", LeftLine: 3}, | |
| 89 | + | {Type: git.DiffLineAdd, Content: "+pub fn main() !void {", RightLine: 3}, | |
| 90 | + | {Type: git.DiffLineAdd, Content: "+ // zig comment", RightLine: 4}, | |
| 91 | + | {Type: git.DiffLinePlain, Content: " }", LeftLine: 4, RightLine: 5}, | |
| 92 | + | }, | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | htmlOut, err := FormatDiffHunk(nil, "src/main.zig", section, "diff-main.zig-hunk-0") | |
| 96 | + | if err != nil { | |
| 97 | + | t.Fatalf("FormatDiffHunk: %v", err) | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // Verify Zig syntax highlighting tokens | |
| 101 | + | if !strings.Contains(htmlOut, "class=\"k\"") && !strings.Contains(htmlOut, "class=\"kd\"") { | |
| 102 | + | t.Errorf("expected keyword class for 'pub' or 'fn' or 'const', got:\n%s", htmlOut) | |
| 103 | + | } | |
| 104 | + | if !strings.Contains(htmlOut, "diff-line-add") { | |
| 105 | + | t.Errorf("expected diff-line-add in zig diff") | |
| 106 | + | } | |
| 107 | + | if !strings.Contains(htmlOut, `href="#diff-main.zig-hunk-0-R3"`) { | |
| 108 | + | t.Errorf("expected anchor link in zig diff, got:\n%s", htmlOut) | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | func TestFormatDiffHunkFallback(t *testing.T) { | |
| 113 | + | section := &git.DiffSection{ | |
| 114 | + | Lines: []*git.DiffLine{ | |
| 115 | + | {Type: git.DiffLineSection, Content: "@@ -1,2 +1,2 @@"}, | |
| 116 | + | {Type: git.DiffLineDelete, Content: "-foo", LeftLine: 1}, | |
| 117 | + | {Type: git.DiffLineAdd, Content: "+bar", RightLine: 1}, | |
| 118 | + | }, | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // Unknown extension should not error and still render diff table correctly | |
| 122 | + | htmlOut, err := FormatDiffHunk(nil, "unknown.xyz", section, "") | |
| 123 | + | if err != nil { | |
| 124 | + | t.Fatalf("FormatDiffHunk: %v", err) | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | if !strings.Contains(htmlOut, "diff-table") { | |
| 128 | + | t.Errorf("expected diff-table class in output") | |
| 129 | + | } | |
| 130 | + | if !strings.Contains(htmlOut, "diff-line-add") { | |
| 131 | + | t.Errorf("expected diff-line-add in unknown file diff") | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func TestFormatDiffFile(t *testing.T) { | |
| 136 | + | file := &git.DiffFile{ | |
| 137 | + | Name: "test.go", | |
| 138 | + | Sections: []*git.DiffSection{ | |
| 139 | + | { | |
| 140 | + | Lines: []*git.DiffLine{ | |
| 141 | + | {Type: git.DiffLineSection, Content: "@@ -1,2 +1,2 @@"}, | |
| 142 | + | {Type: git.DiffLineDelete, Content: "-package old", LeftLine: 1}, | |
| 143 | + | {Type: git.DiffLineAdd, Content: "+package new", RightLine: 1}, | |
| 144 | + | }, | |
| 145 | + | }, | |
| 146 | + | }, | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | htmlOut, err := FormatDiffFile(nil, file) | |
| 150 | + | if err != nil { | |
| 151 | + | t.Fatalf("FormatDiffFile: %v", err) | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | if !strings.Contains(htmlOut, "diff-test.go-hunk-0") { | |
| 155 | + | t.Errorf("expected hunk anchor in file diff") | |
| 156 | + | } | |
| 157 | + | if !strings.Contains(htmlOut, "diff-line-add") { | |
| 158 | + | t.Errorf("expected diff-line-add") | |
| 159 | + | } | |
| 160 | + | } |
M
main.go
+12,
-8
| ... | ... | @@ -617,14 +617,7 @@ func (c *Config) writeLogDiff(repo *git.Repository, pageData PageData, commit *C | |
| 617 | 617 | NumAdditions: file.NumAdditions(), | |
| 618 | 618 | NumDeletions: file.NumDeletions(), | |
| 619 | 619 | } | |
| 620 | - | content := "" | |
| 621 | - | for _, section := range file.Sections { | |
| 622 | - | for _, line := range section.Lines { | |
| 623 | - | content += fmt.Sprintf("%s\n", line.Content) | |
| 624 | - | } | |
| 625 | - | } | |
| 626 | - | // set filename to something our `ParseText` recognizes (e.g. `.diff`) | |
| 627 | - | finContent, err := c.parseText("commit.diff", content) | |
| 620 | + | finContent, err := FormatDiffFile(c.Theme, file) | |
| 628 | 621 | bail(err) | |
| 629 | 622 | ||
| 630 | 623 | fl.Content = template.HTML(finContent) |
| ... | ... | @@ -1220,6 +1213,16 @@ func style(theme chroma.Style) string { | |
| 1220 | 1213 | --link-color: %s; | |
| 1221 | 1214 | --hover: %s; | |
| 1222 | 1215 | --visited: %s; | |
| 1216 | + | --grey-light: %s; | |
| 1217 | + | --admin: #f38ba8; | |
| 1218 | + | --success: #66f859; | |
| 1219 | + | --diff-add-bg: rgba(102, 248, 89, 0.12); | |
| 1220 | + | --diff-add-gutter: var(--success); | |
| 1221 | + | --diff-del-bg: rgba(243, 139, 168, 0.12); | |
| 1222 | + | --diff-del-gutter: var(--admin); | |
| 1223 | + | --diff-hunk-bg: rgba(139, 233, 253, 0.08); | |
| 1224 | + | --diff-hunk-text: var(--link-color); | |
| 1225 | + | --diff-num-text: var(--grey-light); | |
| 1223 | 1226 | }`, | |
| 1224 | 1227 | bg.Background.String(), | |
| 1225 | 1228 | txt.Colour.String(), |
| ... | ... | @@ -1227,6 +1230,7 @@ func style(theme chroma.Style) string { | |
| 1227 | 1230 | nv.Colour.String(), | |
| 1228 | 1231 | kw.Colour.String(), | |
| 1229 | 1232 | ln.Colour.String(), | |
| 1233 | + | cm.Colour.String(), | |
| 1230 | 1234 | ) | |
| 1231 | 1235 | } | |
| 1232 | 1236 |
+115,
-0
| ... | ... | @@ -300,3 +300,118 @@ footer { | |
| 300 | 300 | display: none; | |
| 301 | 301 | } | |
| 302 | 302 | } | |
| 303 | + | ||
| 304 | + | .color-green { | |
| 305 | + | color: var(--success); | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | .color-red { | |
| 309 | + | color: var(--admin); | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | .chroma .gi { | |
| 313 | + | font-weight: normal !important; | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | .diff-container { | |
| 317 | + | width: 100%; | |
| 318 | + | overflow-x: auto; | |
| 319 | + | border: 1px solid var(--border); | |
| 320 | + | border-radius: 4px; | |
| 321 | + | margin: var(--grid-height) 0; | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | .diff-table { | |
| 325 | + | width: 100%; | |
| 326 | + | border-collapse: collapse; | |
| 327 | + | font-family: monospace; | |
| 328 | + | font-size: 0.88em; | |
| 329 | + | line-height: var(--line-height); | |
| 330 | + | tab-size: 4; | |
| 331 | + | margin: 0; | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | .diff-line { | |
| 335 | + | vertical-align: top; | |
| 336 | + | } | |
| 337 | + | ||
| 338 | + | .diff-num { | |
| 339 | + | width: 1%; | |
| 340 | + | min-width: 2.5rem; | |
| 341 | + | padding: 0 0.5rem; | |
| 342 | + | text-align: right; | |
| 343 | + | color: var(--diff-num-text, var(--grey-light)); | |
| 344 | + | user-select: none; | |
| 345 | + | -webkit-user-select: none; | |
| 346 | + | font-size: 0.85em; | |
| 347 | + | white-space: nowrap; | |
| 348 | + | scroll-margin-top: 4rem; | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | .diff-num a { | |
| 352 | + | color: inherit; | |
| 353 | + | text-decoration: none; | |
| 354 | + | display: block; | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | .diff-num a:hover { | |
| 358 | + | color: var(--link-color); | |
| 359 | + | text-decoration: underline; | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | .diff-num-old { | |
| 363 | + | border-right: 1px solid transparent; | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | .diff-num-new { | |
| 367 | + | border-right: 1px solid var(--border); | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | .diff-line:has(.diff-num:target), | |
| 371 | + | .diff-line:target { | |
| 372 | + | background-color: rgba(189, 147, 249, 0.2) !important; | |
| 373 | + | outline: 1px solid var(--visited); | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | .diff-gutter { | |
| 377 | + | width: 1%; | |
| 378 | + | min-width: 1.2rem; | |
| 379 | + | padding: 0 0.25rem; | |
| 380 | + | text-align: center; | |
| 381 | + | user-select: none; | |
| 382 | + | -webkit-user-select: none; | |
| 383 | + | font-weight: bold; | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | .diff-code { | |
| 387 | + | padding: 0 0.5rem; | |
| 388 | + | white-space: pre; | |
| 389 | + | font-family: inherit; | |
| 390 | + | width: 100%; | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | .diff-line-add { | |
| 394 | + | background-color: var(--diff-add-bg, rgba(102, 248, 89, 0.12)); | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | .diff-line-add .diff-gutter { | |
| 398 | + | color: var(--diff-add-gutter, var(--success)); | |
| 399 | + | } | |
| 400 | + | ||
| 401 | + | .diff-line-delete { | |
| 402 | + | background-color: var(--diff-del-bg, rgba(243, 139, 168, 0.12)); | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | .diff-line-delete .diff-gutter { | |
| 406 | + | color: var(--diff-del-gutter, var(--admin)); | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | .diff-line-hunk { | |
| 410 | + | background-color: var(--diff-hunk-bg, rgba(139, 233, 253, 0.08)); | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | .diff-line-hunk .diff-code, | |
| 414 | + | .diff-line-hunk .diff-num { | |
| 415 | + | color: var(--diff-hunk-text, var(--link-color)); | |
| 416 | + | } | |
| 417 | + |