From cf93671ecdebc96cfd45fefaeb9fc95f62393a33 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 7 Apr 2019 20:13:23 +0300 Subject: Add syntax highlighting. Fixes #28 --- ui/messages/htmlmessage.go | 50 ++++++++++++++++----- ui/messages/parser/htmlparser.go | 95 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 15 deletions(-) (limited to 'ui/messages') diff --git a/ui/messages/htmlmessage.go b/ui/messages/htmlmessage.go index 51678ba..0ffb6c9 100644 --- a/ui/messages/htmlmessage.go +++ b/ui/messages/htmlmessage.go @@ -72,23 +72,23 @@ func (hw *HTMLMessage) Height() int { } func (hw *HTMLMessage) PlainText() string { - // FIXME - return "Plaintext unavailable" + return hw.Root.PlainText() } func (hw *HTMLMessage) NotificationContent() string { - // FIXME - return "Notification content unavailable" + return hw.Root.PlainText() } type HTMLEntity struct { // Permanent variables - Tag string - Text string - Style tcell.Style - Children []*HTMLEntity - Block bool - Indent int + Tag string + Text string + Style tcell.Style + Children []*HTMLEntity + Block bool + Indent int + + DefaultHeight int // Non-permanent variables (calculated buffer data) buffer []string @@ -130,8 +130,9 @@ func (he *HTMLEntity) Draw(screen mauview.Screen) { func (he *HTMLEntity) String() string { var buf strings.Builder buf.WriteString("&HTMLEntity{\n") - _, _ = fmt.Fprintf(&buf, ` Tag="%s", Style=%d, Block=%t, Indent=%d, startX=%d, height=%d,\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") @@ -150,6 +151,27 @@ func (he *HTMLEntity) String() string { return buf.String() } +func (he *HTMLEntity) 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.Block && !newlined { + buf.WriteRune('\n') + } + newlined = false + buf.WriteString(child.PlainText()) + if child.Block { + buf.WriteRune('\n') + newlined = true + } + } + return buf.String() +} + func (he *HTMLEntity) calculateBuffer(width, startX int, bare bool) int { he.startX = startX if he.Block { @@ -178,6 +200,7 @@ func (he *HTMLEntity) calculateBuffer(width, startX int, bare bool) int { 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 { @@ -210,7 +233,10 @@ func (he *HTMLEntity) calculateBuffer(width, startX int, bare bool) int { textStartX = 0 } } - return 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) { diff --git a/ui/messages/parser/htmlparser.go b/ui/messages/parser/htmlparser.go index 9a9c2d1..688deb3 100644 --- a/ui/messages/parser/htmlparser.go +++ b/ui/messages/parser/htmlparser.go @@ -23,6 +23,9 @@ import ( "strconv" "strings" + "github.com/alecthomas/chroma" + "github.com/alecthomas/chroma/lexers" + "github.com/alecthomas/chroma/styles" "github.com/lucasb-eyer/go-colorful" "golang.org/x/net/html" @@ -216,18 +219,102 @@ func (parser *htmlParser) linkToEntity(node *html.Node, stripLinebreak bool) *me } } } - // TODO add click action for links + // TODO add click action and underline on hover for links return entity } -func (parser *htmlParser) codeblockToEntity(node *html.Node) *messages.HTMLEntity { +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.HTMLEntity{ + 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.HTMLEntity{Block: true, Tag: "br"} + } else { + children[i] = &messages.HTMLEntity{ + Tag: token.Type.String(), + Text: token.Value, + Style: styleEntryToStyle(style.Get(token.Type)), + + DefaultHeight: 1, + } + } + } return &messages.HTMLEntity{ Tag: "pre", - Children: parser.nodeToEntities(node.FirstChild, false), Block: true, + Children: children, } } +func (parser *htmlParser) codeblockToEntity(node *html.Node) *messages.HTMLEntity { + entity := &messages.HTMLEntity{ + Tag: "pre", + Block: true, + } + // TODO allow disabling syntax highlighting + if node.FirstChild.Type == html.ElementNode && node.FirstChild.Data == "code" { + text := (&messages.HTMLEntity{ + 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 { case "blockquote": @@ -242,6 +329,8 @@ func (parser *htmlParser) tagNodeToEntity(node *html.Node, stripLinebreak bool) return parser.basicFormatToEntity(node, stripLinebreak) case "a": return parser.linkToEntity(node, stripLinebreak) + case "img": + return parser.imageToEntity(node) case "pre": return parser.codeblockToEntity(node) default: -- cgit v1.2.3