203 lines
5.5 KiB
Go
203 lines
5.5 KiB
Go
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
package parser
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"maunium.net/go/gomatrix"
|
|
"maunium.net/go/gomuks/debug"
|
|
"maunium.net/go/gomuks/lib/htmlparser"
|
|
"maunium.net/go/gomuks/matrix/rooms"
|
|
"maunium.net/go/gomuks/ui/messages/tstring"
|
|
"maunium.net/go/gomuks/ui/widget"
|
|
"maunium.net/go/tcell"
|
|
)
|
|
|
|
var matrixToURL = regexp.MustCompile("^(?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!].*)")
|
|
|
|
type MatrixHTMLProcessor struct {
|
|
text tstring.TString
|
|
|
|
senderID string
|
|
sender string
|
|
msgtype string
|
|
|
|
indent string
|
|
listType string
|
|
lineIsNew bool
|
|
openTags *TagArray
|
|
|
|
room *rooms.Room
|
|
}
|
|
|
|
func (parser *MatrixHTMLProcessor) newline() {
|
|
if !parser.lineIsNew {
|
|
parser.text = parser.text.Append("\n" + parser.indent)
|
|
parser.lineIsNew = true
|
|
}
|
|
}
|
|
|
|
func (parser *MatrixHTMLProcessor) Preprocess() {
|
|
if parser.msgtype == "m.emote" {
|
|
parser.text = tstring.NewColorTString(fmt.Sprintf("* %s ", parser.sender), widget.GetHashColor(parser.senderID))
|
|
}
|
|
}
|
|
|
|
func (parser *MatrixHTMLProcessor) HandleText(text string) {
|
|
style := tcell.StyleDefault
|
|
for _, tag := range *parser.openTags {
|
|
switch tag.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)
|
|
case "a":
|
|
tag.Text += text
|
|
return
|
|
}
|
|
}
|
|
|
|
if !parser.openTags.Has("pre", "code") {
|
|
text = strings.Replace(text, "\n", "", -1)
|
|
}
|
|
parser.text = parser.text.AppendStyle(text, style)
|
|
parser.lineIsNew = false
|
|
}
|
|
|
|
func (parser *MatrixHTMLProcessor) HandleStartTag(tagName string, attrs map[string]string) {
|
|
tag := &TagWithMeta{Tag: tagName}
|
|
switch tag.Tag {
|
|
case "h1", "h2", "h3", "h4", "h5", "h6":
|
|
length := int(tag.Tag[1] - '0')
|
|
parser.text = parser.text.Append(strings.Repeat("#", length) + " ")
|
|
parser.lineIsNew = false
|
|
case "a":
|
|
tag.Meta, _ = attrs["href"]
|
|
case "ol", "ul":
|
|
parser.listType = tag.Tag
|
|
case "li":
|
|
indentSize := 2
|
|
if parser.listType == "ol" {
|
|
list := parser.openTags.Get(parser.listType)
|
|
list.Counter++
|
|
parser.text = parser.text.Append(fmt.Sprintf("%d. ", list.Counter))
|
|
indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ")
|
|
} else {
|
|
parser.text = parser.text.Append("* ")
|
|
}
|
|
parser.indent += strings.Repeat(" ", indentSize)
|
|
parser.lineIsNew = false
|
|
case "blockquote":
|
|
parser.indent += "> "
|
|
parser.text = parser.text.Append("> ")
|
|
parser.lineIsNew = false
|
|
}
|
|
parser.openTags.PushMeta(tag)
|
|
}
|
|
|
|
func (parser *MatrixHTMLProcessor) HandleSelfClosingTag(tagName string, attrs map[string]string) {
|
|
if tagName == "br" {
|
|
parser.newline()
|
|
}
|
|
}
|
|
|
|
func (parser *MatrixHTMLProcessor) HandleEndTag(tagName string) {
|
|
tag := parser.openTags.Pop(tagName)
|
|
if tag == nil {
|
|
return
|
|
}
|
|
|
|
switch tag.Tag {
|
|
case "li", "blockquote":
|
|
indentSize := 2
|
|
if tag.Tag == "li" && parser.listType == "ol" {
|
|
list := parser.openTags.Get(parser.listType)
|
|
indentSize = int(math.Log10(float64(list.Counter))+1) + len(". ")
|
|
}
|
|
if len(parser.indent) >= indentSize {
|
|
parser.indent = parser.indent[0 : len(parser.indent)-indentSize]
|
|
}
|
|
// TODO this newline is sometimes not good
|
|
parser.newline()
|
|
case "a":
|
|
match := matrixToURL.FindStringSubmatch(tag.Meta)
|
|
if len(match) == 2 {
|
|
pillTarget := match[1]
|
|
if pillTarget[0] == '@' {
|
|
if member := parser.room.GetMember(pillTarget); member != nil {
|
|
parser.text = parser.text.AppendColor(member.DisplayName, widget.GetHashColor(member.UserID))
|
|
} else {
|
|
parser.text = parser.text.Append(pillTarget)
|
|
}
|
|
} else {
|
|
parser.text = parser.text.Append(pillTarget)
|
|
}
|
|
} else {
|
|
// TODO make text clickable rather than printing URL
|
|
parser.text = parser.text.Append(fmt.Sprintf("%s (%s)", tag.Text, tag.Meta))
|
|
}
|
|
parser.lineIsNew = false
|
|
case "p", "pre", "ol", "ul", "h1", "h2", "h3", "h4", "h5", "h6", "div":
|
|
// parser.newline()
|
|
}
|
|
}
|
|
|
|
func (parser *MatrixHTMLProcessor) ReceiveError(err error) {
|
|
if err != io.EOF {
|
|
debug.Print("Unexpected error parsing HTML:", err)
|
|
}
|
|
}
|
|
|
|
func (parser *MatrixHTMLProcessor) Postprocess() {
|
|
if len(parser.text) > 0 && parser.text[len(parser.text)-1].Char == '\n' {
|
|
parser.text = parser.text[:len(parser.text)-1]
|
|
}
|
|
}
|
|
|
|
// ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage.
|
|
func ParseHTMLMessage(room *rooms.Room, evt *gomatrix.Event, senderDisplayname string) tstring.TString {
|
|
htmlData, _ := evt.Content["formatted_body"].(string)
|
|
htmlData = strings.Replace(htmlData, "\t", " ", -1)
|
|
msgtype, _ := evt.Content["msgtype"].(string)
|
|
|
|
processor := &MatrixHTMLProcessor{
|
|
room: room,
|
|
text: tstring.NewBlankTString(),
|
|
msgtype: msgtype,
|
|
senderID: evt.Sender,
|
|
sender: senderDisplayname,
|
|
indent: "",
|
|
listType: "",
|
|
lineIsNew: true,
|
|
openTags: &TagArray{},
|
|
}
|
|
|
|
parser := htmlparser.NewHTMLParserFromString(htmlData, processor)
|
|
parser.Process()
|
|
|
|
return processor.text
|
|
}
|