1
0
Fork 0
forked from Mirrors/gomuks

hicli/html,web/timeline: add syntax highlighting for code blocks

This commit is contained in:
Tulir Asokan 2024-10-20 15:50:02 +03:00
parent 8cc475e66b
commit afa6d3aa4b
8 changed files with 115 additions and 15 deletions

View file

@ -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)
}

2
go.mod
View file

@ -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

10
go.sum
View file

@ -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=

View file

@ -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("</span>")
} 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("<pre><code")
if language != "" {
writeAttribute(w, "class", "language-"+language)
}
w.WriteByte('>')
writeEscapedString(w, block.String())
w.WriteString("</code></pre>")
return
}
err = CodeBlockFormatter.Format(w, styles.Fallback, iter)
if err != nil {
// This should never fail
panic(err)
}
}

View file

@ -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 {

View file

@ -18,6 +18,13 @@ import { createRoot } from "react-dom/client"
import App from "./App.tsx"
import "./index.css"
const styleTags = document.createElement("style")
styleTags.textContent = `
@import "_gomuks/codeblock/github-dark.css" (prefers-color-scheme: dark);
@import "_gomuks/codeblock/github.css" (prefers-color-scheme: light);
`
document.head.appendChild(styleTags)
fetch("/_gomuks/auth", { method: "POST" }).then(resp => {
if (resp.ok) {
createRoot(document.getElementById("root")!).render(

View file

@ -98,7 +98,7 @@ div.timeline-event {
grid-template:
"timestamp content status" auto
/ 3rem 1fr 2rem;
margin-top: 0;
margin-top: .25rem;
> div.sender-avatar, > div.event-sender-and-time {
display: none;