diff --git a/go.mod b/go.mod index a2eb872..f62d05e 100644 --- a/go.mod +++ b/go.mod @@ -12,19 +12,19 @@ require ( github.com/mattn/go-runewidth v0.0.13 github.com/mattn/go-sqlite3 v1.14.12 github.com/rivo/uniseg v0.2.0 - github.com/russross/blackfriday/v2 v2.1.0 github.com/sasha-s/go-deadlock v0.3.1 + github.com/yuin/goldmark v1.4.11 github.com/zyedidia/clipboard v1.0.3 go.etcd.io/bbolt v1.3.6 go.mau.fi/cbind v0.0.0-20220415094356-e1d579b7925e - go.mau.fi/mauview v0.1.4-0.20220415105047-1b3ec9f6480e + go.mau.fi/mauview v0.1.4-0.20220415142327-ec5e0de686a8 go.mau.fi/tcell v0.0.0-20220415093808-07c67d224693 golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 golang.org/x/net v0.0.0-20220412020605-290c469a71a5 gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 gopkg.in/vansante/go-ffprobe.v2 v2.0.3 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b - maunium.net/go/mautrix v0.10.13-0.20220409135345-cbe2dbaef0e9 + maunium.net/go/mautrix v0.10.13-0.20220415172627-597a9560f721 ) require ( diff --git a/go.sum b/go.sum index b7a3232..d29a3d1 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -50,14 +48,16 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= +github.com/yuin/goldmark v1.4.11 h1:i45YIzqLnUc2tGaTlJCyUxSG8TvgyGqhqOZOUKIjJ6w= +github.com/yuin/goldmark v1.4.11/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= github.com/zyedidia/clipboard v1.0.3 h1:F/nCDVYMdbDWTmY8s8cJl0tnwX32q96IF09JHM14bUI= github.com/zyedidia/clipboard v1.0.3/go.mod h1:zykFnZUXX0ErxqvYLUFEq7QDJKId8rmh2FgD0/Y8cjA= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.mau.fi/cbind v0.0.0-20220415094356-e1d579b7925e h1:zY4TZmHAaUhrMFJQfh02dqxDYSfnnXlw/qRoFanxZTw= go.mau.fi/cbind v0.0.0-20220415094356-e1d579b7925e/go.mod h1:9nnzlslhUo/xO+8tsQgkFqG/W+SgD+r0iTYAuglzlmA= -go.mau.fi/mauview v0.1.4-0.20220415105047-1b3ec9f6480e h1:fG4llCg1rIk63dhSdRrliRa8lj81VZ8UzIysVgWKLJ8= -go.mau.fi/mauview v0.1.4-0.20220415105047-1b3ec9f6480e/go.mod h1:CPqlQWgiHEJHLNyD8vjMXotnPluMz0eDpKKCimjxFYE= +go.mau.fi/mauview v0.1.4-0.20220415142327-ec5e0de686a8 h1:+1CFVnx1OmCptuxF/5s2yOD96iYt3+6o/f0x7uyhUGQ= +go.mau.fi/mauview v0.1.4-0.20220415142327-ec5e0de686a8/go.mod h1:CPqlQWgiHEJHLNyD8vjMXotnPluMz0eDpKKCimjxFYE= go.mau.fi/tcell v0.0.0-20220415093808-07c67d224693 h1:pCfn8BS6fiGG1inpebysWQxeRmK/nsgKPMF9TupqYNQ= go.mau.fi/tcell v0.0.0-20220415093808-07c67d224693/go.mod h1:HQLPCz9v8YfYewMetOKrg9pe87XEyNcIfCYYq8VxQbU= golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= @@ -99,5 +99,5 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0= maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= -maunium.net/go/mautrix v0.10.13-0.20220409135345-cbe2dbaef0e9 h1:LcZDeMbuaj+f/WdOfX+OjLHjH58fRCu0PLmt+XO+iyY= -maunium.net/go/mautrix v0.10.13-0.20220409135345-cbe2dbaef0e9/go.mod h1:+LYU9Y5wunwC3ya7O4FuweCBQ5MVaglcgO/DGmXDbcg= +maunium.net/go/mautrix v0.10.13-0.20220415172627-597a9560f721 h1:QuCkJp2siYbXtZPStpYqPZ9Cz6yFCEVOGkBkmUchg7M= +maunium.net/go/mautrix v0.10.13-0.20220415172627-597a9560f721/go.mod h1:zOor2zO/F10T/GbU67vWr0vnhLso88rlRr1HIrb1XWU= diff --git a/ui/commands.go b/ui/commands.go index 97d9ad0..ecf8978 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -37,7 +37,7 @@ import ( "unicode" "github.com/lucasb-eyer/go-colorful" - "github.com/russross/blackfriday/v2" + "github.com/yuin/goldmark" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" @@ -85,21 +85,22 @@ var rainbow = GradientTable{ {colorful.LinearRgb(1, 0, 0.5), 11 / 11.0}, } +var rainbowMark = goldmark.New(format.Extensions, format.HTMLOptions, goldmark.WithExtensions(ExtensionRainbow)) + // TODO this command definitely belongs in a plugin once we have a plugin system. func makeRainbow(cmd *Command, msgtype event.MessageType) { text := strings.Join(cmd.Args, " ") - render := NewRainbowRenderer(blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ - Flags: blackfriday.UseXHTML, - })) - htmlBodyBytes := blackfriday.Run([]byte(text), format.Extensions, blackfriday.WithRenderer(render)) - htmlBody := strings.TrimRight(string(htmlBodyBytes), "\n") + var buf strings.Builder + _ = rainbowMark.Convert([]byte(text), &buf) + + htmlBody := strings.TrimRight(buf.String(), "\n") htmlBody = format.AntiParagraphRegex.ReplaceAllString(htmlBody, "$1") text = format.HTMLToText(htmlBody) - count := strings.Count(htmlBody, render.ColorID) + count := strings.Count(htmlBody, defaultRB.ColorID) i := -1 - htmlBody = regexp.MustCompile(render.ColorID).ReplaceAllStringFunc(htmlBody, func(match string) string { + htmlBody = regexp.MustCompile(defaultRB.ColorID).ReplaceAllStringFunc(htmlBody, func(match string) string { i++ return rainbow.GetInterpolatedColorFor(float64(i) / float64(count)).Hex() }) diff --git a/ui/rainbow.go b/ui/rainbow.go index 8a564be..83f2800 100644 --- a/ui/rainbow.go +++ b/ui/rainbow.go @@ -1,5 +1,5 @@ // gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2020 Tulir Asokan +// Copyright (C) 2022 Tulir Asokan // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -17,24 +17,18 @@ package ui import ( - "bytes" "fmt" - "html" - "io" "math/rand" "unicode" "github.com/rivo/uniseg" - "github.com/russross/blackfriday/v2" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" ) -type RainbowRenderer struct { - *blackfriday.HTMLRenderer - sr *blackfriday.SPRenderer - - ColorID string -} - func Rand(n int) (str string) { b := make([]byte, n) rand.Read(b) @@ -42,69 +36,100 @@ func Rand(n int) (str string) { return } -func NewRainbowRenderer(html *blackfriday.HTMLRenderer) *RainbowRenderer { - return &RainbowRenderer{ - HTMLRenderer: html, - sr: blackfriday.NewSmartypantsRenderer(html.Flags), - ColorID: Rand(16), +type extRainbow struct{} +type rainbowRenderer struct { + HardWraps bool + ColorID string +} + +var ExtensionRainbow = &extRainbow{} +var defaultRB = &rainbowRenderer{HardWraps: true, ColorID: Rand(16)} + +func (er *extRainbow) Extend(m goldmark.Markdown) { + m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(defaultRB, 0))) +} + +func (rb *rainbowRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(ast.KindText, rb.renderText) + reg.Register(ast.KindString, rb.renderString) +} + +type rainbowBufWriter struct { + util.BufWriter + ColorID string +} + +func (rbw rainbowBufWriter) WriteString(s string) (int, error) { + i := 0 + graphemes := uniseg.NewGraphemes(s) + for graphemes.Next() { + runes := graphemes.Runes() + if len(runes) == 1 && unicode.IsSpace(runes[0]) { + i2, err := rbw.BufWriter.WriteRune(runes[0]) + i += i2 + if err != nil { + return i, err + } + continue + } + i2, err := fmt.Fprintf(rbw.BufWriter, "%s", rbw.ColorID, graphemes.Str()) + i += i2 + if err != nil { + return i, err + } + } + return i, nil +} + +func (rbw rainbowBufWriter) Write(data []byte) (int, error) { + return rbw.WriteString(string(data)) +} + +func (rbw rainbowBufWriter) WriteByte(c byte) error { + _, err := rbw.WriteRune(rune(c)) + return err +} + +func (rbw rainbowBufWriter) WriteRune(r rune) (int, error) { + if unicode.IsSpace(r) { + return rbw.BufWriter.WriteRune(r) + } else { + return fmt.Fprintf(rbw.BufWriter, "%c", rbw.ColorID, r) } } -func (r *RainbowRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { - if node.Type == blackfriday.Text { - var buf bytes.Buffer - if r.Flags&blackfriday.Smartypants != 0 { - var tmp bytes.Buffer - escapeHTML(&tmp, node.Literal) - r.sr.Process(&buf, tmp.Bytes()) +func (rb *rainbowRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + n := node.(*ast.Text) + segment := n.Segment + if n.IsRaw() { + html.DefaultWriter.RawWrite(rainbowBufWriter{w, rb.ColorID}, segment.Value(source)) + } else { + html.DefaultWriter.Write(rainbowBufWriter{w, rb.ColorID}, segment.Value(source)) + if n.HardLineBreak() || (n.SoftLineBreak() && rb.HardWraps) { + _, _ = w.WriteString("
\n") + } else if n.SoftLineBreak() { + _ = w.WriteByte('\n') + } + } + return ast.WalkContinue, nil +} + +func (rb *rainbowRenderer) renderString(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + n := node.(*ast.String) + if n.IsCode() { + _, _ = w.Write(n.Value) + } else { + if n.IsRaw() { + html.DefaultWriter.RawWrite(rainbowBufWriter{w, rb.ColorID}, n.Value) } else { - if node.Parent.Type == blackfriday.Link { - escLink(&buf, node.Literal) - } else { - escapeHTML(&buf, node.Literal) - } + html.DefaultWriter.Write(rainbowBufWriter{w, rb.ColorID}, n.Value) } - graphemes := uniseg.NewGraphemes(buf.String()) - buf.Reset() - for graphemes.Next() { - runes := graphemes.Runes() - if len(runes) == 1 && unicode.IsSpace(runes[0]) { - buf.WriteRune(runes[0]) - continue - } - _, _ = fmt.Fprintf(&buf, "%s", r.ColorID, graphemes.Str()) - } - _, _ = w.Write(buf.Bytes()) - return blackfriday.GoToNext } - return r.HTMLRenderer.RenderNode(w, node, entering) -} - -// This stuff is copied directly from blackfriday -var htmlEscaper = [256][]byte{ - '&': []byte("&"), - '<': []byte("<"), - '>': []byte(">"), - '"': []byte("""), -} - -func escapeHTML(w io.Writer, s []byte) { - var start, end int - for end < len(s) { - escSeq := htmlEscaper[s[end]] - if escSeq != nil { - w.Write(s[start:end]) - w.Write(escSeq) - start = end + 1 - } - end++ - } - if start < len(s) && end <= len(s) { - w.Write(s[start:end]) - } -} - -func escLink(w io.Writer, text []byte) { - unesc := html.UnescapeString(string(text)) - escapeHTML(w, []byte(unesc)) + return ast.WalkContinue, nil }