Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-12-28 08:15:51
Commitd737f58666affab00577c1433ca7d04fd54d9ee2
Parentd6784d5fe91e45dad537cd5ae4a85898d1e07d24

Add syntax highlighting

Diffstat

M README.md | 2 +-
M go.mod | 2 ++
M go.sum | 4 ++++
M res/repo/file.html | 6 ++----
M src/repo/file.go | 18 +++++++++++++-----
A src/repo/highlight.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

6 files changed, 100 insertions, 10 deletions

diff --git a/README.md b/README.md
index 1b75d1d..9954bd5 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ A simple and lightweight Git web server.
 - Git Smart HTTP protocol (v2 only)
 - Git SSH protocol (planned)
 - Repository log, tree, refs, and commit viewers
-- File viewer with (planned) syntax highlighting
+- File viewer with syntax highlighting
 - File log, blame (planned), and raw views
 - Public and private repositories
 - Read and write permissions for non owners (planned)
diff --git a/go.mod b/go.mod
index 6778b5a..03063dd 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.21.0
 
 require (
 	github.com/adrg/xdg v0.4.0
+	github.com/alecthomas/chroma v0.10.0
 	github.com/buildkite/terminal-to-html/v3 v3.10.1
 	github.com/dustin/go-humanize v1.0.1
 	github.com/go-chi/chi/v5 v5.0.11
@@ -19,6 +20,7 @@ require (
 	github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
 	github.com/cloudflare/circl v1.3.6 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.4 // indirect
+	github.com/dlclark/regexp2 v1.4.0 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/go-git/go-billy/v5 v5.5.0 // indirect
diff --git a/go.sum b/go.sum
index 03ce2bb..4527859 100644
--- a/go.sum
+++ b/go.sum
@@ -7,6 +7,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX
 github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
 github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
 github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
+github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
+github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -22,6 +24,8 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
+github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
diff --git a/res/repo/file.html b/res/repo/file.html
index cfa6f52..716f6b7 100644
--- a/res/repo/file.html
+++ b/res/repo/file.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<head lang="en-GB">{{template "base/head" .}}</head>
+<head lang="en-GB">{{template "base/head" .}}{{.BodyCss}}</head>
 <body>
 	<header>
 		{{template "repo/header" .}}<hr>
@@ -12,9 +12,7 @@
 					<td class="lnum" style="text-align: right;">
 						<pre>{{range $i, $l := .Lines}}<a id="{{$i}}" href="#{{$i}}">{{$i}}</a>{{end}}</pre>
 					</td>
-					<td class="line">
-						<pre>{{.Body}}</pre>
-					</td>
+					<td class="line">{{.HtmlBody}}</td>
 				</tr>
 			{{else}}
 				<tr><td>Binary file</td></tr>
diff --git a/src/repo/file.go b/src/repo/file.go
index f45935a..5a4ba8d 100644
--- a/src/repo/file.go
+++ b/src/repo/file.go
@@ -44,10 +44,9 @@ func HandleFile(w http.ResponseWriter, r *http.Request) {
 		HeaderFields
 		Title, Path, LineC, Size, Mode string
 		Lines                          []string
-		Body                           string
-		HtmlPath                       template.HTML
+		HtmlBody, HtmlPath, BodyCss    template.HTML
 	}{
-		Title:        repo.Name + " - File",
+		Title:        repo.Name + " - " + tpath,
 		HeaderFields: GetHeaderFields(auth, user, repo, r.Host),
 	}
 
@@ -129,8 +128,17 @@ func HandleFile(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 
-			data.Body = string(append(buf, buf2...))
-			data.Lines = strings.Split(data.Body, "\n")
+			body := string(append(buf, buf2...))
+			buf, css, err := Highlight(file.Name, body)
+			if err != nil {
+				log.Println("[/repo/file]", err.Error())
+				goit.HttpError(w, http.StatusInternalServerError)
+				return
+			}
+
+			data.HtmlBody = template.HTML(buf)
+			data.BodyCss = template.HTML("<style>" + css + "</style>")
+			data.Lines = strings.Split(body, "\n")
 		}
 
 		rc.Close()
diff --git a/src/repo/highlight.go b/src/repo/highlight.go
new file mode 100644
index 0000000..16e75ca
--- /dev/null
+++ b/src/repo/highlight.go
@@ -0,0 +1,78 @@
+// Copyright (C) 2023, Jakob Wakeling
+// All rights reserved.
+
+package repo
+
+import (
+	"bytes"
+
+	"github.com/alecthomas/chroma"
+	"github.com/alecthomas/chroma/formatters/html"
+	"github.com/alecthomas/chroma/lexers"
+	"github.com/alecthomas/chroma/styles"
+)
+
+func Highlight(name, input string) (string, string, error) {
+	var buf, css bytes.Buffer
+
+	lexer := lexers.Match(name)
+	if lexer == nil {
+		lexer = lexers.Fallback
+	}
+
+	formatter := html.New(html.WithClasses(true))
+
+	iter, err := lexer.Tokenise(nil, input)
+	if err != nil {
+		return "", "", err
+	}
+
+	if err := formatter.Format(&buf, goitStyle, iter); err != nil {
+		return "", "", err
+	}
+
+	if err := formatter.WriteCSS(&css, goitStyle); err != nil {
+		return "", "", err
+	}
+
+	return buf.String(), css.String(), nil
+}
+
+var goitStyle = styles.Register(chroma.MustNewStyle("goit", chroma.StyleEntries{
+	chroma.Background:            "#888888",
+	chroma.Comment:               "italic #666666",
+	chroma.CommentPreproc:        "noinherit #8ec07c",
+	chroma.CommentPreprocFile:    "noinherit #b8bb26",
+	chroma.GenericDeleted:        "#d65d0e",
+	chroma.GenericEmph:           "italic",
+	chroma.GenericError:          "bold bg:#fb4934",
+	chroma.GenericHeading:        "bold #fabd2f",
+	chroma.GenericInserted:       "#b8bb26",
+	chroma.GenericOutput:         "#504945",
+	chroma.GenericPrompt:         "#ebdbb2",
+	chroma.GenericStrong:         "bold",
+	chroma.GenericSubheading:     "bold #fabd2f",
+	chroma.GenericTraceback:      "bold bg:#fb4934",
+	chroma.GenericUnderline:      "underline",
+	chroma.Keyword:               "#fb4934",
+	chroma.KeywordNamespace:      "#d3869b",
+	chroma.KeywordType:           "#fabd2f",
+	chroma.LiteralNumber:         "#d3869b",
+	chroma.LiteralString:         "#b8bb26",
+	chroma.LiteralStringEscape:   "#d3869b",
+	chroma.LiteralStringInterpol: "#8ec07c",
+	chroma.LiteralStringRegex:    "#fe8019",
+	chroma.LiteralStringSymbol:   "#83a598",
+	chroma.Name:                  "#ebdbb2",
+	chroma.NameAttribute:         "#fabd2f",
+	chroma.NameBuiltin:           "#fabd2f",
+	chroma.NameClass:             "#fabd2f",
+	chroma.NameConstant:          "#d3869b",
+	chroma.NameEntity:            "#fabd2f",
+	chroma.NameException:         "#fb4934",
+	chroma.NameFunction:          "#fabd2f",
+	chroma.NameLabel:             "#fb4934",
+	chroma.NameTag:               "#8ec07c",
+	chroma.NameVariable:          "#83a598",
+	chroma.Operator:              "#8ec07c",
+}))