Merge branch 'deep-message-rendering'

This commit is contained in:
Tulir Asokan 2019-04-07 23:21:11 +03:00
commit e5db799fa3
15 changed files with 718 additions and 209 deletions

1
go.mod
View File

@ -3,6 +3,7 @@ module maunium.net/go/gomuks
go 1.12 go 1.12
require ( require (
github.com/alecthomas/chroma v0.6.3
github.com/disintegration/imaging v1.6.0 github.com/disintegration/imaging v1.6.0
github.com/kyokomi/emoji v2.1.0+incompatible github.com/kyokomi/emoji v2.1.0+incompatible
github.com/lithammer/fuzzysearch v1.0.2 github.com/lithammer/fuzzysearch v1.0.2

17
go.sum
View File

@ -1,5 +1,16 @@
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.6.3 h1:8H1D0yddf0mvgvO4JDBKnzLd9ERmzzAijBxnZXGV/FA=
github.com/alecthomas/chroma v0.6.3/go.mod h1:quT2EpvJNqkuPi6DmBHB+E33FXBgBBPzyH5++Dn1LPc=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.1.15/go.mod h1:0m2VYms8rH0qbCqVB2gvGHk74bqLIq0HXjCs5bNbNQU=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA= github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/kyokomi/emoji v2.1.0+incompatible h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o= github.com/kyokomi/emoji v2.1.0+incompatible h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o=
@ -8,16 +19,21 @@ github.com/lithammer/fuzzysearch v1.0.2 h1:AjCE2iwc5y+8K+h2nXVc0Pmrpjvu+JVqMgiZ0
github.com/lithammer/fuzzysearch v1.0.2/go.mod h1:bvAJyokfCQ7Vknrd4Kgc+izmMrPj5CiBAu2t6rK1Kak= github.com/lithammer/fuzzysearch v1.0.2/go.mod h1:bvAJyokfCQ7Vknrd4Kgc+izmMrPj5CiBAu2t6rK1Kak=
github.com/lucasb-eyer/go-colorful v1.0.1 h1:nKJRBvZWPzvkwB4sY8A3U4zgqLf2Y9c02yzPsbXu/5c= github.com/lucasb-eyer/go-colorful v1.0.1 h1:nKJRBvZWPzvkwB4sY8A3U4zgqLf2Y9c02yzPsbXu/5c=
github.com/lucasb-eyer/go-colorful v1.0.1/go.mod h1:tLy1nWSoU0DGtxQyNRrUmb6PUiB7usbds6gd97XTXwA= github.com/lucasb-eyer/go-colorful v1.0.1/go.mod h1:tLy1nWSoU0DGtxQyNRrUmb6PUiB7usbds6gd97XTXwA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs= github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs=
github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE= github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d h1:Lhqt2eo+rgM8aswvM7nTtAMVm8ARPWzkE9n6eZDOccY= github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d h1:Lhqt2eo+rgM8aswvM7nTtAMVm8ARPWzkE9n6eZDOccY=
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY= github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
@ -32,6 +48,7 @@ golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190326090315-15845e8f865b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190326090315-15845e8f865b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -76,12 +76,12 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
Gomuks: parent.gmx, Gomuks: parent.gmx,
}, },
aliases: map[string]*Alias{ aliases: map[string]*Alias{
"part": {"leave"}, "part": {"leave"},
"send": {"sendevent"}, "send": {"sendevent"},
"msend": {"msendevent"}, "msend": {"msendevent"},
"state": {"setstate"}, "state": {"setstate"},
"mstate":{"msetstate"}, "mstate": {"msetstate"},
"rb": {"rainbow"}, "rb": {"rainbow"},
}, },
commands: map[string]CommandHandler{ commands: map[string]CommandHandler{
"unknown-command": cmdUnknownCommand, "unknown-command": cmdUnknownCommand,
@ -102,6 +102,7 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
"msetstate": cmdMSetState, "msetstate": cmdMSetState,
"rainbow": cmdRainbow, "rainbow": cmdRainbow,
"invite": cmdInvite, "invite": cmdInvite,
"hprof": cmdHeapProfile,
}, },
} }
} }

View File

@ -19,6 +19,9 @@ package ui
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"runtime"
"runtime/pprof"
"strings" "strings"
"unicode" "unicode"
@ -69,6 +72,18 @@ var rainbow = GradientTable{
{colorful.LinearRgb(1, 0, 0.5), 1}, {colorful.LinearRgb(1, 0, 0.5), 1},
} }
func cmdHeapProfile(cmd *Command) {
runtime.GC()
memProfile, err := os.Create("gomuks.prof")
if err != nil {
debug.Print(err)
}
defer memProfile.Close()
if err := pprof.WriteHeapProfile(memProfile); err != nil {
debug.Print(err)
}
}
// TODO this command definitely belongs in a plugin once we have a plugin system. // TODO this command definitely belongs in a plugin once we have a plugin system.
func cmdRainbow(cmd *Command) { func cmdRainbow(cmd *Command) {
text := strings.Join(cmd.Args, " ") text := strings.Join(cmd.Args, " ")

View File

@ -75,6 +75,7 @@ func NewMessageView(parent *RoomView) *MessageView {
textBuffer: make([]tstring.TString, 0), textBuffer: make([]tstring.TString, 0),
metaBuffer: make([]ifc.MessageMeta, 0), metaBuffer: make([]ifc.MessageMeta, 0),
width: 80,
widestSender: 5, widestSender: 5,
prevWidth: -1, prevWidth: -1,
prevHeight: -1, prevHeight: -1,
@ -159,8 +160,8 @@ func (view *MessageView) appendBuffer(message messages.UIMessage) {
} }
} }
view.textBuffer = append(view.textBuffer, message.Buffer()...) for i := 0; i < message.Height(); i++ {
for range message.Buffer() { view.textBuffer = append(view.textBuffer, nil)
view.metaBuffer = append(view.metaBuffer, message) view.metaBuffer = append(view.metaBuffer, message)
} }
view.prevMsgCount++ view.prevMsgCount++
@ -200,10 +201,15 @@ func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages
end++ end++
} }
view.textBuffer = append(append(view.textBuffer[0:start], new.Buffer()...), view.textBuffer[end:]...) if new.Height() == 0 {
if len(new.Buffer()) != end-start { new.CalculateBuffer(view.prevPrefs, view.prevWidth)
}
textBuf := make([]tstring.TString, new.Height())
view.textBuffer = append(append(view.textBuffer[0:start], textBuf...), view.textBuffer[end:]...)
if new.Height() != end-start {
metaBuffer := view.metaBuffer[0:start] metaBuffer := view.metaBuffer[0:start]
for range new.Buffer() { for i := 0; i < new.Height(); i++ {
metaBuffer = append(metaBuffer, new) metaBuffer = append(metaBuffer, new)
} }
view.metaBuffer = append(metaBuffer, view.metaBuffer[end:]...) view.metaBuffer = append(metaBuffer, view.metaBuffer[end:]...)
@ -497,15 +503,30 @@ func (view *MessageView) Draw(screen mauview.Screen) {
if len(meta.FormatTime()) > 0 { if len(meta.FormatTime()) > 0 {
widget.WriteLineSimpleColor(screen, meta.FormatTime(), 0, line, meta.TimestampColor()) widget.WriteLineSimpleColor(screen, meta.FormatTime(), 0, line, meta.TimestampColor())
} }
if !bareMode && (prevMeta == nil || meta.Sender() != prevMeta.Sender()) { // TODO hiding senders might not be that nice after all, maybe an option? (disabled for now)
widget.WriteLineColor( //if !bareMode && (prevMeta == nil || meta.Sender() != prevMeta.Sender()) {
screen, mauview.AlignRight, meta.Sender(), widget.WriteLineColor(
usernameX, line, view.widestSender, screen, mauview.AlignRight, meta.Sender(),
meta.SenderColor()) usernameX, line, view.widestSender,
} meta.SenderColor())
//}
prevMeta = meta prevMeta = meta
} }
text.Draw(screen, messageX, line) message, ok := meta.(messages.UIMessage)
if ok {
for i := index - 1; i >= 0 && view.metaBuffer[i] == meta; i-- {
line--
}
message.Draw(mauview.NewProxyScreen(screen, messageX, line, view.width-messageX, message.Height()))
if !bareMode {
for i := line; i < line+message.Height(); i++ {
screen.SetContent(separatorX, i, borderChar, nil, borderStyle)
}
}
line += message.Height() - 1
} else {
text.Draw(screen, messageX, line)
}
} }
} }

View File

@ -21,9 +21,9 @@ import (
"time" "time"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
@ -34,33 +34,30 @@ func init() {
} }
type BaseMessage struct { type BaseMessage struct {
MsgID string MsgID string
MsgType mautrix.MessageType MsgType mautrix.MessageType
MsgSenderID string MsgSenderID string
MsgSender string MsgSender string
MsgSenderColor tcell.Color MsgSenderColor tcell.Color
MsgTimestamp time.Time MsgTimestamp time.Time
MsgState ifc.MessageState MsgState ifc.MessageState
MsgIsHighlight bool MsgIsHighlight bool
MsgIsService bool MsgIsService bool
buffer []tstring.TString buffer []tstring.TString
plainBuffer []tstring.TString plainBuffer []tstring.TString
prevBufferWidth int
prevPrefs config.UserPreferences
} }
func newBaseMessage(id, sender, displayname string, msgtype mautrix.MessageType, timestamp time.Time) BaseMessage { func newBaseMessage(id, sender, displayname string, msgtype mautrix.MessageType, timestamp time.Time) BaseMessage {
return BaseMessage{ return BaseMessage{
MsgSenderID: sender, MsgSenderID: sender,
MsgSender: displayname, MsgSender: displayname,
MsgTimestamp: timestamp, MsgTimestamp: timestamp,
MsgSenderColor: widget.GetHashColor(sender), MsgSenderColor: widget.GetHashColor(sender),
MsgType: msgtype, MsgType: msgtype,
MsgID: id, MsgID: id,
prevBufferWidth: 0, MsgState: ifc.MessageStateDefault,
MsgState: ifc.MessageStateDefault, MsgIsHighlight: false,
MsgIsHighlight: false, MsgIsService: false,
MsgIsService: false,
} }
} }
@ -227,3 +224,9 @@ func (msg *BaseMessage) IsService() bool {
func (msg *BaseMessage) SetIsService(isService bool) { func (msg *BaseMessage) SetIsService(isService bool) {
msg.MsgIsService = isService msg.MsgIsService = isService
} }
func (msg *BaseMessage) Draw(screen mauview.Screen) {
for y, line := range msg.buffer {
line.Draw(screen, 0, y)
}
}

View File

@ -58,8 +58,3 @@ func (msg *ExpandedTextMessage) PlainText() string {
func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int) { func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int) {
msg.calculateBufferWithText(prefs, msg.MsgText, width) msg.calculateBufferWithText(prefs, msg.MsgText, width)
} }
// RecalculateBuffer calculates the buffer again with the previously provided width.
func (msg *ExpandedTextMessage) RecalculateBuffer() {
msg.CalculateBuffer(msg.prevPrefs, msg.prevBufferWidth)
}

411
ui/messages/htmlmessage.go Normal file
View File

@ -0,0 +1,411 @@
// 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 <https://www.gnu.org/licenses/>.
package messages
import (
"fmt"
"math"
"strings"
"time"
"github.com/mattn/go-runewidth"
"maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/ui/widget"
)
type HTMLMessage struct {
BaseMessage
Root HTMLEntity
}
func NewHTMLMessage(id, sender, displayname string, msgtype mautrix.MessageType, root HTMLEntity, timestamp time.Time) UIMessage {
return &HTMLMessage{
BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp),
Root: root,
}
}
func (hw *HTMLMessage) Draw(screen mauview.Screen) {
hw.Root.Draw(screen)
}
func (hw *HTMLMessage) OnKeyEvent(event mauview.KeyEvent) bool {
return false
}
func (hw *HTMLMessage) OnMouseEvent(event mauview.MouseEvent) bool {
return false
}
func (hw *HTMLMessage) OnPasteEvent(event mauview.PasteEvent) bool {
return false
}
func (hw *HTMLMessage) CalculateBuffer(preferences config.UserPreferences, width int) {
if width <= 0 {
panic("Negative width in CalculateBuffer")
}
// TODO account for bare messages in initial startX
startX := 0
hw.Root.calculateBuffer(width, startX, preferences.BareMessageView)
}
func (hw *HTMLMessage) Height() int {
return hw.Root.Height()
}
func (hw *HTMLMessage) PlainText() string {
return hw.Root.PlainText()
}
func (hw *HTMLMessage) NotificationContent() string {
return hw.Root.PlainText()
}
type AdjustStyleFunc func(tcell.Style) tcell.Style
type HTMLEntity interface {
AdjustStyle(AdjustStyleFunc) HTMLEntity
Draw(screen mauview.Screen)
IsBlock() bool
GetTag() string
PlainText() string
String() string
Height() int
calculateBuffer(width, startX int, bare bool) int
getStartX() int
}
type BlockquoteEntity struct {
*BaseHTMLEntity
}
func NewBlockquoteEntity(children []HTMLEntity) *BlockquoteEntity {
return &BlockquoteEntity{&BaseHTMLEntity{
Tag: "blockquote",
Children: children,
Block: true,
Indent: 2,
}}
}
func (be *BlockquoteEntity) Draw(screen mauview.Screen) {
be.BaseHTMLEntity.Draw(screen)
for y := 0; y < be.height; y++ {
screen.SetContent(0, y, '>', nil, be.Style)
}
}
func (be *BlockquoteEntity) String() string {
return fmt.Sprintf("&BlockquoteEntity{%s},\n", be.BaseHTMLEntity)
}
type ListEntity struct {
*BaseHTMLEntity
Ordered bool
Start int
}
func digits(num int) int {
if num <= 0 {
return 0
}
return int(math.Floor(math.Log10(float64(num))) + 1)
}
func NewListEntity(ordered bool, start int, children []HTMLEntity) *ListEntity {
entity := &ListEntity{
BaseHTMLEntity: &BaseHTMLEntity{
Tag: "ul",
Children: children,
Block: true,
Indent: 2,
},
Ordered: ordered,
Start: start,
}
if ordered {
entity.Tag = "ol"
entity.Indent += digits(start + len(children) - 1)
}
return entity
}
func (le *ListEntity) Draw(screen mauview.Screen) {
width, _ := screen.Size()
proxyScreen := &mauview.ProxyScreen{Parent: screen, OffsetX: le.Indent, Width: width - le.Indent}
for i, entity := range le.Children {
proxyScreen.Height = entity.Height()
if le.Ordered {
number := le.Start + i
line := fmt.Sprintf("%d. %s", number, strings.Repeat(" ", le.Indent-2-digits(number)))
widget.WriteLine(screen, mauview.AlignLeft, line, 0, proxyScreen.OffsetY, le.Indent, le.Style)
} else {
screen.SetContent(0, proxyScreen.OffsetY, '●', nil, le.Style)
}
entity.Draw(proxyScreen)
proxyScreen.OffsetY += entity.Height()
}
}
func (le *ListEntity) String() string {
return fmt.Sprintf("&ListEntity{Ordered=%t, Start=%d, Base=%s},\n", le.Ordered, le.Start, le.BaseHTMLEntity)
}
type BreakEntity struct {
*BaseHTMLEntity
}
func NewBreakEntity() *BreakEntity {
return &BreakEntity{&BaseHTMLEntity{
Tag: "br",
Block: true,
}}
}
type BaseHTMLEntity struct {
// Permanent variables
Tag string
Text string
Style tcell.Style
Children []HTMLEntity
Block bool
Indent int
DefaultHeight int
// Non-permanent variables (calculated buffer data)
buffer []string
prevWidth int
startX int
height int
}
func NewHTMLTextEntity(text string) *BaseHTMLEntity {
return &BaseHTMLEntity{
Tag: "text",
Text: text,
}
}
func NewHTMLEntity(tag string, children []HTMLEntity, block bool) *BaseHTMLEntity {
return &BaseHTMLEntity{
Tag: tag,
Children: children,
Block: block,
}
}
func (he *BaseHTMLEntity) AdjustStyle(fn AdjustStyleFunc) HTMLEntity {
for _, child := range he.Children {
child.AdjustStyle(fn)
}
he.Style = fn(he.Style)
return he
}
func (he *BaseHTMLEntity) IsBlock() bool {
return he.Block
}
func (he *BaseHTMLEntity) GetTag() string {
return he.Tag
}
func (he *BaseHTMLEntity) Height() int {
return he.height
}
func (he *BaseHTMLEntity) getStartX() int {
return he.startX
}
func (he *BaseHTMLEntity) String() string {
var buf strings.Builder
buf.WriteString("&BaseHTMLEntity{\n")
_, _ = fmt.Fprintf(&buf, ` Tag="%s", Style=%d, Block=%t, Indent=%d, startX=%d, height=%d,`,
he.Tag, he.Style, he.Block, he.Indent, he.startX, he.height)
buf.WriteRune('\n')
_, _ = fmt.Fprintf(&buf, ` Buffer=["%s"]`, strings.Join(he.buffer, "\", \""))
if len(he.Text) > 0 {
buf.WriteString(",\n")
_, _ = fmt.Fprintf(&buf, ` Text="%s"`, he.Text)
}
if len(he.Children) > 0 {
buf.WriteString(",\n")
buf.WriteString(" Children={")
for _, child := range he.Children {
buf.WriteString("\n ")
buf.WriteString(strings.Join(strings.Split(strings.TrimRight(child.String(), "\n"), "\n"), "\n "))
}
buf.WriteString("\n },")
}
buf.WriteString("\n},\n")
return buf.String()
}
func (he *BaseHTMLEntity) PlainText() string {
if len(he.Children) == 0 {
return he.Text
}
var buf strings.Builder
buf.WriteString(he.Text)
newlined := false
for _, child := range he.Children {
if child.IsBlock() && !newlined {
buf.WriteRune('\n')
}
newlined = false
buf.WriteString(child.PlainText())
if child.IsBlock() {
buf.WriteRune('\n')
newlined = true
}
}
return buf.String()
}
func (he *BaseHTMLEntity) Draw(screen mauview.Screen) {
width, _ := screen.Size()
if len(he.buffer) > 0 {
x := he.startX
for y, line := range he.buffer {
widget.WriteLine(screen, mauview.AlignLeft, line, x, y, width, he.Style)
x = 0
}
}
if len(he.Children) > 0 {
prevBreak := false
proxyScreen := &mauview.ProxyScreen{Parent: screen, OffsetX: he.Indent, Width: width - he.Indent}
for i, entity := range he.Children {
if i != 0 && entity.getStartX() == 0 {
proxyScreen.OffsetY++
}
proxyScreen.Height = entity.Height()
entity.Draw(proxyScreen)
proxyScreen.OffsetY += entity.Height() - 1
_, isBreak := entity.(*BreakEntity)
if prevBreak && isBreak {
proxyScreen.OffsetY++
}
prevBreak = isBreak
}
}
}
func (he *BaseHTMLEntity) calculateBuffer(width, startX int, bare bool) int {
he.startX = startX
if he.Block {
he.startX = 0
}
he.height = 0
if len(he.Children) > 0 {
childStartX := he.startX
prevBreak := false
for _, entity := range he.Children {
if entity.IsBlock() || childStartX == 0 || he.height == 0 {
he.height++
}
childStartX = entity.calculateBuffer(width-he.Indent, childStartX, bare)
he.height += entity.Height() - 1
_, isBreak := entity.(*BreakEntity)
if prevBreak && isBreak {
he.height++
}
prevBreak = isBreak
}
if len(he.Text) == 0 && !he.Block {
return childStartX
}
}
if len(he.Text) > 0 {
he.prevWidth = width
if he.buffer == nil {
he.buffer = []string{}
}
bufPtr := 0
text := he.Text
textStartX := he.startX
for {
// TODO add option no wrap and character wrap options
extract := runewidth.Truncate(text, width-textStartX, "")
extract, wordWrapped := trim(extract, text, bare)
if !wordWrapped && textStartX > 0 {
if bufPtr < len(he.buffer) {
he.buffer[bufPtr] = ""
} else {
he.buffer = append(he.buffer, "")
}
bufPtr++
textStartX = 0
continue
}
if bufPtr < len(he.buffer) {
he.buffer[bufPtr] = extract
} else {
he.buffer = append(he.buffer, extract)
}
bufPtr++
text = text[len(extract):]
if len(text) == 0 {
he.buffer = he.buffer[:bufPtr]
he.height += len(he.buffer)
// This entity is over, return the startX for the next entity
if he.Block {
// ...except if it's a block entity
return 0
}
return textStartX + runewidth.StringWidth(extract)
}
textStartX = 0
}
}
if len(he.Text) == 0 && len(he.Children) == 0 {
he.height = he.DefaultHeight
}
return he.startX
}
func trim(extract, full string, bare bool) (string, bool) {
if len(extract) == len(full) {
return extract, true
}
if spaces := spacePattern.FindStringIndex(full[len(extract):]); spaces != nil && spaces[0] == 0 {
extract = full[:len(extract)+spaces[1]]
}
regex := boundaryPattern
if bare {
regex = bareBoundaryPattern
}
matches := regex.FindAllStringIndex(extract, -1)
if len(matches) > 0 {
if match := matches[len(matches)-1]; len(match) >= 2 {
if until := match[1]; until < len(extract) {
extract = extract[:until]
return extract, true
}
}
}
return extract, len(extract) > 0 && extract[len(extract)-1] == ' '
}

View File

@ -112,11 +112,4 @@ func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int
} }
msg.buffer = image.Render() msg.buffer = image.Render()
msg.prevBufferWidth = width
msg.prevPrefs = prefs
}
// RecalculateBuffer calculates the buffer again with the previously provided width.
func (msg *ImageMessage) RecalculateBuffer() {
msg.CalculateBuffer(msg.prevPrefs, msg.prevBufferWidth)
} }

View File

@ -19,7 +19,7 @@ package messages
import ( import (
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/mauview"
) )
// UIMessage is a wrapper for the content and metadata of a Matrix message intended to be displayed. // UIMessage is a wrapper for the content and metadata of a Matrix message intended to be displayed.
@ -27,8 +27,7 @@ type UIMessage interface {
ifc.Message ifc.Message
CalculateBuffer(preferences config.UserPreferences, width int) CalculateBuffer(preferences config.UserPreferences, width int)
RecalculateBuffer() Draw(screen mauview.Screen)
Buffer() []tstring.TString
Height() int Height() int
PlainText() string PlainText() string

View File

@ -17,12 +17,13 @@
package parser package parser
import ( import (
"fmt"
"math"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
"golang.org/x/net/html" "golang.org/x/net/html"
@ -30,7 +31,7 @@ import (
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/messages"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
) )
@ -40,11 +41,6 @@ type htmlParser struct {
room *rooms.Room room *rooms.Room
} }
type taggedTString struct {
tstring.TString
tag string
}
func AdjustStyleBold(style tcell.Style) tcell.Style { func AdjustStyleBold(style tcell.Style) tcell.Style {
return style.Bold(true) return style.Bold(true)
} }
@ -82,65 +78,53 @@ func (parser *htmlParser) getAttribute(node *html.Node, attribute string) string
return "" return ""
} }
func digits(num int) int { func (parser *htmlParser) listToEntity(node *html.Node, stripLinebreak bool) messages.HTMLEntity {
if num <= 0 { children := parser.nodeToEntities(node.FirstChild, stripLinebreak)
return 0
}
return int(math.Floor(math.Log10(float64(num))) + 1)
}
func (parser *htmlParser) listToTString(node *html.Node, stripLinebreak bool) tstring.TString {
ordered := node.Data == "ol" ordered := node.Data == "ol"
taggedChildren := parser.nodeToTaggedTStrings(node.FirstChild, stripLinebreak) start := 1
counter := 1
indentLength := 0
if ordered { if ordered {
start := parser.getAttribute(node, "start") if startRaw := parser.getAttribute(node, "start"); len(startRaw) > 0 {
if len(start) > 0 { var err error
counter, _ = strconv.Atoi(start) start, err = strconv.Atoi(startRaw)
if err != nil {
start = 1
}
} }
longestIndex := (counter - 1) + len(taggedChildren)
indentLength = digits(longestIndex)
} }
indent := strings.Repeat(" ", indentLength+2) listItems := children[:0]
var children []tstring.TString for _, child := range children {
for _, child := range taggedChildren { if child.GetTag() == "li" {
if child.tag != "li" { listItems = append(listItems, child)
continue
} }
var prefix string
if ordered {
indexPadding := indentLength - digits(counter)
prefix = fmt.Sprintf("%d. %s", counter, strings.Repeat(" ", indexPadding))
} else {
prefix = "● "
}
str := child.TString.Prepend(prefix)
counter++
parts := str.Split('\n')
for i, part := range parts[1:] {
parts[i+1] = part.Prepend(indent)
}
str = tstring.Join(parts, "\n")
children = append(children, str)
} }
return tstring.Join(children, "\n") return messages.NewListEntity(ordered, start, listItems)
} }
func (parser *htmlParser) basicFormatToTString(node *html.Node, stripLinebreak bool) tstring.TString { func (parser *htmlParser) basicFormatToEntity(node *html.Node, stripLinebreak bool) messages.HTMLEntity {
str := parser.nodeToTagAwareTString(node.FirstChild, stripLinebreak) entity := &messages.BaseHTMLEntity{
Tag: node.Data,
Children: parser.nodeToEntities(node.FirstChild, stripLinebreak),
}
switch node.Data { switch node.Data {
case "b", "strong": case "b", "strong":
str.AdjustStyleFull(AdjustStyleBold) entity.AdjustStyle(AdjustStyleBold)
case "i", "em": case "i", "em":
str.AdjustStyleFull(AdjustStyleItalic) entity.AdjustStyle(AdjustStyleItalic)
case "s", "del": case "s", "del":
str.AdjustStyleFull(AdjustStyleStrikethrough) entity.AdjustStyle(AdjustStyleStrikethrough)
case "u", "ins": case "u", "ins":
str.AdjustStyleFull(AdjustStyleUnderline) entity.AdjustStyle(AdjustStyleUnderline)
case "font":
fgColor, ok := parser.parseColor(node, "data-mx-color", "color")
if ok {
entity.AdjustStyle(AdjustStyleTextColor(fgColor))
}
bgColor, ok := parser.parseColor(node, "data-mx-bg-color", "background-color")
if ok {
entity.AdjustStyle(AdjustStyleBackgroundColor(bgColor))
}
} }
return str return entity
} }
func (parser *htmlParser) parseColor(node *html.Node, mainName, altName string) (color tcell.Color, ok bool) { func (parser *htmlParser) parseColor(node *html.Node, mainName, altName string) (color tcell.Color, ok bool) {
@ -165,103 +149,200 @@ func (parser *htmlParser) parseColor(node *html.Node, mainName, altName string)
return tcell.NewRGBColor(int32(r), int32(g), int32(b)), true return tcell.NewRGBColor(int32(r), int32(g), int32(b)), true
} }
func (parser *htmlParser) fontToTString(node *html.Node, stripLinebreak bool) tstring.TString { func (parser *htmlParser) headerToEntity(node *html.Node, stripLinebreak bool) messages.HTMLEntity {
str := parser.nodeToTagAwareTString(node.FirstChild, stripLinebreak)
fgColor, ok := parser.parseColor(node, "data-mx-color", "color")
if ok {
str.AdjustStyleFull(AdjustStyleTextColor(fgColor))
}
bgColor, ok := parser.parseColor(node, "data-mx-bg-color", "background-color")
if ok {
str.AdjustStyleFull(AdjustStyleBackgroundColor(bgColor))
}
return str
}
func (parser *htmlParser) headerToTString(node *html.Node, stripLinebreak bool) tstring.TString {
children := parser.nodeToTStrings(node.FirstChild, stripLinebreak)
length := int(node.Data[1] - '0') length := int(node.Data[1] - '0')
prefix := strings.Repeat("#", length) + " " prefix := strings.Repeat("#", length) + " "
return tstring.Join(children, "").Prepend(prefix) return (&messages.BaseHTMLEntity{
Tag: node.Data,
Text: prefix,
Children: parser.nodeToEntities(node.FirstChild, stripLinebreak),
}).AdjustStyle(AdjustStyleBold)
} }
func (parser *htmlParser) blockquoteToTString(node *html.Node, stripLinebreak bool) tstring.TString { func (parser *htmlParser) blockquoteToEntity(node *html.Node, stripLinebreak bool) messages.HTMLEntity {
str := parser.nodeToTagAwareTString(node.FirstChild, stripLinebreak) return messages.NewBlockquoteEntity(parser.nodeToEntities(node.FirstChild, stripLinebreak))
childrenArr := str.TrimSpace().Split('\n') }
for index, child := range childrenArr {
childrenArr[index] = child.Prepend("> ") func (parser *htmlParser) linkToEntity(node *html.Node, stripLinebreak bool) messages.HTMLEntity {
entity := &messages.BaseHTMLEntity{
Tag: "a",
Children: parser.nodeToEntities(node.FirstChild, stripLinebreak),
} }
return tstring.Join(childrenArr, "\n")
}
func (parser *htmlParser) linkToTString(node *html.Node, stripLinebreak bool) tstring.TString {
str := parser.nodeToTagAwareTString(node.FirstChild, stripLinebreak)
href := parser.getAttribute(node, "href") href := parser.getAttribute(node, "href")
if len(href) == 0 { if len(href) == 0 {
return str return entity
} }
match := matrixToURL.FindStringSubmatch(href) match := matrixToURL.FindStringSubmatch(href)
if len(match) == 2 { if len(match) == 2 {
entity.Children = nil
pillTarget := match[1] pillTarget := match[1]
entity.Text = pillTarget
if pillTarget[0] == '@' { if pillTarget[0] == '@' {
if member := parser.room.GetMember(pillTarget); member != nil { if member := parser.room.GetMember(pillTarget); member != nil {
return tstring.NewColorTString(member.Displayname, widget.GetHashColor(pillTarget)) entity.Text = member.Displayname
entity.Style = entity.Style.Foreground(widget.GetHashColor(pillTarget))
} }
} }
return tstring.NewTString(pillTarget)
} }
return str.Append(fmt.Sprintf(" (%s)", href)) // TODO add click action and underline on hover for links
return entity
} }
func (parser *htmlParser) tagToTString(node *html.Node, stripLinebreak bool) tstring.TString { func (parser *htmlParser) imageToEntity(node *html.Node) messages.HTMLEntity {
alt := parser.getAttribute(node, "alt")
if len(alt) == 0 {
alt = parser.getAttribute(node, "title")
if len(alt) == 0 {
alt = "[inline image]"
}
}
entity := &messages.BaseHTMLEntity{
Tag: "img",
Text: alt,
}
// TODO add click action and underline on hover for inline images
return entity
}
func colourToColor(colour chroma.Colour) tcell.Color {
if !colour.IsSet() {
return tcell.ColorDefault
}
return tcell.NewRGBColor(int32(colour.Red()), int32(colour.Green()), int32(colour.Blue()))
}
func styleEntryToStyle(se chroma.StyleEntry) tcell.Style {
return tcell.StyleDefault.
Bold(se.Bold == chroma.Yes).
Italic(se.Italic == chroma.Yes).
Underline(se.Underline == chroma.Yes).
Foreground(colourToColor(se.Colour)).
Background(colourToColor(se.Background))
}
func (parser *htmlParser) syntaxHighlight(text, language string) messages.HTMLEntity {
lexer := lexers.Get(language)
if lexer == nil {
return nil
}
iter, err := lexer.Tokenise(nil, text)
if err != nil {
return nil
}
style := styles.SolarizedDark
tokens := iter.Tokens()
children := make([]messages.HTMLEntity, len(tokens))
for i, token := range tokens {
if token.Value == "\n" {
children[i] = &messages.BaseHTMLEntity{Block: true, Tag: "br"}
} else {
children[i] = &messages.BaseHTMLEntity{
Tag: token.Type.String(),
Text: token.Value,
Style: styleEntryToStyle(style.Get(token.Type)),
DefaultHeight: 1,
}
}
}
return &messages.BaseHTMLEntity{
Tag: "pre",
Block: true,
Children: children,
}
}
func (parser *htmlParser) codeblockToEntity(node *html.Node) messages.HTMLEntity {
entity := &messages.BaseHTMLEntity{
Tag: "pre",
Block: true,
}
// TODO allow disabling syntax highlighting
if node.FirstChild.Type == html.ElementNode && node.FirstChild.Data == "code" {
text := (&messages.BaseHTMLEntity{
Children: parser.nodeToEntities(node.FirstChild.FirstChild, false),
}).PlainText()
attr := parser.getAttribute(node.FirstChild, "class")
var lang string
for _, class := range strings.Split(attr, " ") {
if strings.HasPrefix(class, "language-") {
lang = class[len("language-"):]
break
}
}
if len(lang) != 0 {
if parsed := parser.syntaxHighlight(text, lang); parsed != nil {
return parsed
}
}
}
entity.Children = parser.nodeToEntities(node.FirstChild, false)
return entity
}
func (parser *htmlParser) tagNodeToEntity(node *html.Node, stripLinebreak bool) messages.HTMLEntity {
switch node.Data { switch node.Data {
case "blockquote": case "blockquote":
return parser.blockquoteToTString(node, stripLinebreak) return parser.blockquoteToEntity(node, stripLinebreak)
case "ol", "ul": case "ol", "ul":
return parser.listToTString(node, stripLinebreak) return parser.listToEntity(node, stripLinebreak)
case "h1", "h2", "h3", "h4", "h5", "h6": case "h1", "h2", "h3", "h4", "h5", "h6":
return parser.headerToTString(node, stripLinebreak) return parser.headerToEntity(node, stripLinebreak)
case "br": case "br":
return tstring.NewTString("\n") return messages.NewBreakEntity()
case "b", "strong", "i", "em", "s", "del", "u", "ins": case "b", "strong", "i", "em", "s", "del", "u", "ins", "font":
return parser.basicFormatToTString(node, stripLinebreak) return parser.basicFormatToEntity(node, stripLinebreak)
case "font":
return parser.fontToTString(node, stripLinebreak)
case "a": case "a":
return parser.linkToTString(node, stripLinebreak) return parser.linkToEntity(node, stripLinebreak)
case "p": case "img":
return parser.nodeToTagAwareTString(node.FirstChild, stripLinebreak).Append("\n") return parser.imageToEntity(node)
case "pre": case "pre":
return parser.nodeToTString(node.FirstChild, false) return parser.codeblockToEntity(node)
default: default:
return parser.nodeToTagAwareTString(node.FirstChild, stripLinebreak) return &messages.BaseHTMLEntity{
Tag: node.Data,
Children: parser.nodeToEntities(node.FirstChild, stripLinebreak),
Block: parser.isBlockTag(node.Data),
}
} }
} }
func (parser *htmlParser) singleNodeToTString(node *html.Node, stripLinebreak bool) taggedTString { func (parser *htmlParser) singleNodeToEntity(node *html.Node, stripLinebreak bool) messages.HTMLEntity {
switch node.Type { switch node.Type {
case html.TextNode: case html.TextNode:
if stripLinebreak { if stripLinebreak {
node.Data = strings.Replace(node.Data, "\n", "", -1) node.Data = strings.Replace(node.Data, "\n", "", -1)
} }
return taggedTString{tstring.NewTString(node.Data), "text"} return &messages.BaseHTMLEntity{
Tag: "text",
Text: node.Data,
}
case html.ElementNode: case html.ElementNode:
return taggedTString{parser.tagToTString(node, stripLinebreak), node.Data} return parser.tagNodeToEntity(node, stripLinebreak)
case html.DocumentNode: case html.DocumentNode:
return taggedTString{parser.nodeToTagAwareTString(node.FirstChild, stripLinebreak), "html"} if node.FirstChild.Data == "html" && node.FirstChild.NextSibling == nil {
return parser.singleNodeToEntity(node.FirstChild, stripLinebreak)
}
return &messages.BaseHTMLEntity{
Tag: "html",
Children: parser.nodeToEntities(node.FirstChild, stripLinebreak),
Block: true,
}
default: default:
return taggedTString{tstring.NewBlankTString(), "unknown"} return nil
} }
} }
func (parser *htmlParser) nodeToTaggedTStrings(node *html.Node, stripLinebreak bool) (strs []taggedTString) { func (parser *htmlParser) nodeToEntities(node *html.Node, stripLinebreak bool) (entities []messages.HTMLEntity) {
for ; node != nil; node = node.NextSibling { for ; node != nil; node = node.NextSibling {
strs = append(strs, parser.singleNodeToTString(node, stripLinebreak)) if entity := parser.singleNodeToEntity(node, stripLinebreak); entity != nil {
entities = append(entities, entity)
}
} }
return return
} }
var BlockTags = []string{"p", "h1", "h2", "h3", "h4", "h5", "h6", "ol", "ul", "pre", "blockquote", "div", "hr", "table"} var BlockTags = []string{"p", "h1", "h2", "h3", "h4", "h5", "h6", "ol", "ul", "li", "pre", "blockquote", "div", "hr", "table"}
func (parser *htmlParser) isBlockTag(tag string) bool { func (parser *htmlParser) isBlockTag(tag string) bool {
for _, blockTag := range BlockTags { for _, blockTag := range BlockTags {
@ -272,51 +353,31 @@ func (parser *htmlParser) isBlockTag(tag string) bool {
return false return false
} }
func (parser *htmlParser) nodeToTagAwareTString(node *html.Node, stripLinebreak bool) tstring.TString { func (parser *htmlParser) Parse(htmlData string) messages.HTMLEntity {
strs := parser.nodeToTaggedTStrings(node, stripLinebreak)
output := tstring.NewBlankTString()
for _, str := range strs {
tstr := str.TString
if parser.isBlockTag(str.tag) {
tstr = tstr.Prepend("\n").Append("\n")
}
output = output.AppendTString(tstr)
}
return output.TrimSpace()
}
func (parser *htmlParser) nodeToTStrings(node *html.Node, stripLinebreak bool) (strs []tstring.TString) {
for ; node != nil; node = node.NextSibling {
strs = append(strs, parser.singleNodeToTString(node, stripLinebreak).TString)
}
return
}
func (parser *htmlParser) nodeToTString(node *html.Node, stripLinebreak bool) tstring.TString {
return tstring.Join(parser.nodeToTStrings(node, stripLinebreak), "")
}
func (parser *htmlParser) Parse(htmlData string) tstring.TString {
node, _ := html.Parse(strings.NewReader(htmlData)) node, _ := html.Parse(strings.NewReader(htmlData))
return parser.nodeToTagAwareTString(node, true) return parser.singleNodeToEntity(node, true)
} }
// ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage. // ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage.
func ParseHTMLMessage(room *rooms.Room, evt *mautrix.Event, senderDisplayname string) tstring.TString { func ParseHTMLMessage(room *rooms.Room, evt *mautrix.Event, senderDisplayname string) messages.HTMLEntity {
htmlData := evt.Content.FormattedBody htmlData := evt.Content.FormattedBody
htmlData = strings.Replace(htmlData, "\t", " ", -1) htmlData = strings.Replace(htmlData, "\t", " ", -1)
parser := htmlParser{room} parser := htmlParser{room}
str := parser.Parse(htmlData) root := parser.Parse(htmlData)
root.(*messages.BaseHTMLEntity).Block = false
if evt.Content.MsgType == mautrix.MsgEmote { if evt.Content.MsgType == mautrix.MsgEmote {
str = tstring.Join([]tstring.TString{ root = &messages.BaseHTMLEntity{
tstring.NewTString("* "), Tag: "emote",
tstring.NewColorTString(senderDisplayname, widget.GetHashColor(evt.Sender)), Children: []messages.HTMLEntity{
tstring.NewTString(" "), messages.NewHTMLTextEntity("* "),
str, messages.NewHTMLTextEntity("* ").AdjustStyle(AdjustStyleTextColor(widget.GetHashColor(evt.Sender))),
}, "") messages.NewHTMLTextEntity(" "),
root,
},
}
} }
return str return root
} }

View File

@ -113,7 +113,7 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Eve
replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body) replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body)
} }
evt.Content.FormattedBody = fmt.Sprintf( evt.Content.FormattedBody = fmt.Sprintf(
"In reply to <a href='https://matrix.to/#/%[1]s'>%[1]s</a><blockquote>%[2]s</blockquote><br/>%[3]s", "In reply to <a href='https://matrix.to/#/%[1]s'>%[1]s</a><blockquote>%[2]s</blockquote><br/><br/>%[3]s",
replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody) replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody)
} else { } else {
evt.Content.FormattedBody = fmt.Sprintf( evt.Content.FormattedBody = fmt.Sprintf(
@ -125,8 +125,7 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Eve
switch evt.Content.MsgType { switch evt.Content.MsgType {
case "m.text", "m.notice", "m.emote": case "m.text", "m.notice", "m.emote":
if evt.Content.Format == mautrix.FormatHTML { if evt.Content.Format == mautrix.FormatHTML {
text := ParseHTMLMessage(room, evt, displayname) return messages.NewHTMLMessage(evt.ID, evt.Sender, displayname, evt.Content.MsgType, ParseHTMLMessage(room, evt, displayname), ts)
return messages.NewExpandedTextMessage(evt.ID, evt.Sender, displayname, evt.Content.MsgType, text, ts)
} }
evt.Content.Body = strings.Replace(evt.Content.Body, "\t", " ", -1) evt.Content.Body = strings.Replace(evt.Content.Body, "\t", " ", -1)
return messages.NewTextMessage(evt.ID, evt.Sender, displayname, evt.Content.MsgType, evt.Content.Body, ts) return messages.NewTextMessage(evt.ID, evt.Sender, displayname, evt.Content.MsgType, evt.Content.Body, ts)

View File

@ -92,6 +92,4 @@ func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, te
str = str[len(extract):] str = str[len(extract):]
} }
} }
msg.prevBufferWidth = width
msg.prevPrefs = prefs
} }

View File

@ -90,8 +90,3 @@ func (msg *TextMessage) PlainText() string {
func (msg *TextMessage) CalculateBuffer(prefs config.UserPreferences, width int) { func (msg *TextMessage) CalculateBuffer(prefs config.UserPreferences, width int) {
msg.calculateBufferWithText(prefs, msg.getCache(), width) msg.calculateBufferWithText(prefs, msg.getCache(), width)
} }
// RecalculateBuffer calculates the buffer again with the previously provided width.
func (msg *TextMessage) RecalculateBuffer() {
msg.CalculateBuffer(msg.prevPrefs, msg.prevBufferWidth)
}

View File

@ -74,8 +74,8 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
view.username.SetText(ui.gmx.Config().UserID) view.username.SetText(ui.gmx.Config().UserID)
view.password.SetMaskCharacter('*') view.password.SetMaskCharacter('*')
view.quitButton.SetOnClick(ui.gmx.Stop).SetBackgroundColor(tcell.ColorBlue) view.quitButton.SetOnClick(ui.gmx.Stop).SetBackgroundColor(tcell.ColorDarkCyan)
view.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorBlue) view.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorDarkCyan)
view.SetColumns([]int{1, 10, 1, 9, 1, 9, 1, 10, 1}) view.SetColumns([]int{1, 10, 1, 9, 1, 9, 1, 10, 1})
view.SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}) view.SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1})