From e7bf5bd59fc0a43172b6ab5b338e1d60bd4b3bbb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Apr 2018 00:34:25 +0300 Subject: [PATCH] Add basic HTML rendering (ref #16) --- matrix/rooms/room.go | 6 +- ui/messages/base.go | 4 +- ui/messages/htmlparser.go | 136 ++++++++++++++++++++++++++++++++++ ui/messages/parser.go | 10 ++- ui/messages/tstring/string.go | 33 ++++++++- ui/view-main.go | 4 +- 6 files changed, 179 insertions(+), 14 deletions(-) create mode 100644 ui/messages/htmlparser.go diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index 7dd2af4..2e12a28 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -94,11 +94,7 @@ func (room *Room) UpdateState(event *gomatrix.Event) { room.memberCache = nil room.firstMemberCache = "" fallthrough - case "m.room.name": - fallthrough - case "m.room.canonical_alias": - fallthrough - case "m.room.alias": + case "m.room.name", "m.room.canonical_alias", "m.room.alias": room.nameCache = "" case "m.room.topic": room.topicCache = "" diff --git a/ui/messages/base.go b/ui/messages/base.go index deb153f..aed7903 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -141,9 +141,7 @@ func (msg *BaseMessage) TextColor() tcell.Color { switch { case stateColor != tcell.ColorDefault: return stateColor - case msg.MsgIsService: - fallthrough - case msg.MsgType == "m.notice": + case msg.MsgIsService, msg.MsgType == "m.notice": return tcell.ColorGray case msg.MsgIsHighlight: return tcell.ColorYellow diff --git a/ui/messages/htmlparser.go b/ui/messages/htmlparser.go new file mode 100644 index 0000000..0475e7a --- /dev/null +++ b/ui/messages/htmlparser.go @@ -0,0 +1,136 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package messages + +import ( + "strings" + + "golang.org/x/net/html" + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/tcell" +) + +// TagArray is a reversed queue for remembering what HTML tags are open. +type TagArray []string + +// Pushb converts the given byte array into a string and calls Push(). +func (ta *TagArray) Pushb(tag []byte) { + ta.Push(string(tag)) +} + +// Popb converts the given byte array into a string and calls Pop(). +func (ta *TagArray) Popb(tag []byte) { + ta.Pop(string(tag)) +} + +// Hasb converts the given byte array into a string and calls Has(). +func (ta *TagArray) Hasb(tag []byte) { + ta.Has(string(tag)) +} + +// HasAfterb converts the given byte array into a string and calls HasAfter(). +func (ta *TagArray) HasAfterb(tag []byte, after int) { + ta.HasAfter(string(tag), after) +} + +// Push adds the given tag to the array. +func (ta *TagArray) Push(tag string) { + *ta = append(*ta, "") + copy((*ta)[1:], *ta) + (*ta)[0] = tag +} + +// Pop removes the given tag from the array. +func (ta *TagArray) Pop(tag string) { + if (*ta)[0] == tag { + // This is the default case and is lighter than append(), so we handle it separately. + *ta = (*ta)[1:] + } else if index := ta.Has(tag); index != -1 { + *ta = append((*ta)[:index], (*ta)[index+1:]...) + } +} + +// Has returns the first index where the given tag is, or -1 if it's not in the list. +func (ta *TagArray) Has(tag string) int { + return ta.HasAfter(tag, -1) +} + +// HasAfter returns the first index after the given index where the given tag is, +// or -1 if the given tag is not on the list after the given index. +func (ta *TagArray) HasAfter(tag string, after int) int { + for i := after + 1; i < len(*ta); i++ { + if (*ta)[i] == tag { + return i + } + } + return -1 +} + +// ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage. +func ParseHTMLMessage(evt *gomatrix.Event) tstring.TString { + //textData, _ := evt.Content["body"].(string) + htmlData, _ := evt.Content["formatted_body"].(string) + + z := html.NewTokenizer(strings.NewReader(htmlData)) + text := tstring.NewTString("") + + openTags := &TagArray{} + +Loop: + for { + tt := z.Next() + switch tt { + case html.ErrorToken: + break Loop + case html.TextToken: + style := tcell.StyleDefault + for _, tag := range *openTags { + switch tag { + case "b", "strong": + style = style.Bold(true) + case "i", "em": + style = style.Italic(true) + case "s", "del": + style = style.Strikethrough(true) + case "u", "ins": + style = style.Underline(true) + } + } + text = text.AppendStyle(string(z.Text()), style) + case html.SelfClosingTagToken, html.StartTagToken: + tagb, _ := z.TagName() + tag := string(tagb) + switch tag { + case "br": + debug.Print("BR found") + debug.Print(text.String()) + text = text.Append("\n") + default: + if tt == html.StartTagToken { + openTags.Push(tag) + } + } + case html.EndTagToken: + tagb, _ := z.TagName() + openTags.Popb(tagb) + } + } + + return text +} diff --git a/ui/messages/parser.go b/ui/messages/parser.go index 263bced..d8069c6 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -56,8 +56,14 @@ func ParseMessage(gmx ifc.Gomuks, evt *gomatrix.Event) UIMessage { ts := unixToTime(evt.Timestamp) switch msgtype { case "m.text", "m.notice", "m.emote": - text, _ := evt.Content["body"].(string) - return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) + format, hasFormat := evt.Content["format"].(string) + if hasFormat && format == "org.matrix.custom.html" { + text := ParseHTMLMessage(evt) + return NewExpandedTextMessage(evt.ID, evt.Sender, msgtype, text, ts) + } else { + text, _ := evt.Content["body"].(string) + return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) + } case "m.image": url, _ := evt.Content["url"].(string) data, hs, id, err := gmx.Matrix().Download(url) diff --git a/ui/messages/tstring/string.go b/ui/messages/tstring/string.go index ad0b2c8..d1ad446 100644 --- a/ui/messages/tstring/string.go +++ b/ui/messages/tstring/string.go @@ -19,8 +19,8 @@ package tstring import ( "strings" - "maunium.net/go/tcell" "github.com/mattn/go-runewidth" + "maunium.net/go/tcell" ) type TString []Cell @@ -49,6 +49,37 @@ func NewStyleTString(str string, style tcell.Style) TString { return newStr } +func (str TString) AppendTString(data TString) TString { + return append(str, data...) +} + +func (str TString) Append(data string) TString { + newStr := make(TString, len(str)+len(data)) + copy(newStr, str) + for i, char := range data { + newStr[i+len(str)] = NewCell(char) + } + return newStr +} + +func (str TString) AppendColor(data string, color tcell.Color) TString { + newStr := make(TString, len(str)+len(data)) + copy(newStr, str) + for i, char := range data { + newStr[i+len(str)] = NewColorCell(char, color) + } + return newStr +} + +func (str TString) AppendStyle(data string, style tcell.Style) TString { + newStr := make(TString, len(str)+len(data)) + copy(newStr, str) + for i, char := range data { + newStr[i+len(str)] = NewStyleCell(char, style) + } + return newStr +} + func (str TString) Colorize(from, length int, color tcell.Color) { for i := from; i < from+length; i++ { str[i].Style = str[i].Style.Foreground(color) diff --git a/ui/view-main.go b/ui/view-main.go index e5850d3..ccb3cc1 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -164,9 +164,7 @@ func (view *MainView) HandleCommand(roomView *RoomView, command string, args []s view.gmx.Stop() case "/panic": panic("This is a test panic.") - case "/part": - fallthrough - case "/leave": + case "/part", "/leave": debug.Print("Leave room result:", view.matrix.LeaveRoom(roomView.Room.ID)) case "/join": if len(args) == 0 {