diff options
author | Tulir Asokan <tulir@maunium.net> | 2020-03-20 14:32:29 +0200 |
---|---|---|
committer | Tulir Asokan <tulir@maunium.net> | 2020-03-20 14:32:30 +0200 |
commit | 87b394abecc54b136487d0086c3e62dac6a2acf2 (patch) | |
tree | ae12fe3466f5f56f687b750b97721862a457a80a /ui | |
parent | 5a2c74514dd672343fbd012ab864518b9ee12957 (diff) |
Support formatting in rainbows
Fixes #119
Diffstat (limited to 'ui')
-rw-r--r-- | ui/commands.go | 31 | ||||
-rw-r--r-- | ui/rainbow.go | 109 | ||||
-rw-r--r-- | ui/room-view.go | 6 |
3 files changed, 134 insertions, 12 deletions
diff --git a/ui/commands.go b/ui/commands.go index 3d12eff..8cfe55d 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -22,6 +22,7 @@ import ( "io" "math" "os" + "regexp" "runtime" dbg "runtime/debug" "runtime/pprof" @@ -29,11 +30,12 @@ import ( "strconv" "strings" "time" - "unicode" "github.com/lucasb-eyer/go-colorful" + "github.com/russross/blackfriday/v2" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/format" "maunium.net/go/gomuks/debug" ) @@ -79,16 +81,23 @@ var rainbow = GradientTable{ // TODO this command definitely belongs in a plugin once we have a plugin system. func makeRainbow(cmd *Command, msgtype mautrix.MessageType) { text := strings.Join(cmd.Args, " ") - var html strings.Builder - for i, char := range text { - if unicode.IsSpace(char) { - html.WriteRune(char) - continue - } - color := rainbow.GetInterpolatedColorFor(float64(i) / float64(len(text))).Hex() - _, _ = fmt.Fprintf(&html, "<font data-mx-color=\"%[1]s\" color=\"%[1]s\">%[2]c</font>", color, char) - } - go cmd.Room.SendMessage(msgtype, html.String()) + + 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") + htmlBody = format.AntiParagraphRegex.ReplaceAllString(htmlBody, "$1") + text = format.HTMLToText(htmlBody) + + count := strings.Count(htmlBody, render.ColorID) + i := -1 + htmlBody = regexp.MustCompile(render.ColorID).ReplaceAllStringFunc(htmlBody, func(match string) string { + i++ + return rainbow.GetInterpolatedColorFor(float64(i) / float64(count)).Hex() + }) + + go cmd.Room.SendMessageHTML(msgtype, text, htmlBody) } func cmdRainbow(cmd *Command) { diff --git a/ui/rainbow.go b/ui/rainbow.go new file mode 100644 index 0000000..fca3cba --- /dev/null +++ b/ui/rainbow.go @@ -0,0 +1,109 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2020 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 +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +package ui + +import ( + "bytes" + "fmt" + "html" + "io" + "math/rand" + "unicode" + + "github.com/rivo/uniseg" + "github.com/russross/blackfriday/v2" +) + +type RainbowRenderer struct { + *blackfriday.HTMLRenderer + sr *blackfriday.SPRenderer + + ColorID string +} + +func Rand(n int) (str string) { + b := make([]byte, n) + rand.Read(b) + str = fmt.Sprintf("%x", b) + return +} + +func NewRainbowRenderer(html *blackfriday.HTMLRenderer) *RainbowRenderer { + return &RainbowRenderer{ + HTMLRenderer: html, + sr: blackfriday.NewSmartypantsRenderer(html.Flags), + ColorID: Rand(16), + } +} + +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()) + } else { + if node.Parent.Type == blackfriday.Link { + escLink(&buf, node.Literal) + } else { + escapeHTML(&buf, node.Literal) + } + } + 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]) + } + _, _ = fmt.Fprintf(&buf, "<font color=\"%s\">%s</font>", 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)) +} diff --git a/ui/room-view.go b/ui/room-view.go index 3c4be8f..ef19c9d 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -622,6 +622,10 @@ func (view *RoomView) SendReaction(eventID string, reaction string) { } func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) { + view.SendMessageHTML(msgtype, text, "") +} + +func (view *RoomView) SendMessageHTML(msgtype mautrix.MessageType, text, html string) { defer debug.Recover() debug.Print("Sending message", msgtype, text, "to", view.Room.ID) if !view.config.Preferences.DisableEmojis { @@ -639,7 +643,7 @@ func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) { Event: view.replying, } } - evt := view.parent.matrix.PrepareMarkdownMessage(view.Room.ID, msgtype, text, rel) + evt := view.parent.matrix.PrepareMarkdownMessage(view.Room.ID, msgtype, text, html, rel) msg := view.parseEvent(evt.SomewhatDangerousCopy()) view.content.AddMessage(msg, AppendMessage) view.ClearAllContext() |