Add server-side syntax highlighting
01d06286da7f9b6e0a3b88bf886366fca3b57853
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]), "<tag>") { |
|
| 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> |