From 87b394abecc54b136487d0086c3e62dac6a2acf2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 20 Mar 2020 14:32:29 +0200 Subject: Support formatting in rainbows Fixes #119 --- ui/commands.go | 31 ++++++++++------ ui/rainbow.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ui/room-view.go | 6 +++- 3 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 ui/rainbow.go (limited to 'ui') 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, "%[2]c", 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 . + +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, "%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)) +} 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() -- cgit v1.2.3