aboutsummaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorTulir Asokan <tulir@maunium.net>2019-04-07 20:13:23 +0300
committerTulir Asokan <tulir@maunium.net>2019-04-07 20:13:23 +0300
commitcf93671ecdebc96cfd45fefaeb9fc95f62393a33 (patch)
tree4be979095f5a622f8308092a82a033be665d0781 /ui
parent083ae8bd44ad34859781ea88cae9f57d9404c7e5 (diff)
Add syntax highlighting. Fixes #28
Diffstat (limited to 'ui')
-rw-r--r--ui/messages/htmlmessage.go50
-rw-r--r--ui/messages/parser/htmlparser.go95
2 files changed, 130 insertions, 15 deletions
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: