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 --- go.mod | 4 +- go.sum | 4 ++ interface/matrix.go | 2 +- lib/bfhtml/doc.go | 2 - lib/bfhtml/html.go | 34 ---------------- matrix/matrix.go | 16 ++++++-- matrix/sync.go | 5 ++- ui/commands.go | 31 +++++++++------ ui/rainbow.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++ ui/room-view.go | 6 ++- 10 files changed, 159 insertions(+), 54 deletions(-) delete mode 100644 lib/bfhtml/doc.go delete mode 100644 lib/bfhtml/html.go create mode 100644 ui/rainbow.go diff --git a/go.mod b/go.mod index 8f7900f..dfeb7ad 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require ( github.com/mattn/go-runewidth v0.0.8 github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 + github.com/rivo/uniseg v0.1.0 + github.com/russross/blackfriday/v2 v2.0.1 github.com/sasha-s/go-deadlock v0.2.0 github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/stretchr/testify v1.5.1 @@ -19,7 +21,7 @@ require ( golang.org/x/net v0.0.0-20200301022130-244492dfa37a gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 gopkg.in/yaml.v2 v2.2.8 - maunium.net/go/mautrix v0.1.0-beta.1 + maunium.net/go/mautrix v0.1.0-beta.1.0.20200320123139-8ba1d97ed86b maunium.net/go/mauview v0.1.0-beta.1 maunium.net/go/tcell v0.1.0 ) diff --git a/go.sum b/go.sum index 0e71021..8076ac6 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,10 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= maunium.net/go/mautrix v0.1.0-beta.1 h1:o7EzSO3sMf7tpNvxanITcpMw3nL3SaJ/LDpBRPaZIt8= maunium.net/go/mautrix v0.1.0-beta.1/go.mod h1:YFMU9DBeXH7cqx7sJLg0DkVxwNPbih8QbpUTYf/IjMM= +maunium.net/go/mautrix v0.1.0-beta.1.0.20200320113420-8101f052c782 h1:gLsc8FRp9mIQHXjxFpdpISUCoJO8jt4SQyaFYUCLJ5U= +maunium.net/go/mautrix v0.1.0-beta.1.0.20200320113420-8101f052c782/go.mod h1:YFMU9DBeXH7cqx7sJLg0DkVxwNPbih8QbpUTYf/IjMM= +maunium.net/go/mautrix v0.1.0-beta.1.0.20200320123139-8ba1d97ed86b h1:sQ7w1dOTvTg76wLCUyb30+jEsVIVvI4Zw4IOPdwogIE= +maunium.net/go/mautrix v0.1.0-beta.1.0.20200320123139-8ba1d97ed86b/go.mod h1:YFMU9DBeXH7cqx7sJLg0DkVxwNPbih8QbpUTYf/IjMM= maunium.net/go/mauview v0.1.0-beta.1 h1:hRprD6NTi5Mw7i97DKmgs/TzFQeNpGPytPoswNlU/Ww= maunium.net/go/mauview v0.1.0-beta.1/go.mod h1:og9WbzmWe9SNYNyOFlCv8qa9zMcOvG2nzRJ5vYyud9U= maunium.net/go/tcell v0.1.0 h1:XzsEoGCvOw5nac+tlkSLzQcliLYTN4PrtA7ar2ptjSM= diff --git a/interface/matrix.go b/interface/matrix.go index 6814f07..d4351d7 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -40,7 +40,7 @@ type MatrixContainer interface { Logout() SendPreferencesToMatrix() - PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, message string, relation *Relation) *event.Event + PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text, html string, relation *Relation) *event.Event SendEvent(evt *event.Event) (string, error) Redact(roomID, eventID, reason string) error SendTyping(roomID string, typing bool) diff --git a/lib/bfhtml/doc.go b/lib/bfhtml/doc.go deleted file mode 100644 index 4881087..0000000 --- a/lib/bfhtml/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package bfhtml contains an extension to the Blackfriday HTML renderer to disable paragraph tags. -package bfhtml diff --git a/lib/bfhtml/html.go b/lib/bfhtml/html.go deleted file mode 100644 index cfffb96..0000000 --- a/lib/bfhtml/html.go +++ /dev/null @@ -1,34 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 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 bfhtml - -import ( - "io" - - bf "github.com/russross/blackfriday/v2" -) - -type HTMLRenderer struct { - *bf.HTMLRenderer -} - -func (r *HTMLRenderer) RenderNode(w io.Writer, node *bf.Node, entering bool) bf.WalkStatus { - if node.Type == bf.Paragraph { - return bf.GoToNext - } - return r.HTMLRenderer.RenderNode(w, node, entering) -} diff --git a/matrix/matrix.go b/matrix/matrix.go index ef5ef8a..226df6c 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -729,9 +729,19 @@ func (c *Container) MarkRead(roomID, eventID string) { _, _ = c.client.MakeRequest("POST", urlPath, struct{}{}, nil) } -func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text string, rel *ifc.Relation) *event.Event { - content := format.RenderMarkdown(text) - content.MsgType = msgtype +func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text, html string, rel *ifc.Relation) *event.Event { + var content mautrix.Content + if html != "" { + content = mautrix.Content{ + FormattedBody: html, + Format: mautrix.FormatHTML, + Body: text, + MsgType: msgtype, + } + } else { + content = format.RenderMarkdown(text) + content.MsgType = msgtype + } if rel != nil && rel.Type == mautrix.RelReplace { contentCopy := content diff --git a/matrix/sync.go b/matrix/sync.go index 81c0596..8ec22b5 100644 --- a/matrix/sync.go +++ b/matrix/sync.go @@ -153,6 +153,9 @@ func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err func (s *GomuksSyncer) processSyncEvents(room *rooms.Room, events []json.RawMessage, source EventSource) { for _, event := range events { + if source == EventSourcePresence { + debug.Print(string(event)) + } s.processSyncEvent(room, event, source) } } @@ -241,7 +244,7 @@ func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { "m.room.power_levels", "m.room.tombstone", }, - Limit: 50, +// Limit: 50, }, Ephemeral: mautrix.FilterPart{ Types: []string{"m.typing", "m.receipt"}, 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