Add server-side syntax highlighting

01d06286da7f9b6e0a3b88bf886366fca3b57853
Alexis Sellier committed ago 1 parent 30121682
git.go +12 -2
1 1
package main
2 2
3 3
import (
4 +
	"html/template"
4 5
	"sort"
5 6
	"strings"
6 7
	"time"
7 8
)
8 9
60 61
	Hunks    []DiffHunk
61 62
	IsBinary bool
62 63
	Stats    DiffStats
63 64
}
64 65
66 +
func diffFileName(file DiffFile) string {
67 +
	if file.NewName != "" {
68 +
		return file.NewName
69 +
	}
70 +
	return file.OldName
71 +
}
72 +
65 73
type DiffStats struct {
66 74
	Added   int
67 75
	Deleted int
68 76
}
69 77
73 81
}
74 82
75 83
type DiffLine struct {
76 84
	Type    string // "context", "add", "del"
77 85
	Content string
78 -
	OldNum  int
79 -
	NewNum  int
86 +
	// Highlighted contains escaped, syntax-highlighted source.
87 +
	Highlighted template.HTML
88 +
	OldNum      int
89 +
	NewNum      int
80 90
}
81 91
82 92
type TreeNode struct {
83 93
	Name     string
84 94
	IsDir    bool
git_cli.go +0 -0
+0 -0
go.mod +6 -1
1 1
module forge
2 2
3 3
go 1.25.7
4 4
5 5
require (
6 +
	github.com/alecthomas/chroma/v2 v2.27.0
7 +
	modernc.org/sqlite v1.46.1
8 +
)
9 +
10 +
require (
11 +
	github.com/dlclark/regexp2/v2 v2.2.1 // indirect
6 12
	github.com/dustin/go-humanize v1.0.1 // indirect
7 13
	github.com/google/uuid v1.6.0 // indirect
8 14
	github.com/mattn/go-isatty v0.0.20 // indirect
9 15
	github.com/ncruces/go-strftime v1.0.0 // indirect
10 16
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
11 17
	golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
12 18
	golang.org/x/sys v0.37.0 // indirect
13 19
	modernc.org/libc v1.67.6 // indirect
14 20
	modernc.org/mathutil v1.7.1 // indirect
15 21
	modernc.org/memory v1.11.0 // indirect
16 -
	modernc.org/sqlite v1.46.1 // indirect
17 22
)
go.sum +40 -0
1 +
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
2 +
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
3 +
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
4 +
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
5 +
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
6 +
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
7 +
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
8 +
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
1 9
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
2 10
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
11 +
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
12 +
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
3 13
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
4 14
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
15 +
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
16 +
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
17 +
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
18 +
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
5 19
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
6 20
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
7 21
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
8 22
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
9 23
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
10 24
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
11 25
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
12 26
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
27 +
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
28 +
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
29 +
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
30 +
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
13 31
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14 32
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
15 33
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
34 +
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
35 +
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
36 +
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
37 +
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
38 +
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
39 +
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
40 +
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
41 +
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
42 +
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
43 +
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
44 +
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
45 +
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
46 +
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
47 +
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
16 48
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
17 49
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
18 50
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
19 51
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
20 52
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
21 53
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
54 +
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
55 +
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
56 +
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
57 +
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
22 58
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
23 59
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
60 +
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
61 +
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
62 +
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
63 +
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
handler_test.go +14 -11
501 501
func TestTree_File(t *testing.T) {
502 502
	srv := testServer(t)
503 503
	ref := defaultRef(t, srv)
504 504
	w := get(t, srv, "/testrepo/tree/"+ref+"/main.go")
505 505
	assertCode(t, w, 200)
506 -
	assertContains(t, w, "package main")
507 -
	assertContains(t, w, "func main()")
506 +
	assertContains(t, w, `<span class="syntax-keyword">package</span> main`)
507 +
	assertContains(t, w, `<span class="syntax-keyword">func</span>`)
508 +
	assertContains(t, w, `class="syntax-punctuation"`)
508 509
	assertContains(t, w, "main.go")
509 510
	assertContains(t, w, "/raw/")
510 511
}
511 512
512 513
func TestTree_NestedFile(t *testing.T) {
513 514
	srv := testServer(t)
514 515
	ref := defaultRef(t, srv)
515 516
	w := get(t, srv, "/testrepo/tree/"+ref+"/pkg/lib.go")
516 517
	assertCode(t, w, 200)
517 -
	assertContains(t, w, "package pkg")
518 +
	assertContains(t, w, `<span class="syntax-keyword">package</span> pkg`)
518 519
}
519 520
520 521
func TestTree_BranchWithSlash(t *testing.T) {
521 522
	srv := testServer(t)
522 523
	w := get(t, srv, "/testrepo/tree/feature/test")
1608 1609
	if got := fn(2); !strings.Contains(got, "padding-left") {
1609 1610
		t.Errorf("got %q", got)
1610 1611
	}
1611 1612
}
1612 1613
1613 -
func TestFuncMap_LangClass(t *testing.T) {
1614 -
	fn := funcMap["langClass"].(func(string) string)
1615 -
	if got := fn("file.rad"); got != "language-radiance" {
1616 -
		t.Errorf("got %q", got)
1614 +
func TestHighlightLines(t *testing.T) {
1615 +
	lines := highlightLines("main.go", []string{"package main", "/* comment", "continued */", `var s = "<tag>"`})
1616 +
	if len(lines) != 4 {
1617 +
		t.Fatalf("got %d lines, want 4", len(lines))
1617 1618
	}
1618 -
	if got := fn("file.ril"); got != "language-ril" {
1619 -
		t.Errorf("got %q", got)
1619 +
	for _, line := range lines[1:3] {
1620 +
		if !strings.Contains(string(line), `class="syntax-comment"`) {
1621 +
			t.Errorf("multiline comment is not highlighted: %q", line)
1622 +
		}
1620 1623
	}
1621 -
	if got := fn("file.go"); got != "" {
1622 -
		t.Errorf("got %q", got)
1624 +
	if strings.Contains(string(lines[3]), "<tag>") || !strings.Contains(string(lines[3]), "&lt;tag&gt;") {
1625 +
		t.Errorf("highlighted source is not escaped: %q", lines[3])
1623 1626
	}
1624 1627
}
1625 1628
1626 1629
func TestFuncMap_Add(t *testing.T) {
1627 1630
	fn := funcMap["add"].(func(int, int) int)
file.go +0 -0
old.go +0 -0
new.go +0 -0
image.png +0 -0
file.go +0 -0
a.go +0 -0
b.go +0 -0
highlight.go added +167 -0
1 +
package main
2 +
3 +
import (
4 +
	"html"
5 +
	"html/template"
6 +
	"path"
7 +
	"strings"
8 +
9 +
	"github.com/alecthomas/chroma/v2"
10 +
	"github.com/alecthomas/chroma/v2/lexers"
11 +
)
12 +
13 +
var radianceLexer = chroma.MustNewLexer(
14 +
	&chroma.Config{Name: "Radiance"},
15 +
	func() chroma.Rules {
16 +
		return chroma.Rules{"root": {
17 +
			{Pattern: `//[^\n]*`, Type: chroma.CommentSingle},
18 +
			{Pattern: `"(?:\\.|[^"\\])*"|'[^']{1,2}'`, Type: chroma.LiteralString},
19 +
			{Pattern: `\b(?:0x[0-9a-fA-F]+|0b[01]+|[0-9]+(?:\.[0-9]+)?)\b`, Type: chroma.LiteralNumber},
20 +
			{Pattern: `@[a-zA-Z]+`, Type: chroma.NameBuiltin},
21 +
			{Pattern: `\b(?:fn|if|else|for|while|break|switch|match|set|record|union|constant|align|let|use|mod|module|case|continue|return|true|false|loop|extern|panic|device|register|bit|catch|throw|throws|test|trait|instance|at|mut|nil|undefined|static|in|is|where|export|as|and|or|xor|not|try|atomic|select|of|assert)\b`, Type: chroma.Keyword},
22 +
			{Pattern: `\b(?:bool|u8|u16|u32|u64|i8|i16|i32|i64|f32|void|opaque)\b`, Type: chroma.KeywordType},
23 +
			{Pattern: `->|=>|::|\.\.|<>|\?{1,2}|!{1,2}|>=?|<=?|[&*.=+\-/%]`, Type: chroma.Operator},
24 +
			{Pattern: `[(){}\[\]]`, Type: chroma.Punctuation},
25 +
			{Pattern: `\s+`, Type: chroma.TextWhitespace},
26 +
			{Pattern: `.`, Type: chroma.Text},
27 +
		}}
28 +
	},
29 +
)
30 +
31 +
var radianceILLexer = chroma.MustNewLexer(
32 +
	&chroma.Config{Name: "Radiance IL"},
33 +
	func() chroma.Rules {
34 +
		return chroma.Rules{"root": {
35 +
			{Pattern: `//[^\n]*`, Type: chroma.CommentSingle},
36 +
			{Pattern: `"[^"\n]*"`, Type: chroma.LiteralString},
37 +
			{Pattern: `%[0-9]+`, Type: chroma.NameVariable},
38 +
			{Pattern: `@[A-Za-z_][A-Za-z#0-9_]*`, Type: chroma.NameLabel},
39 +
			{Pattern: `\$[A-Za-z_][A-Za-z0-9_]*`, Type: chroma.Name},
40 +
			{Pattern: `\b(?:-?[0-9]+|0x[a-fA-F0-9]+)\b`, Type: chroma.LiteralNumber},
41 +
			{Pattern: `\b(?:fn|data|extern|mut|align)\b`, Type: chroma.Keyword},
42 +
			{Pattern: `\b(?:br\.eq|br\.ne|br\.slt|br\.ult|reserve|load|sload|store|blit|copy|add|sub|mul|sdiv|udiv|srem|urem|neg|eq|ne|slt|sge|ult|uge|and|or|xor|shl|sshr|ushr|not|zext|sext|call|ret|jmp|switch|unreachable|ecall|ebreak)\b`, Type: chroma.NameBuiltin},
43 +
			{Pattern: `\b(?:str|sym|undef|w8|w16|w32|w64)\b`, Type: chroma.KeywordType},
44 +
			{Pattern: `[{}();:,]`, Type: chroma.Punctuation},
45 +
			{Pattern: `\s+`, Type: chroma.TextWhitespace},
46 +
			{Pattern: `.`, Type: chroma.Text},
47 +
		}}
48 +
	},
49 +
)
50 +
51 +
// highlightLines returns one escaped HTML fragment for each source line.
52 +
func highlightLines(filename string, lines []string) []template.HTML {
53 +
	if len(lines) == 0 {
54 +
		return nil
55 +
	}
56 +
57 +
	lexer := lexerForFilename(filename)
58 +
	iterator, err := lexer.Tokenise(nil, strings.Join(lines, "\n"))
59 +
	if err != nil {
60 +
		return escapeLines(lines)
61 +
	}
62 +
63 +
	highlighted := make([]strings.Builder, len(lines))
64 +
	line := 0
65 +
	for token := iterator(); token != chroma.EOF; token = iterator() {
66 +
		parts := strings.Split(token.Value, "\n")
67 +
		class := tokenClass(token.Type)
68 +
		for i, part := range parts {
69 +
			if i > 0 {
70 +
				line++
71 +
			}
72 +
			if line >= len(highlighted) || part == "" {
73 +
				continue
74 +
			}
75 +
			if class != "" {
76 +
				highlighted[line].WriteString(`<span class="`)
77 +
				highlighted[line].WriteString(class)
78 +
				highlighted[line].WriteString(`">`)
79 +
			}
80 +
			highlighted[line].WriteString(html.EscapeString(part))
81 +
			if class != "" {
82 +
				highlighted[line].WriteString(`</span>`)
83 +
			}
84 +
		}
85 +
	}
86 +
87 +
	result := make([]template.HTML, len(highlighted))
88 +
	for i := range highlighted {
89 +
		result[i] = template.HTML(highlighted[i].String())
90 +
	}
91 +
	return result
92 +
}
93 +
94 +
func highlightDiff(file DiffFile, lines []DiffLine) {
95 +
	oldSource := make([]string, 0, len(lines))
96 +
	newSource := make([]string, 0, len(lines))
97 +
	for i := range lines {
98 +
		if lines[i].OldNum > 0 {
99 +
			oldSource = append(oldSource, lines[i].Content)
100 +
		}
101 +
		if lines[i].NewNum > 0 {
102 +
			newSource = append(newSource, lines[i].Content)
103 +
		}
104 +
	}
105 +
106 +
	oldHighlighted := highlightLines(file.OldName, oldSource)
107 +
	newHighlighted := highlightLines(file.NewName, newSource)
108 +
	oldIndex, newIndex := 0, 0
109 +
	for i := range lines {
110 +
		line := &lines[i]
111 +
		if line.Type == "del" {
112 +
			line.Highlighted = oldHighlighted[oldIndex]
113 +
		} else {
114 +
			line.Highlighted = newHighlighted[newIndex]
115 +
		}
116 +
		if line.OldNum > 0 {
117 +
			oldIndex++
118 +
		}
119 +
		if line.NewNum > 0 {
120 +
			newIndex++
121 +
		}
122 +
	}
123 +
}
124 +
125 +
func lexerForFilename(filename string) chroma.Lexer {
126 +
	switch strings.ToLower(path.Ext(filename)) {
127 +
	case ".rad":
128 +
		return radianceLexer
129 +
	case ".ril":
130 +
		return radianceILLexer
131 +
	}
132 +
	if lexer := lexers.Match(filename); lexer != nil {
133 +
		return chroma.Coalesce(lexer)
134 +
	}
135 +
	return lexers.Fallback
136 +
}
137 +
138 +
func tokenClass(tokenType chroma.TokenType) string {
139 +
	switch {
140 +
	case tokenType.InCategory(chroma.Keyword):
141 +
		return "syntax-keyword"
142 +
	case tokenType.InSubCategory(chroma.NameBuiltin):
143 +
		return "syntax-builtin"
144 +
	case tokenType.InSubCategory(chroma.LiteralString):
145 +
		return "syntax-string"
146 +
	case tokenType.InSubCategory(chroma.LiteralNumber):
147 +
		return "syntax-number"
148 +
	case tokenType.InCategory(chroma.Comment):
149 +
		return "syntax-comment"
150 +
	case tokenType.InCategory(chroma.Operator):
151 +
		return "syntax-operator"
152 +
	case tokenType.InCategory(chroma.Punctuation):
153 +
		return "syntax-punctuation"
154 +
	case tokenType.InSubCategory(chroma.NameVariable), tokenType == chroma.NameLabel:
155 +
		return "syntax-name"
156 +
	default:
157 +
		return ""
158 +
	}
159 +
}
160 +
161 +
func escapeLines(lines []string) []template.HTML {
162 +
	escaped := make([]template.HTML, len(lines))
163 +
	for i, line := range lines {
164 +
		escaped[i] = template.HTML(html.EscapeString(line))
165 +
	}
166 +
	return escaped
167 +
}
static/style.css +10 -25
778 778
footer p {
779 779
  display: flex;
780 780
  justify-content: space-between;
781 781
}
782 782
783 -
/* --- Syntax highlighting (Radiance) --- */
784 -
785 -
.language-radiance .comment { color: var(--color-dimmer); font-style: italic; }
786 -
.language-radiance .builtin { color: var(--color-secondary); }
787 -
.language-radiance .op { color: var(--color-dim); }
788 -
.language-radiance .access,
789 -
.language-radiance .delim { color: var(--color-dim); }
790 -
.language-radiance .keyword { color: var(--color-dim); font-weight: 600; }
791 -
.language-radiance .type { color: var(--color-highlight-dim); font-weight: 600; }
792 -
.language-radiance .ref { color: var(--color-dim); }
793 -
.language-radiance .string { color: var(--color-secondary); }
794 -
.language-radiance .number { color: var(--color-highlight); }
795 -
796 -
/* --- Syntax highlighting (Radiance IL) --- */
797 -
798 -
.language-ril .comment { color: var(--color-dimmer); font-style: italic; }
799 -
.language-ril .keyword,
800 -
.language-ril .instr { color: var(--color-dim); font-weight: 600; }
801 -
.language-ril .type { color: var(--color-highlight-dim); font-weight: 600; }
802 -
.language-ril .delim { color: var(--color-dim); }
803 -
.language-ril .number { color: var(--color-highlight); }
804 -
.language-ril .string { color: var(--color-secondary); }
805 -
.language-ril .reg { color: var(--color-secondary); }
806 -
.language-ril .symbol { color: var(--color-highlight-dim); }
807 -
.language-ril .label { color: var(--color-highlight-dim); font-style: italic; }
783 +
/* --- Syntax highlighting --- */
784 +
785 +
.syntax-comment { color: var(--color-dimmer); font-style: italic; }
786 +
.syntax-keyword { color: var(--color-dim); font-weight: 600; }
787 +
.syntax-builtin { color: var(--color-secondary); }
788 +
.syntax-string { color: var(--color-secondary); }
789 +
.syntax-number { color: var(--color-highlight); }
790 +
.syntax-operator,
791 +
.syntax-punctuation { color: var(--color-dim); }
792 +
.syntax-name { color: var(--color-highlight-dim); }
808 793
809 794
/* --- Discussions --- */
810 795
811 796
.discussions-header {
812 797
  display: flex;
template.go +3 -17
84 84
		return template.HTML(fmt.Sprintf(`<time title="%s">%s</time>`, full, ago))
85 85
	},
86 86
	"formatDate": func(t time.Time) string {
87 87
		return t.Format("2006-01-02 15:04:05 -0700")
88 88
	},
89 -
	"add": func(a, b int) int { return a + b },
90 -
	"diffFileName": func(f DiffFile) string {
91 -
		if f.NewName != "" {
92 -
			return f.NewName
93 -
		}
94 -
		return f.OldName
95 -
	},
89 +
	"add":          func(a, b int) int { return a + b },
90 +
	"diffFileName": diffFileName,
96 91
	"statusLabel": func(s string) string {
97 92
		switch s {
98 93
		case "A":
99 94
			return "added"
100 95
		case "D":
146 141
			b.WriteString(`<span class="bar-neutral"></span>`)
147 142
		}
148 143
		b.WriteString(`</span>`)
149 144
		return template.HTML(b.String())
150 145
	},
151 -
	"langClass": func(name string) string {
152 -
		ext := path.Ext(name)
153 -
		switch ext {
154 -
		case ".rad":
155 -
			return "language-radiance"
156 -
		case ".ril":
157 -
			return "language-ril"
158 -
		}
159 -
		return ""
160 -
	},
146 +
	"highlightLines": highlightLines,
161 147
	"parentPath": func(p string) string {
162 148
		dir := path.Dir(p)
163 149
		if dir == "." {
164 150
			return ""
165 151
		}
templates/commit.html +1 -1
32 32
  {{range .Lines}}
33 33
    <tr class="diff-line diff-{{.Type}}">
34 34
      <td class="diff-num">{{if .OldNum}}{{.OldNum}}{{end}}</td>
35 35
      <td class="diff-num">{{if .NewNum}}{{.NewNum}}{{end}}</td>
36 36
      <td class="diff-marker">{{if eq .Type "add"}}+{{else if eq .Type "del"}}-{{else}} {{end}}</td>
37 -
      <td class="line-code"><pre{{with langClass (diffFileName $file)}} class="{{.}}"{{end}}>{{.Content}}</pre></td>
37 +
      <td class="line-code"><pre>{{.Highlighted}}</pre></td>
38 38
    </tr>
39 39
  {{end}}
40 40
  </tbody>
41 41
  </table>
42 42
  {{end}}
templates/home.html +2 -1
53 53
      <p class="empty">Binary file. <a href="{{.BaseURL}}/{{.Repo}}/raw/{{.Data.DefaultRef}}/{{.Data.ActivePath}}">Download</a></p>
54 54
      {{else}}
55 55
      <div class="blob-view">
56 56
      <table class="blob-code">
57 57
      <tbody>
58 +
      {{$lines := highlightLines .Data.ActiveBlob.Name .Data.ActiveBlob.Lines}}
58 59
      {{range $i, $line := .Data.ActiveBlob.Lines}}
59 60
      <tr id="L{{add $i 1}}">
60 61
        <td class="line-num"><a href="#L{{add $i 1}}">{{add $i 1}}</a></td>
61 -
        <td class="line-code"><pre{{with langClass $.Data.ActiveBlob.Name}} class="{{.}}"{{end}}>{{autolink $line}}</pre></td>
62 +
        <td class="line-code"><pre>{{index $lines $i}}</pre></td>
62 63
      </tr>
63 64
      {{end}}
64 65
      </tbody>
65 66
      </table>
66 67
      </div>