diff --git a/cmd/gomuks/server.go b/cmd/gomuks/server.go index cc4da75..7f57ab2 100644 --- a/cmd/gomuks/server.go +++ b/cmd/gomuks/server.go @@ -28,6 +28,7 @@ import ( "strings" "time" + "github.com/alecthomas/chroma/v2/styles" "github.com/rs/zerolog/hlog" "go.mau.fi/util/exerrors" "go.mau.fi/util/exhttp" @@ -36,6 +37,7 @@ import ( "golang.org/x/crypto/bcrypt" "maunium.net/go/mautrix" + "go.mau.fi/gomuks/pkg/hicli" "go.mau.fi/gomuks/web" ) @@ -45,6 +47,7 @@ func (gmx *Gomuks) StartServer() { api.HandleFunc("POST /auth", gmx.Authenticate) api.HandleFunc("POST /upload", gmx.UploadMedia) api.HandleFunc("GET /media/{server}/{media_id}", gmx.DownloadMedia) + api.HandleFunc("GET /codeblock/{style}", gmx.GetCodeblockCSS) apiHandler := exhttp.ApplyMiddleware( api, hlog.NewHandler(*gmx.Log), @@ -226,3 +229,18 @@ func (gmx *Gomuks) AuthMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +func (gmx *Gomuks) GetCodeblockCSS(w http.ResponseWriter, r *http.Request) { + styleName := r.PathValue("style") + if !strings.HasSuffix(styleName, ".css") { + w.WriteHeader(http.StatusNotFound) + return + } + style := styles.Get(strings.TrimSuffix(styleName, ".css")) + if style == nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "text/css") + _ = hicli.CodeBlockFormatter.WriteCSS(w, style) +} diff --git a/go.mod b/go.mod index f70c85f..9c14635 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 toolchain go1.23.2 require ( + github.com/alecthomas/chroma/v2 v2.14.0 github.com/chzyer/readline v1.5.1 github.com/coder/websocket v1.8.12 github.com/gabriel-vasile/mimetype v1.4.6 @@ -29,6 +30,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect diff --git a/go.sum b/go.sum index 477d158..be078a3 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,12 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= @@ -14,9 +20,13 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= diff --git a/pkg/hicli/html.go b/pkg/hicli/html.go index d0c1332..2ae7a9f 100644 --- a/pkg/hicli/html.go +++ b/pkg/hicli/html.go @@ -17,6 +17,10 @@ import ( "strconv" "strings" + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/alecthomas/chroma/v2/styles" "golang.org/x/net/html" "golang.org/x/net/html/atom" "maunium.net/go/mautrix/id" @@ -45,7 +49,7 @@ func isSelfClosing(tag atom.Atom) bool { } } -var languageRegex = regexp.MustCompile(`^language-[a-zA-Z0-9-]+$`) +var languageRegex = regexp.MustCompile(`^language-([a-zA-Z0-9-]+)$`) var allowedColorRegex = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) // This is approximately a mirror of web/src/util/mediasize.ts in gomuks @@ -489,9 +493,23 @@ func (ts *tagStack) pop(tag atom.Atom) bool { return false } +func getCodeBlockLanguage(token html.Token) string { + for _, attr := range token.Attr { + if attr.Key == "class" { + match := languageRegex.FindStringSubmatch(attr.Val) + if len(match) == 2 { + return match[1] + } + } + } + return "" +} + func sanitizeAndLinkifyHTML(body string) (string, []id.ContentURI, error) { tz := html.NewTokenizer(strings.NewReader(body)) var built strings.Builder + var codeBlock *strings.Builder + var codeBlockLanguage string var inlineImages []id.ContentURI ts := make(tagStack, 2) Loop: @@ -505,6 +523,13 @@ Loop: return "", nil, err case html.StartTagToken, html.SelfClosingTagToken: token := tz.Token() + if codeBlock != nil { + if token.DataAtom == atom.Code { + codeBlockLanguage = getCodeBlockLanguage(token) + } + // Don't allow any tags inside code blocks + continue + } if !tagIsAllowed(token.DataAtom) { continue } @@ -513,6 +538,9 @@ Loop: continue } switch token.DataAtom { + case atom.Pre: + codeBlock = &strings.Builder{} + continue case atom.A: mxc := writeA(&built, token.Attr) if !mxc.IsEmpty() { @@ -541,7 +569,11 @@ Loop: case html.EndTagToken: tagName, _ := tz.TagName() tag := atom.Lookup(tagName) - if tagIsAllowed(tag) && ts.pop(tag) { + if tag == atom.Pre && codeBlock != nil { + writeCodeBlock(&built, codeBlockLanguage, codeBlock) + codeBlockLanguage = "" + codeBlock = nil + } else if tagIsAllowed(tag) && ts.pop(tag) { if tag == atom.Font { built.WriteString("") } else { @@ -551,7 +583,9 @@ Loop: } } case html.TextToken: - if ts.contains(atom.Pre, atom.Code, atom.A) { + if codeBlock != nil { + codeBlock.Write(tz.Text()) + } else if ts.contains(atom.Pre, atom.Code, atom.A) { writeEscapedBytes(&built, tz.Text()) } else { linkifyAndWriteBytes(&built, tz.Text()) @@ -568,3 +602,32 @@ Loop: } return built.String(), inlineImages, nil } + +var CodeBlockFormatter = chromahtml.New( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(true), +) + +func writeCodeBlock(w *strings.Builder, language string, block *strings.Builder) { + lexer := lexers.Get(language) + if lexer == nil { + lexer = lexers.Fallback + } + lexer = chroma.Coalesce(lexer) + iter, err := lexer.Tokenise(nil, block.String()) + if err != nil { + w.WriteString("
')
+ writeEscapedString(w, block.String())
+ w.WriteString("
")
+ return
+ }
+ err = CodeBlockFormatter.Format(w, styles.Fallback, iter)
+ if err != nil {
+ // This should never fail
+ panic(err)
+ }
+}
diff --git a/pkg/hicli/sync.go b/pkg/hicli/sync.go
index 36c59b3..7e3a3bd 100644
--- a/pkg/hicli/sync.go
+++ b/pkg/hicli/sync.go
@@ -388,7 +388,7 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
return nil, nil
}
-const CurrentHTMLSanitizerVersion = 2
+const CurrentHTMLSanitizerVersion = 3
func (h *HiClient) ReprocessExistingEvent(ctx context.Context, evt *database.Event) {
if evt.Type != event.EventMessage.Type || evt.LocalContent == nil || evt.LocalContent.HTMLVersion >= CurrentHTMLSanitizerVersion {
diff --git a/web/index.html b/web/index.html
index 0127288..496ba7a 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1,13 +1,13 @@
-
-
-
-
-