From 2b6c435e5020535a916e23b09d47608d788eaf05 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 9 Apr 2019 18:42:49 +0300 Subject: Refactor message parsing things --- ui/messages/html/blockquote.go | 49 +++++ ui/messages/html/break.go | 28 +++ ui/messages/html/codeblock.go | 48 +++++ ui/messages/html/colormap.go | 156 ++++++++++++++++ ui/messages/html/entity.go | 312 ++++++++++++++++++++++++++++++++ ui/messages/html/list.go | 80 ++++++++ ui/messages/html/parser.go | 380 ++++++++++++++++++++++++++++++++++++++ ui/messages/htmlmessage.go | 381 +-------------------------------------- ui/messages/parser.go | 288 +++++++++++++++++++++++++++++ ui/messages/parser/colormap.go | 156 ---------------- ui/messages/parser/doc.go | 2 - ui/messages/parser/htmlparser.go | 376 -------------------------------------- ui/messages/parser/parser.go | 287 ----------------------------- ui/view-main.go | 4 +- 14 files changed, 1352 insertions(+), 1195 deletions(-) create mode 100644 ui/messages/html/blockquote.go create mode 100644 ui/messages/html/break.go create mode 100644 ui/messages/html/codeblock.go create mode 100644 ui/messages/html/colormap.go create mode 100644 ui/messages/html/entity.go create mode 100644 ui/messages/html/list.go create mode 100644 ui/messages/html/parser.go create mode 100644 ui/messages/parser.go delete mode 100644 ui/messages/parser/colormap.go delete mode 100644 ui/messages/parser/doc.go delete mode 100644 ui/messages/parser/htmlparser.go delete mode 100644 ui/messages/parser/parser.go (limited to 'ui') diff --git a/ui/messages/html/blockquote.go b/ui/messages/html/blockquote.go new file mode 100644 index 0000000..30ce52e --- /dev/null +++ b/ui/messages/html/blockquote.go @@ -0,0 +1,49 @@ +// 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 . + +package html + +import ( + "fmt" + + "maunium.net/go/mauview" +) + +type BlockquoteEntity struct { + *BaseEntity +} + +const BlockQuoteChar = '>' + +func NewBlockquoteEntity(children []Entity) *BlockquoteEntity { + return &BlockquoteEntity{&BaseEntity{ + Tag: "blockquote", + Children: children, + Block: true, + Indent: 2, + }} +} + +func (be *BlockquoteEntity) Draw(screen mauview.Screen) { + be.BaseEntity.Draw(screen) + for y := 0; y < be.height; y++ { + screen.SetContent(0, y, BlockQuoteChar, nil, be.Style) + } +} + +func (be *BlockquoteEntity) String() string { + return fmt.Sprintf("&html.BlockquoteEntity{%s},\n", be.BaseEntity) +} diff --git a/ui/messages/html/break.go b/ui/messages/html/break.go new file mode 100644 index 0000000..d400f00 --- /dev/null +++ b/ui/messages/html/break.go @@ -0,0 +1,28 @@ +// 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 . + +package html + +type BreakEntity struct { + *BaseEntity +} + +func NewBreakEntity() *BreakEntity { + return &BreakEntity{&BaseEntity{ + Tag: "br", + Block: true, + }} +} diff --git a/ui/messages/html/codeblock.go b/ui/messages/html/codeblock.go new file mode 100644 index 0000000..ec6181d --- /dev/null +++ b/ui/messages/html/codeblock.go @@ -0,0 +1,48 @@ +// 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 . + +package html + +import ( + "maunium.net/go/mauview" + "maunium.net/go/tcell" +) + +type CodeBlockEntity struct { + *BaseEntity + Background tcell.Style +} + +func NewCodeBlockEntity(children []Entity, background tcell.Style) *CodeBlockEntity { + return &CodeBlockEntity{ + BaseEntity: &BaseEntity{ + Tag: "pre", + Block: true, + Children: children, + }, + Background: background, + } +} + +func (ce *CodeBlockEntity) Draw(screen mauview.Screen) { + screen.Fill(' ', ce.Background) + ce.BaseEntity.Draw(screen) +} + +func (ce *CodeBlockEntity) AdjustStyle(fn AdjustStyleFunc) Entity { + // Don't allow adjusting code block style. + return ce +} diff --git a/ui/messages/html/colormap.go b/ui/messages/html/colormap.go new file mode 100644 index 0000000..305309c --- /dev/null +++ b/ui/messages/html/colormap.go @@ -0,0 +1,156 @@ +// From https://github.com/golang/image/blob/master/colornames/colornames.go +package html + +import ( + "image/color" +) + +var colorMap = map[string]color.RGBA{ + "aliceblue": {0xf0, 0xf8, 0xff, 0xff}, // rgb(240, 248, 255) + "antiquewhite": {0xfa, 0xeb, 0xd7, 0xff}, // rgb(250, 235, 215) + "aqua": {0x00, 0xff, 0xff, 0xff}, // rgb(0, 255, 255) + "aquamarine": {0x7f, 0xff, 0xd4, 0xff}, // rgb(127, 255, 212) + "azure": {0xf0, 0xff, 0xff, 0xff}, // rgb(240, 255, 255) + "beige": {0xf5, 0xf5, 0xdc, 0xff}, // rgb(245, 245, 220) + "bisque": {0xff, 0xe4, 0xc4, 0xff}, // rgb(255, 228, 196) + "black": {0x00, 0x00, 0x00, 0xff}, // rgb(0, 0, 0) + "blanchedalmond": {0xff, 0xeb, 0xcd, 0xff}, // rgb(255, 235, 205) + "blue": {0x00, 0x00, 0xff, 0xff}, // rgb(0, 0, 255) + "blueviolet": {0x8a, 0x2b, 0xe2, 0xff}, // rgb(138, 43, 226) + "brown": {0xa5, 0x2a, 0x2a, 0xff}, // rgb(165, 42, 42) + "burlywood": {0xde, 0xb8, 0x87, 0xff}, // rgb(222, 184, 135) + "cadetblue": {0x5f, 0x9e, 0xa0, 0xff}, // rgb(95, 158, 160) + "chartreuse": {0x7f, 0xff, 0x00, 0xff}, // rgb(127, 255, 0) + "chocolate": {0xd2, 0x69, 0x1e, 0xff}, // rgb(210, 105, 30) + "coral": {0xff, 0x7f, 0x50, 0xff}, // rgb(255, 127, 80) + "cornflowerblue": {0x64, 0x95, 0xed, 0xff}, // rgb(100, 149, 237) + "cornsilk": {0xff, 0xf8, 0xdc, 0xff}, // rgb(255, 248, 220) + "crimson": {0xdc, 0x14, 0x3c, 0xff}, // rgb(220, 20, 60) + "cyan": {0x00, 0xff, 0xff, 0xff}, // rgb(0, 255, 255) + "darkblue": {0x00, 0x00, 0x8b, 0xff}, // rgb(0, 0, 139) + "darkcyan": {0x00, 0x8b, 0x8b, 0xff}, // rgb(0, 139, 139) + "darkgoldenrod": {0xb8, 0x86, 0x0b, 0xff}, // rgb(184, 134, 11) + "darkgray": {0xa9, 0xa9, 0xa9, 0xff}, // rgb(169, 169, 169) + "darkgreen": {0x00, 0x64, 0x00, 0xff}, // rgb(0, 100, 0) + "darkgrey": {0xa9, 0xa9, 0xa9, 0xff}, // rgb(169, 169, 169) + "darkkhaki": {0xbd, 0xb7, 0x6b, 0xff}, // rgb(189, 183, 107) + "darkmagenta": {0x8b, 0x00, 0x8b, 0xff}, // rgb(139, 0, 139) + "darkolivegreen": {0x55, 0x6b, 0x2f, 0xff}, // rgb(85, 107, 47) + "darkorange": {0xff, 0x8c, 0x00, 0xff}, // rgb(255, 140, 0) + "darkorchid": {0x99, 0x32, 0xcc, 0xff}, // rgb(153, 50, 204) + "darkred": {0x8b, 0x00, 0x00, 0xff}, // rgb(139, 0, 0) + "darksalmon": {0xe9, 0x96, 0x7a, 0xff}, // rgb(233, 150, 122) + "darkseagreen": {0x8f, 0xbc, 0x8f, 0xff}, // rgb(143, 188, 143) + "darkslateblue": {0x48, 0x3d, 0x8b, 0xff}, // rgb(72, 61, 139) + "darkslategray": {0x2f, 0x4f, 0x4f, 0xff}, // rgb(47, 79, 79) + "darkslategrey": {0x2f, 0x4f, 0x4f, 0xff}, // rgb(47, 79, 79) + "darkturquoise": {0x00, 0xce, 0xd1, 0xff}, // rgb(0, 206, 209) + "darkviolet": {0x94, 0x00, 0xd3, 0xff}, // rgb(148, 0, 211) + "deeppink": {0xff, 0x14, 0x93, 0xff}, // rgb(255, 20, 147) + "deepskyblue": {0x00, 0xbf, 0xff, 0xff}, // rgb(0, 191, 255) + "dimgray": {0x69, 0x69, 0x69, 0xff}, // rgb(105, 105, 105) + "dimgrey": {0x69, 0x69, 0x69, 0xff}, // rgb(105, 105, 105) + "dodgerblue": {0x1e, 0x90, 0xff, 0xff}, // rgb(30, 144, 255) + "firebrick": {0xb2, 0x22, 0x22, 0xff}, // rgb(178, 34, 34) + "floralwhite": {0xff, 0xfa, 0xf0, 0xff}, // rgb(255, 250, 240) + "forestgreen": {0x22, 0x8b, 0x22, 0xff}, // rgb(34, 139, 34) + "fuchsia": {0xff, 0x00, 0xff, 0xff}, // rgb(255, 0, 255) + "gainsboro": {0xdc, 0xdc, 0xdc, 0xff}, // rgb(220, 220, 220) + "ghostwhite": {0xf8, 0xf8, 0xff, 0xff}, // rgb(248, 248, 255) + "gold": {0xff, 0xd7, 0x00, 0xff}, // rgb(255, 215, 0) + "goldenrod": {0xda, 0xa5, 0x20, 0xff}, // rgb(218, 165, 32) + "gray": {0x80, 0x80, 0x80, 0xff}, // rgb(128, 128, 128) + "green": {0x00, 0x80, 0x00, 0xff}, // rgb(0, 128, 0) + "greenyellow": {0xad, 0xff, 0x2f, 0xff}, // rgb(173, 255, 47) + "grey": {0x80, 0x80, 0x80, 0xff}, // rgb(128, 128, 128) + "honeydew": {0xf0, 0xff, 0xf0, 0xff}, // rgb(240, 255, 240) + "hotpink": {0xff, 0x69, 0xb4, 0xff}, // rgb(255, 105, 180) + "indianred": {0xcd, 0x5c, 0x5c, 0xff}, // rgb(205, 92, 92) + "indigo": {0x4b, 0x00, 0x82, 0xff}, // rgb(75, 0, 130) + "ivory": {0xff, 0xff, 0xf0, 0xff}, // rgb(255, 255, 240) + "khaki": {0xf0, 0xe6, 0x8c, 0xff}, // rgb(240, 230, 140) + "lavender": {0xe6, 0xe6, 0xfa, 0xff}, // rgb(230, 230, 250) + "lavenderblush": {0xff, 0xf0, 0xf5, 0xff}, // rgb(255, 240, 245) + "lawngreen": {0x7c, 0xfc, 0x00, 0xff}, // rgb(124, 252, 0) + "lemonchiffon": {0xff, 0xfa, 0xcd, 0xff}, // rgb(255, 250, 205) + "lightblue": {0xad, 0xd8, 0xe6, 0xff}, // rgb(173, 216, 230) + "lightcoral": {0xf0, 0x80, 0x80, 0xff}, // rgb(240, 128, 128) + "lightcyan": {0xe0, 0xff, 0xff, 0xff}, // rgb(224, 255, 255) + "lightgoldenrodyellow": {0xfa, 0xfa, 0xd2, 0xff}, // rgb(250, 250, 210) + "lightgray": {0xd3, 0xd3, 0xd3, 0xff}, // rgb(211, 211, 211) + "lightgreen": {0x90, 0xee, 0x90, 0xff}, // rgb(144, 238, 144) + "lightgrey": {0xd3, 0xd3, 0xd3, 0xff}, // rgb(211, 211, 211) + "lightpink": {0xff, 0xb6, 0xc1, 0xff}, // rgb(255, 182, 193) + "lightsalmon": {0xff, 0xa0, 0x7a, 0xff}, // rgb(255, 160, 122) + "lightseagreen": {0x20, 0xb2, 0xaa, 0xff}, // rgb(32, 178, 170) + "lightskyblue": {0x87, 0xce, 0xfa, 0xff}, // rgb(135, 206, 250) + "lightslategray": {0x77, 0x88, 0x99, 0xff}, // rgb(119, 136, 153) + "lightslategrey": {0x77, 0x88, 0x99, 0xff}, // rgb(119, 136, 153) + "lightsteelblue": {0xb0, 0xc4, 0xde, 0xff}, // rgb(176, 196, 222) + "lightyellow": {0xff, 0xff, 0xe0, 0xff}, // rgb(255, 255, 224) + "lime": {0x00, 0xff, 0x00, 0xff}, // rgb(0, 255, 0) + "limegreen": {0x32, 0xcd, 0x32, 0xff}, // rgb(50, 205, 50) + "linen": {0xfa, 0xf0, 0xe6, 0xff}, // rgb(250, 240, 230) + "magenta": {0xff, 0x00, 0xff, 0xff}, // rgb(255, 0, 255) + "maroon": {0x80, 0x00, 0x00, 0xff}, // rgb(128, 0, 0) + "mediumaquamarine": {0x66, 0xcd, 0xaa, 0xff}, // rgb(102, 205, 170) + "mediumblue": {0x00, 0x00, 0xcd, 0xff}, // rgb(0, 0, 205) + "mediumorchid": {0xba, 0x55, 0xd3, 0xff}, // rgb(186, 85, 211) + "mediumpurple": {0x93, 0x70, 0xdb, 0xff}, // rgb(147, 112, 219) + "mediumseagreen": {0x3c, 0xb3, 0x71, 0xff}, // rgb(60, 179, 113) + "mediumslateblue": {0x7b, 0x68, 0xee, 0xff}, // rgb(123, 104, 238) + "mediumspringgreen": {0x00, 0xfa, 0x9a, 0xff}, // rgb(0, 250, 154) + "mediumturquoise": {0x48, 0xd1, 0xcc, 0xff}, // rgb(72, 209, 204) + "mediumvioletred": {0xc7, 0x15, 0x85, 0xff}, // rgb(199, 21, 133) + "midnightblue": {0x19, 0x19, 0x70, 0xff}, // rgb(25, 25, 112) + "mintcream": {0xf5, 0xff, 0xfa, 0xff}, // rgb(245, 255, 250) + "mistyrose": {0xff, 0xe4, 0xe1, 0xff}, // rgb(255, 228, 225) + "moccasin": {0xff, 0xe4, 0xb5, 0xff}, // rgb(255, 228, 181) + "navajowhite": {0xff, 0xde, 0xad, 0xff}, // rgb(255, 222, 173) + "navy": {0x00, 0x00, 0x80, 0xff}, // rgb(0, 0, 128) + "oldlace": {0xfd, 0xf5, 0xe6, 0xff}, // rgb(253, 245, 230) + "olive": {0x80, 0x80, 0x00, 0xff}, // rgb(128, 128, 0) + "olivedrab": {0x6b, 0x8e, 0x23, 0xff}, // rgb(107, 142, 35) + "orange": {0xff, 0xa5, 0x00, 0xff}, // rgb(255, 165, 0) + "orangered": {0xff, 0x45, 0x00, 0xff}, // rgb(255, 69, 0) + "orchid": {0xda, 0x70, 0xd6, 0xff}, // rgb(218, 112, 214) + "palegoldenrod": {0xee, 0xe8, 0xaa, 0xff}, // rgb(238, 232, 170) + "palegreen": {0x98, 0xfb, 0x98, 0xff}, // rgb(152, 251, 152) + "paleturquoise": {0xaf, 0xee, 0xee, 0xff}, // rgb(175, 238, 238) + "palevioletred": {0xdb, 0x70, 0x93, 0xff}, // rgb(219, 112, 147) + "papayawhip": {0xff, 0xef, 0xd5, 0xff}, // rgb(255, 239, 213) + "peachpuff": {0xff, 0xda, 0xb9, 0xff}, // rgb(255, 218, 185) + "peru": {0xcd, 0x85, 0x3f, 0xff}, // rgb(205, 133, 63) + "pink": {0xff, 0xc0, 0xcb, 0xff}, // rgb(255, 192, 203) + "plum": {0xdd, 0xa0, 0xdd, 0xff}, // rgb(221, 160, 221) + "powderblue": {0xb0, 0xe0, 0xe6, 0xff}, // rgb(176, 224, 230) + "purple": {0x80, 0x00, 0x80, 0xff}, // rgb(128, 0, 128) + "red": {0xff, 0x00, 0x00, 0xff}, // rgb(255, 0, 0) + "rosybrown": {0xbc, 0x8f, 0x8f, 0xff}, // rgb(188, 143, 143) + "royalblue": {0x41, 0x69, 0xe1, 0xff}, // rgb(65, 105, 225) + "saddlebrown": {0x8b, 0x45, 0x13, 0xff}, // rgb(139, 69, 19) + "salmon": {0xfa, 0x80, 0x72, 0xff}, // rgb(250, 128, 114) + "sandybrown": {0xf4, 0xa4, 0x60, 0xff}, // rgb(244, 164, 96) + "seagreen": {0x2e, 0x8b, 0x57, 0xff}, // rgb(46, 139, 87) + "seashell": {0xff, 0xf5, 0xee, 0xff}, // rgb(255, 245, 238) + "sienna": {0xa0, 0x52, 0x2d, 0xff}, // rgb(160, 82, 45) + "silver": {0xc0, 0xc0, 0xc0, 0xff}, // rgb(192, 192, 192) + "skyblue": {0x87, 0xce, 0xeb, 0xff}, // rgb(135, 206, 235) + "slateblue": {0x6a, 0x5a, 0xcd, 0xff}, // rgb(106, 90, 205) + "slategray": {0x70, 0x80, 0x90, 0xff}, // rgb(112, 128, 144) + "slategrey": {0x70, 0x80, 0x90, 0xff}, // rgb(112, 128, 144) + "snow": {0xff, 0xfa, 0xfa, 0xff}, // rgb(255, 250, 250) + "springgreen": {0x00, 0xff, 0x7f, 0xff}, // rgb(0, 255, 127) + "steelblue": {0x46, 0x82, 0xb4, 0xff}, // rgb(70, 130, 180) + "tan": {0xd2, 0xb4, 0x8c, 0xff}, // rgb(210, 180, 140) + "teal": {0x00, 0x80, 0x80, 0xff}, // rgb(0, 128, 128) + "thistle": {0xd8, 0xbf, 0xd8, 0xff}, // rgb(216, 191, 216) + "tomato": {0xff, 0x63, 0x47, 0xff}, // rgb(255, 99, 71) + "turquoise": {0x40, 0xe0, 0xd0, 0xff}, // rgb(64, 224, 208) + "violet": {0xee, 0x82, 0xee, 0xff}, // rgb(238, 130, 238) + "wheat": {0xf5, 0xde, 0xb3, 0xff}, // rgb(245, 222, 179) + "white": {0xff, 0xff, 0xff, 0xff}, // rgb(255, 255, 255) + "whitesmoke": {0xf5, 0xf5, 0xf5, 0xff}, // rgb(245, 245, 245) + "yellow": {0xff, 0xff, 0x00, 0xff}, // rgb(255, 255, 0) + "yellowgreen": {0x9a, 0xcd, 0x32, 0xff}, // rgb(154, 205, 50) +} diff --git a/ui/messages/html/entity.go b/ui/messages/html/entity.go new file mode 100644 index 0000000..2ce37a8 --- /dev/null +++ b/ui/messages/html/entity.go @@ -0,0 +1,312 @@ +// 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 . + +package html + +import ( + "fmt" + "regexp" + "strings" + + "github.com/mattn/go-runewidth" + + "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/mauview" + "maunium.net/go/tcell" +) + +// AdjustStyleFunc is a lambda function type to edit an existing tcell Style. +type AdjustStyleFunc func(tcell.Style) tcell.Style + +type Entity interface { + // AdjustStyle recursively changes the style of the entity and all its children. + AdjustStyle(AdjustStyleFunc) Entity + // Draw draws the entity onto the given mauview Screen. + Draw(screen mauview.Screen) + // IsBlock returns whether or not it's a block-type entity. + IsBlock() bool + // GetTag returns the HTML tag of the entity. + GetTag() string + // PlainText returns the plaintext content in the entity and all its children. + PlainText() string + // String returns a string representation of the entity struct. + String() string + // Clone creates a deep copy of the entity. + Clone() Entity + + // Height returns the render height of the entity. + Height() int + // CalculateBuffer prepares the entity and all its children for rendering with the given parameters + CalculateBuffer(width, startX int, bare bool) int + + getStartX() int +} + +type BaseEntity struct { + // The HTML tag of this entity. + Tag string + // Text in this entity. + Text string + // Style for this entity. + Style tcell.Style + // Child entities. + Children []Entity + // Whether or not this is a block-type entity. + Block bool + // Number of cells to indent children. + Indent int + + // Height to use for entity if both text and children are empty. + DefaultHeight int + + buffer []string + prevWidth int + startX int + height int +} + +// NewTextEntity creates a new text-only Entity. +func NewTextEntity(text string) *BaseEntity { + return &BaseEntity{ + Tag: "text", + Text: text, + } +} + +// AdjustStyle recursively changes the style of this entity and all its children. +func (he *BaseEntity) AdjustStyle(fn AdjustStyleFunc) Entity { + for _, child := range he.Children { + child.AdjustStyle(fn) + } + he.Style = fn(he.Style) + return he +} + +// IsBlock returns whether or not this is a block-type entity. +func (he *BaseEntity) IsBlock() bool { + return he.Block +} + +// GetTag returns the HTML tag of this entity. +func (he *BaseEntity) GetTag() string { + return he.Tag +} + +// Height returns the render height of this entity. +func (he *BaseEntity) Height() int { + return he.height +} + +func (he *BaseEntity) getStartX() int { + return he.startX +} + +// Clone creates a deep copy of this entity. +func (he *BaseEntity) Clone() Entity { + children := make([]Entity, len(he.Children)) + for i, child := range he.Children { + children[i] = child.Clone() + } + return &BaseEntity{ + Tag: he.Tag, + Text: he.Text, + Style: he.Style, + Children: children, + Block: he.Block, + Indent: he.Indent, + DefaultHeight: he.DefaultHeight, + } +} + +// String returns a textual representation of this BaseEntity struct. +func (he *BaseEntity) String() string { + var buf strings.Builder + buf.WriteString("&html.BaseEntity{\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() +} + +// PlainText returns the plaintext content in this entity and all its children. +func (he *BaseEntity) 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() +} + +// Draw draws this entity onto the given mauview Screen. +func (he *BaseEntity) 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, Style: he.Style} + for i, entity := range he.Children { + if i != 0 && entity.getStartX() == 0 { + proxyScreen.OffsetY++ + } + proxyScreen.Height = entity.Height() + entity.Draw(proxyScreen) + proxyScreen.SetStyle(he.Style) + proxyScreen.OffsetY += entity.Height() - 1 + _, isBreak := entity.(*BreakEntity) + if prevBreak && isBreak { + proxyScreen.OffsetY++ + } + prevBreak = isBreak + } + } +} + +// CalculateBuffer prepares this entity and all its children for rendering with the given parameters +func (he *BaseEntity) 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 +} + +var ( + boundaryPattern = regexp.MustCompile(`([[:punct:]]\s*|\s+)`) + bareBoundaryPattern = regexp.MustCompile(`(\s+)`) + spacePattern = regexp.MustCompile(`\s+`) +) + +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] == ' ' +} diff --git a/ui/messages/html/list.go b/ui/messages/html/list.go new file mode 100644 index 0000000..9f45b92 --- /dev/null +++ b/ui/messages/html/list.go @@ -0,0 +1,80 @@ +// 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 . + +package html + +import ( + "fmt" + "math" + "strings" + + "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/mauview" +) + +type ListEntity struct { + *BaseEntity + 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 []Entity) *ListEntity { + entity := &ListEntity{ + BaseEntity: &BaseEntity{ + 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, Style: le.Style} + 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.SetStyle(le.Style) + proxyScreen.OffsetY += entity.Height() + } +} + +func (le *ListEntity) String() string { + return fmt.Sprintf("&html.ListEntity{Ordered=%t, Start=%d, Base=%s},\n", le.Ordered, le.Start, le.BaseEntity) +} diff --git a/ui/messages/html/parser.go b/ui/messages/html/parser.go new file mode 100644 index 0000000..dbd487e --- /dev/null +++ b/ui/messages/html/parser.go @@ -0,0 +1,380 @@ +// 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 . + +package html + +import ( + "regexp" + "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" + + "maunium.net/go/mautrix" + "maunium.net/go/tcell" + + "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/gomuks/ui/widget" +) + +var matrixToURL = regexp.MustCompile("^(?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!].*)") + +type htmlParser struct { + room *rooms.Room + + keepLinebreak bool +} + +func AdjustStyleBold(style tcell.Style) tcell.Style { + return style.Bold(true) +} + +func AdjustStyleItalic(style tcell.Style) tcell.Style { + return style.Italic(true) +} + +func AdjustStyleUnderline(style tcell.Style) tcell.Style { + return style.Underline(true) +} + +func AdjustStyleStrikethrough(style tcell.Style) tcell.Style { + return style.Strikethrough(true) +} + +func AdjustStyleTextColor(color tcell.Color) func(tcell.Style) tcell.Style { + return func(style tcell.Style) tcell.Style { + return style.Foreground(color) + } +} + +func AdjustStyleBackgroundColor(color tcell.Color) func(tcell.Style) tcell.Style { + return func(style tcell.Style) tcell.Style { + return style.Background(color) + } +} + +func (parser *htmlParser) getAttribute(node *html.Node, attribute string) string { + for _, attr := range node.Attr { + if attr.Key == attribute { + return attr.Val + } + } + return "" +} + +func (parser *htmlParser) listToEntity(node *html.Node) Entity { + children := parser.nodeToEntities(node.FirstChild) + ordered := node.Data == "ol" + start := 1 + if ordered { + if startRaw := parser.getAttribute(node, "start"); len(startRaw) > 0 { + var err error + start, err = strconv.Atoi(startRaw) + if err != nil { + start = 1 + } + } + } + listItems := children[:0] + for _, child := range children { + if child.GetTag() == "li" { + listItems = append(listItems, child) + } + } + return NewListEntity(ordered, start, listItems) +} + +func (parser *htmlParser) basicFormatToEntity(node *html.Node) Entity { + entity := &BaseEntity{ + Tag: node.Data, + Children: parser.nodeToEntities(node.FirstChild), + } + switch node.Data { + case "b", "strong": + entity.AdjustStyle(AdjustStyleBold) + case "i", "em": + entity.AdjustStyle(AdjustStyleItalic) + case "s", "del": + entity.AdjustStyle(AdjustStyleStrikethrough) + case "u", "ins": + 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 entity +} + +func (parser *htmlParser) parseColor(node *html.Node, mainName, altName string) (color tcell.Color, ok bool) { + hex := parser.getAttribute(node, mainName) + if len(hex) == 0 { + hex = parser.getAttribute(node, altName) + if len(hex) == 0 { + return + } + } + + cful, err := colorful.Hex(hex) + if err != nil { + color2, found := colorMap[strings.ToLower(hex)] + if !found { + return + } + cful, _ = colorful.MakeColor(color2) + } + + r, g, b := cful.RGB255() + return tcell.NewRGBColor(int32(r), int32(g), int32(b)), true +} + +func (parser *htmlParser) headerToEntity(node *html.Node) Entity { + length := int(node.Data[1] - '0') + prefix := strings.Repeat("#", length) + " " + return (&BaseEntity{ + Tag: node.Data, + Text: prefix, + Children: parser.nodeToEntities(node.FirstChild), + }).AdjustStyle(AdjustStyleBold) +} + +func (parser *htmlParser) blockquoteToEntity(node *html.Node) Entity { + return NewBlockquoteEntity(parser.nodeToEntities(node.FirstChild)) +} + +func (parser *htmlParser) linkToEntity(node *html.Node) Entity { + entity := &BaseEntity{ + Tag: "a", + Children: parser.nodeToEntities(node.FirstChild), + } + href := parser.getAttribute(node, "href") + if len(href) == 0 { + return entity + } + match := matrixToURL.FindStringSubmatch(href) + if len(match) == 2 { + entity.Children = nil + pillTarget := match[1] + entity.Text = pillTarget + if pillTarget[0] == '@' { + if member := parser.room.GetMember(pillTarget); member != nil { + entity.Text = member.Displayname + entity.Style = entity.Style.Foreground(widget.GetHashColor(pillTarget)) + } + } + } + // TODO add click action and underline on hover for links + return entity +} + +func (parser *htmlParser) imageToEntity(node *html.Node) Entity { + alt := parser.getAttribute(node, "alt") + if len(alt) == 0 { + alt = parser.getAttribute(node, "title") + if len(alt) == 0 { + alt = "[inline image]" + } + } + entity := &BaseEntity{ + 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) Entity { + lexer := lexers.Get(strings.ToLower(language)) + if lexer == nil { + return nil + } + iter, err := lexer.Tokenise(nil, text) + if err != nil { + return nil + } + // TODO allow changing theme + style := styles.SolarizedDark + + tokens := iter.Tokens() + children := make([]Entity, len(tokens)) + for i, token := range tokens { + if token.Value == "\n" { + children[i] = &BaseEntity{Block: true, Tag: "br"} + } else { + children[i] = &BaseEntity{ + Tag: token.Type.String(), + Text: token.Value, + Style: styleEntryToStyle(style.Get(token.Type)), + + DefaultHeight: 1, + } + } + } + return NewCodeBlockEntity(children, styleEntryToStyle(style.Get(chroma.Background))) +} + +func (parser *htmlParser) codeblockToEntity(node *html.Node) Entity { + lang := "plaintext" + // TODO allow disabling syntax highlighting + if node.FirstChild.Type == html.ElementNode && node.FirstChild.Data == "code" { + node = node.FirstChild + attr := parser.getAttribute(node, "class") + for _, class := range strings.Split(attr, " ") { + if strings.HasPrefix(class, "language-") { + lang = class[len("language-"):] + break + } + } + } + parser.keepLinebreak = true + text := (&BaseEntity{ + Children: parser.nodeToEntities(node.FirstChild), + }).PlainText() + parser.keepLinebreak = false + return parser.syntaxHighlight(text, lang) +} + +func (parser *htmlParser) tagNodeToEntity(node *html.Node) Entity { + switch node.Data { + case "blockquote": + return parser.blockquoteToEntity(node) + case "ol", "ul": + return parser.listToEntity(node) + case "h1", "h2", "h3", "h4", "h5", "h6": + return parser.headerToEntity(node) + case "br": + return NewBreakEntity() + case "b", "strong", "i", "em", "s", "del", "u", "ins", "font": + return parser.basicFormatToEntity(node) + case "a": + return parser.linkToEntity(node) + case "img": + return parser.imageToEntity(node) + case "pre": + return parser.codeblockToEntity(node) + default: + return &BaseEntity{ + Tag: node.Data, + Children: parser.nodeToEntities(node.FirstChild), + Block: parser.isBlockTag(node.Data), + } + } +} + +func (parser *htmlParser) singleNodeToEntity(node *html.Node) Entity { + switch node.Type { + case html.TextNode: + if !parser.keepLinebreak { + node.Data = strings.ReplaceAll(node.Data, "\n", "") + } + return &BaseEntity{ + Tag: "text", + Text: node.Data, + } + case html.ElementNode: + return parser.tagNodeToEntity(node) + case html.DocumentNode: + if node.FirstChild.Data == "html" && node.FirstChild.NextSibling == nil { + return parser.singleNodeToEntity(node.FirstChild) + } + return &BaseEntity{ + Tag: "html", + Children: parser.nodeToEntities(node.FirstChild), + Block: true, + } + default: + return nil + } +} + +func (parser *htmlParser) nodeToEntities(node *html.Node) (entities []Entity) { + for ; node != nil; node = node.NextSibling { + if entity := parser.singleNodeToEntity(node); entity != nil { + entities = append(entities, entity) + } + } + return +} + +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 { + for _, blockTag := range BlockTags { + if tag == blockTag { + return true + } + } + return false +} + +func (parser *htmlParser) Parse(htmlData string) Entity { + node, _ := html.Parse(strings.NewReader(htmlData)) + return parser.singleNodeToEntity(node) +} + +const TabLength = 4 + +// Parse parses a HTML-formatted Matrix event into a UIMessage. +func Parse(room *rooms.Room, evt *mautrix.Event, senderDisplayname string) Entity { + htmlData := evt.Content.FormattedBody + if evt.Content.Format != mautrix.FormatHTML { + htmlData = strings.ReplaceAll(html.EscapeString(evt.Content.Body), "\n", "
") + } + htmlData = strings.Replace(htmlData, "\t", strings.Repeat(" ", TabLength), -1) + + parser := htmlParser{room: room} + root := parser.Parse(htmlData) + root.(*BaseEntity).Block = false + + if evt.Content.MsgType == mautrix.MsgEmote { + root = &BaseEntity{ + Tag: "emote", + Children: []Entity{ + NewTextEntity("* "), + NewTextEntity(senderDisplayname).AdjustStyle(AdjustStyleTextColor(widget.GetHashColor(evt.Sender))), + NewTextEntity(" "), + root, + }, + } + } + + return root +} diff --git a/ui/messages/htmlmessage.go b/ui/messages/htmlmessage.go index eac0841..ed8b7c1 100644 --- a/ui/messages/htmlmessage.go +++ b/ui/messages/htmlmessage.go @@ -17,40 +17,33 @@ package messages import ( - "fmt" - "math" - "strings" - "time" - - "github.com/mattn/go-runewidth" + "maunium.net/go/gomuks/ui/messages/html" "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 - - FocusedBackground tcell.Color - - focused bool + Root html.Entity + FocusedBg tcell.Color + focused bool } -func NewHTMLMessage(id, sender, displayname string, msgtype mautrix.MessageType, root HTMLEntity, timestamp time.Time) UIMessage { +func NewHTMLMessage(event *mautrix.Event, displayname string, root html.Entity) UIMessage { return &HTMLMessage{ - BaseMessage: newBaseMessage(id, sender, displayname, msgtype, timestamp), + BaseMessage: newBaseMessage(event.ID, event.Sender, displayname, event.Content.MsgType, unixToTime(event.Timestamp)), Root: root, } } + func (hw *HTMLMessage) Draw(screen mauview.Screen) { if hw.focused { - screen.SetStyle(tcell.StyleDefault.Background(hw.FocusedBackground)) + screen.SetStyle(tcell.StyleDefault.Background(hw.FocusedBg)) } screen.Clear() hw.Root.Draw(screen) @@ -82,7 +75,7 @@ func (hw *HTMLMessage) CalculateBuffer(preferences config.UserPreferences, width } // TODO account for bare messages in initial startX startX := 0 - hw.Root.calculateBuffer(width, startX, preferences.BareMessageView) + hw.Root.CalculateBuffer(width, startX, preferences.BareMessageView) } func (hw *HTMLMessage) Height() int { @@ -96,359 +89,3 @@ func (hw *HTMLMessage) PlainText() string { 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, Style: le.Style} - 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.SetStyle(le.Style) - 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 CodeBlockEntity struct { - *BaseHTMLEntity - Background tcell.Style -} - -func NewCodeBlockEntity(children []HTMLEntity, background tcell.Style) *CodeBlockEntity { - return &CodeBlockEntity{ - BaseHTMLEntity: &BaseHTMLEntity{ - Tag: "pre", - Block: true, - Children: children, - }, - Background: background, - } -} - -func (ce *CodeBlockEntity) Draw(screen mauview.Screen) { - screen.Fill(' ', ce.Background) - ce.BaseHTMLEntity.Draw(screen) -} - -func (ce *CodeBlockEntity) AdjustStyle(fn AdjustStyleFunc) HTMLEntity { - return ce -} - -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, Style: he.Style} - for i, entity := range he.Children { - if i != 0 && entity.getStartX() == 0 { - proxyScreen.OffsetY++ - } - proxyScreen.Height = entity.Height() - entity.Draw(proxyScreen) - proxyScreen.SetStyle(he.Style) - 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] == ' ' -} diff --git a/ui/messages/parser.go b/ui/messages/parser.go new file mode 100644 index 0000000..ae0606d --- /dev/null +++ b/ui/messages/parser.go @@ -0,0 +1,288 @@ +// 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 . + +package messages + +import ( + "fmt" + "html" + "strings" + "time" + + "maunium.net/go/mautrix" + "maunium.net/go/tcell" + + "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/interface" + "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/gomuks/ui/messages/tstring" + "maunium.net/go/gomuks/ui/widget" + htmlp "maunium.net/go/gomuks/ui/messages/html" +) + +func ParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage { + switch evt.Type { + case mautrix.EventSticker: + evt.Content.MsgType = mautrix.MsgImage + fallthrough + case mautrix.EventMessage: + return ParseMessage(matrix, room, evt) + case mautrix.StateTopic, mautrix.StateRoomName, mautrix.StateAliases, mautrix.StateCanonicalAlias: + return ParseStateEvent(matrix, room, evt) + case mautrix.StateMember: + return ParseMembershipEvent(room, evt) + } + return nil +} + +func unixToTime(unix int64) time.Time { + timestamp := time.Now() + if unix != 0 { + timestamp = time.Unix(unix/1000, unix%1000*1000) + } + return timestamp +} + +func ParseStateEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage { + displayname := evt.Sender + member := room.GetMember(evt.Sender) + if member != nil { + displayname = member.Displayname + } + text := tstring.NewColorTString(displayname, widget.GetHashColor(evt.Sender)) + switch evt.Type { + case mautrix.StateTopic: + if len(evt.Content.Topic) == 0 { + text = text.AppendColor(" removed the topic.", tcell.ColorGreen) + } else { + text = text.AppendColor(" changed the topic to ", tcell.ColorGreen). + AppendStyle(evt.Content.Topic, tcell.StyleDefault.Underline(true)). + AppendColor(".", tcell.ColorGreen) + } + case mautrix.StateRoomName: + if len(evt.Content.Name) == 0 { + text = text.AppendColor(" removed the room name.", tcell.ColorGreen) + } else { + text = text.AppendColor(" changed the room name to ", tcell.ColorGreen). + AppendStyle(evt.Content.Name, tcell.StyleDefault.Underline(true)). + AppendColor(".", tcell.ColorGreen) + } + case mautrix.StateCanonicalAlias: + if len(evt.Content.Alias) == 0 { + text = text.AppendColor(" removed the main address of the room.", tcell.ColorGreen) + } else { + text = text.AppendColor(" changed the main address of the room to ", tcell.ColorGreen). + AppendStyle(evt.Content.Alias, tcell.StyleDefault.Underline(true)). + AppendColor(".", tcell.ColorGreen) + } + case mautrix.StateAliases: + text = ParseAliasEvent(evt, displayname) + } + ts := unixToTime(evt.Timestamp) + return NewExpandedTextMessage(evt.ID, evt.Sender, displayname, mautrix.MessageType(evt.Type.Type), text, ts) +} + +func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage { + displayname := evt.Sender + member := room.GetMember(evt.Sender) + if member != nil { + displayname = member.Displayname + } + if len(evt.Content.GetReplyTo()) > 0 { + evt.Content.RemoveReplyFallback() + roomID := evt.Content.RelatesTo.InReplyTo.RoomID + if len(roomID) == 0 { + roomID = room.ID + } + replyToEvt, _ := matrix.GetEvent(room, evt.Content.GetReplyTo()) + if replyToEvt != nil { + replyToEvt.Content.RemoveReplyFallback() + if len(replyToEvt.Content.FormattedBody) == 0 { + replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body) + } + evt.Content.FormattedBody = fmt.Sprintf( + "In reply to %[1]s
%[2]s


%[3]s", + replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody) + } else { + evt.Content.FormattedBody = fmt.Sprintf( + "In reply to unknown event https://matrix.to/#/%[1]s/%[2]s
%[3]s", + roomID, evt.Content.GetReplyTo(), evt.Content.FormattedBody) + } + } + ts := unixToTime(evt.Timestamp) + switch evt.Content.MsgType { + case "m.text", "m.notice", "m.emote": + if evt.Content.Format == mautrix.FormatHTML { + return NewHTMLMessage(evt, displayname, htmlp.Parse(room, evt, displayname)) + } + evt.Content.Body = strings.Replace(evt.Content.Body, "\t", " ", -1) + return NewTextMessage(evt.ID, evt.Sender, displayname, evt.Content.MsgType, evt.Content.Body, ts) + case "m.image": + data, hs, id, err := matrix.Download(evt.Content.URL) + if err != nil { + debug.Printf("Failed to download %s: %v", evt.Content.URL, err) + } + return NewImageMessage(matrix, evt.ID, evt.Sender, displayname, evt.Content.MsgType, evt.Content.Body, hs, id, data, ts) + } + return nil +} + +func getMembershipChangeMessage(evt *mautrix.Event, membership, prevMembership mautrix.Membership, senderDisplayname, displayname, prevDisplayname string) (sender string, text tstring.TString) { + switch membership { + case "invite": + sender = "---" + text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", senderDisplayname, displayname), tcell.ColorGreen) + text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) + text.Colorize(len(senderDisplayname)+len(" invited "), len(displayname), widget.GetHashColor(*evt.StateKey)) + case "join": + sender = "-->" + text = tstring.NewColorTString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen) + text.Colorize(0, len(displayname), widget.GetHashColor(*evt.StateKey)) + case "leave": + sender = "<--" + if evt.Sender != *evt.StateKey { + if prevMembership == mautrix.MembershipBan { + text = tstring.NewColorTString(fmt.Sprintf("%s unbanned %s", senderDisplayname, displayname), tcell.ColorGreen) + text.Colorize(len(senderDisplayname)+len(" unbanned "), len(displayname), widget.GetHashColor(*evt.StateKey)) + } else { + text = tstring.NewColorTString(fmt.Sprintf("%s kicked %s: %s", senderDisplayname, displayname, evt.Content.Reason), tcell.ColorRed) + text.Colorize(len(senderDisplayname)+len(" kicked "), len(displayname), widget.GetHashColor(*evt.StateKey)) + } + text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) + } else { + if displayname == *evt.StateKey { + displayname = prevDisplayname + } + text = tstring.NewColorTString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed) + text.Colorize(0, len(displayname), widget.GetHashColor(*evt.StateKey)) + } + case "ban": + text = tstring.NewColorTString(fmt.Sprintf("%s banned %s: %s", senderDisplayname, displayname, evt.Content.Reason), tcell.ColorRed) + text.Colorize(len(senderDisplayname)+len(" banned "), len(displayname), widget.GetHashColor(*evt.StateKey)) + text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) + } + return +} + +func getMembershipEventContent(room *rooms.Room, evt *mautrix.Event) (sender string, text tstring.TString) { + member := room.GetMember(evt.Sender) + senderDisplayname := evt.Sender + if member != nil { + senderDisplayname = member.Displayname + } + + membership := evt.Content.Membership + displayname := evt.Content.Displayname + if len(displayname) == 0 { + displayname = *evt.StateKey + } + + prevMembership := mautrix.MembershipLeave + prevDisplayname := *evt.StateKey + if evt.Unsigned.PrevContent != nil { + prevMembership = evt.Unsigned.PrevContent.Membership + prevDisplayname = evt.Unsigned.PrevContent.Displayname + if len(prevDisplayname) == 0 { + prevDisplayname = *evt.StateKey + } + } + + if membership != prevMembership { + sender, text = getMembershipChangeMessage(evt, membership, prevMembership, senderDisplayname, displayname, prevDisplayname) + } else if displayname != prevDisplayname { + sender = "---" + color := widget.GetHashColor(*evt.StateKey) + text = tstring.NewBlankTString(). + AppendColor(prevDisplayname, color). + AppendColor(" changed their display name to ", tcell.ColorGreen). + AppendColor(displayname, color). + AppendColor(".", tcell.ColorGreen) + } + return +} + +func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) UIMessage { + displayname, text := getMembershipEventContent(room, evt) + if len(text) == 0 { + return nil + } + + ts := unixToTime(evt.Timestamp) + return NewExpandedTextMessage(evt.ID, evt.Sender, displayname, "m.room.member", text, ts) +} + +func ParseAliasEvent(evt *mautrix.Event, displayname string) tstring.TString { + var prevAliases []string + if evt.Unsigned.PrevContent != nil { + prevAliases = evt.Unsigned.PrevContent.Aliases + } + aliases := evt.Content.Aliases + var added, removed []tstring.TString +Outer1: + for _, oldAlias := range prevAliases { + for _, newAlias := range aliases { + if oldAlias == newAlias { + continue Outer1 + } + } + removed = append(removed, tstring.NewStyleTString(oldAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(oldAlias)).Underline(true))) + } +Outer2: + for _, newAlias := range aliases { + for _, oldAlias := range prevAliases { + if oldAlias == newAlias { + continue Outer2 + } + } + added = append(added, tstring.NewStyleTString(newAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(newAlias)).Underline(true))) + } + var addedStr, removedStr tstring.TString + if len(added) == 1 { + addedStr = added[0] + } else if len(added) > 1 { + addedStr = tstring. + Join(added[:len(added)-1], ", "). + Append(" and "). + AppendTString(added[len(added)-1]) + } + if len(removed) == 1 { + removedStr = removed[0] + } else if len(removed) > 1 { + removedStr = tstring. + Join(removed[:len(removed)-1], ", "). + Append(" and "). + AppendTString(removed[len(removed)-1]) + } + text := tstring.NewBlankTString() + if len(addedStr) > 0 && len(removedStr) > 0 { + text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). + AppendTString(addedStr). + AppendColor(" and removed ", tcell.ColorGreen). + AppendTString(removedStr). + AppendColor(" as addresses for this room.", tcell.ColorGreen) + } else if len(addedStr) > 0 { + text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). + AppendTString(addedStr). + AppendColor(" as addresses for this room.", tcell.ColorGreen) + } else if len(removedStr) > 0 { + text = text.AppendColor(fmt.Sprintf("%s removed ", displayname), tcell.ColorGreen). + AppendTString(removedStr). + AppendColor(" as addresses for this room.", tcell.ColorGreen) + } else { + return nil + } + return text +} diff --git a/ui/messages/parser/colormap.go b/ui/messages/parser/colormap.go deleted file mode 100644 index 19cdf06..0000000 --- a/ui/messages/parser/colormap.go +++ /dev/null @@ -1,156 +0,0 @@ -// From https://github.com/golang/image/blob/master/colornames/colornames.go -package parser - -import ( - "image/color" -) - -var ColorMap = map[string]color.RGBA{ - "aliceblue": {0xf0, 0xf8, 0xff, 0xff}, // rgb(240, 248, 255) - "antiquewhite": {0xfa, 0xeb, 0xd7, 0xff}, // rgb(250, 235, 215) - "aqua": {0x00, 0xff, 0xff, 0xff}, // rgb(0, 255, 255) - "aquamarine": {0x7f, 0xff, 0xd4, 0xff}, // rgb(127, 255, 212) - "azure": {0xf0, 0xff, 0xff, 0xff}, // rgb(240, 255, 255) - "beige": {0xf5, 0xf5, 0xdc, 0xff}, // rgb(245, 245, 220) - "bisque": {0xff, 0xe4, 0xc4, 0xff}, // rgb(255, 228, 196) - "black": {0x00, 0x00, 0x00, 0xff}, // rgb(0, 0, 0) - "blanchedalmond": {0xff, 0xeb, 0xcd, 0xff}, // rgb(255, 235, 205) - "blue": {0x00, 0x00, 0xff, 0xff}, // rgb(0, 0, 255) - "blueviolet": {0x8a, 0x2b, 0xe2, 0xff}, // rgb(138, 43, 226) - "brown": {0xa5, 0x2a, 0x2a, 0xff}, // rgb(165, 42, 42) - "burlywood": {0xde, 0xb8, 0x87, 0xff}, // rgb(222, 184, 135) - "cadetblue": {0x5f, 0x9e, 0xa0, 0xff}, // rgb(95, 158, 160) - "chartreuse": {0x7f, 0xff, 0x00, 0xff}, // rgb(127, 255, 0) - "chocolate": {0xd2, 0x69, 0x1e, 0xff}, // rgb(210, 105, 30) - "coral": {0xff, 0x7f, 0x50, 0xff}, // rgb(255, 127, 80) - "cornflowerblue": {0x64, 0x95, 0xed, 0xff}, // rgb(100, 149, 237) - "cornsilk": {0xff, 0xf8, 0xdc, 0xff}, // rgb(255, 248, 220) - "crimson": {0xdc, 0x14, 0x3c, 0xff}, // rgb(220, 20, 60) - "cyan": {0x00, 0xff, 0xff, 0xff}, // rgb(0, 255, 255) - "darkblue": {0x00, 0x00, 0x8b, 0xff}, // rgb(0, 0, 139) - "darkcyan": {0x00, 0x8b, 0x8b, 0xff}, // rgb(0, 139, 139) - "darkgoldenrod": {0xb8, 0x86, 0x0b, 0xff}, // rgb(184, 134, 11) - "darkgray": {0xa9, 0xa9, 0xa9, 0xff}, // rgb(169, 169, 169) - "darkgreen": {0x00, 0x64, 0x00, 0xff}, // rgb(0, 100, 0) - "darkgrey": {0xa9, 0xa9, 0xa9, 0xff}, // rgb(169, 169, 169) - "darkkhaki": {0xbd, 0xb7, 0x6b, 0xff}, // rgb(189, 183, 107) - "darkmagenta": {0x8b, 0x00, 0x8b, 0xff}, // rgb(139, 0, 139) - "darkolivegreen": {0x55, 0x6b, 0x2f, 0xff}, // rgb(85, 107, 47) - "darkorange": {0xff, 0x8c, 0x00, 0xff}, // rgb(255, 140, 0) - "darkorchid": {0x99, 0x32, 0xcc, 0xff}, // rgb(153, 50, 204) - "darkred": {0x8b, 0x00, 0x00, 0xff}, // rgb(139, 0, 0) - "darksalmon": {0xe9, 0x96, 0x7a, 0xff}, // rgb(233, 150, 122) - "darkseagreen": {0x8f, 0xbc, 0x8f, 0xff}, // rgb(143, 188, 143) - "darkslateblue": {0x48, 0x3d, 0x8b, 0xff}, // rgb(72, 61, 139) - "darkslategray": {0x2f, 0x4f, 0x4f, 0xff}, // rgb(47, 79, 79) - "darkslategrey": {0x2f, 0x4f, 0x4f, 0xff}, // rgb(47, 79, 79) - "darkturquoise": {0x00, 0xce, 0xd1, 0xff}, // rgb(0, 206, 209) - "darkviolet": {0x94, 0x00, 0xd3, 0xff}, // rgb(148, 0, 211) - "deeppink": {0xff, 0x14, 0x93, 0xff}, // rgb(255, 20, 147) - "deepskyblue": {0x00, 0xbf, 0xff, 0xff}, // rgb(0, 191, 255) - "dimgray": {0x69, 0x69, 0x69, 0xff}, // rgb(105, 105, 105) - "dimgrey": {0x69, 0x69, 0x69, 0xff}, // rgb(105, 105, 105) - "dodgerblue": {0x1e, 0x90, 0xff, 0xff}, // rgb(30, 144, 255) - "firebrick": {0xb2, 0x22, 0x22, 0xff}, // rgb(178, 34, 34) - "floralwhite": {0xff, 0xfa, 0xf0, 0xff}, // rgb(255, 250, 240) - "forestgreen": {0x22, 0x8b, 0x22, 0xff}, // rgb(34, 139, 34) - "fuchsia": {0xff, 0x00, 0xff, 0xff}, // rgb(255, 0, 255) - "gainsboro": {0xdc, 0xdc, 0xdc, 0xff}, // rgb(220, 220, 220) - "ghostwhite": {0xf8, 0xf8, 0xff, 0xff}, // rgb(248, 248, 255) - "gold": {0xff, 0xd7, 0x00, 0xff}, // rgb(255, 215, 0) - "goldenrod": {0xda, 0xa5, 0x20, 0xff}, // rgb(218, 165, 32) - "gray": {0x80, 0x80, 0x80, 0xff}, // rgb(128, 128, 128) - "green": {0x00, 0x80, 0x00, 0xff}, // rgb(0, 128, 0) - "greenyellow": {0xad, 0xff, 0x2f, 0xff}, // rgb(173, 255, 47) - "grey": {0x80, 0x80, 0x80, 0xff}, // rgb(128, 128, 128) - "honeydew": {0xf0, 0xff, 0xf0, 0xff}, // rgb(240, 255, 240) - "hotpink": {0xff, 0x69, 0xb4, 0xff}, // rgb(255, 105, 180) - "indianred": {0xcd, 0x5c, 0x5c, 0xff}, // rgb(205, 92, 92) - "indigo": {0x4b, 0x00, 0x82, 0xff}, // rgb(75, 0, 130) - "ivory": {0xff, 0xff, 0xf0, 0xff}, // rgb(255, 255, 240) - "khaki": {0xf0, 0xe6, 0x8c, 0xff}, // rgb(240, 230, 140) - "lavender": {0xe6, 0xe6, 0xfa, 0xff}, // rgb(230, 230, 250) - "lavenderblush": {0xff, 0xf0, 0xf5, 0xff}, // rgb(255, 240, 245) - "lawngreen": {0x7c, 0xfc, 0x00, 0xff}, // rgb(124, 252, 0) - "lemonchiffon": {0xff, 0xfa, 0xcd, 0xff}, // rgb(255, 250, 205) - "lightblue": {0xad, 0xd8, 0xe6, 0xff}, // rgb(173, 216, 230) - "lightcoral": {0xf0, 0x80, 0x80, 0xff}, // rgb(240, 128, 128) - "lightcyan": {0xe0, 0xff, 0xff, 0xff}, // rgb(224, 255, 255) - "lightgoldenrodyellow": {0xfa, 0xfa, 0xd2, 0xff}, // rgb(250, 250, 210) - "lightgray": {0xd3, 0xd3, 0xd3, 0xff}, // rgb(211, 211, 211) - "lightgreen": {0x90, 0xee, 0x90, 0xff}, // rgb(144, 238, 144) - "lightgrey": {0xd3, 0xd3, 0xd3, 0xff}, // rgb(211, 211, 211) - "lightpink": {0xff, 0xb6, 0xc1, 0xff}, // rgb(255, 182, 193) - "lightsalmon": {0xff, 0xa0, 0x7a, 0xff}, // rgb(255, 160, 122) - "lightseagreen": {0x20, 0xb2, 0xaa, 0xff}, // rgb(32, 178, 170) - "lightskyblue": {0x87, 0xce, 0xfa, 0xff}, // rgb(135, 206, 250) - "lightslategray": {0x77, 0x88, 0x99, 0xff}, // rgb(119, 136, 153) - "lightslategrey": {0x77, 0x88, 0x99, 0xff}, // rgb(119, 136, 153) - "lightsteelblue": {0xb0, 0xc4, 0xde, 0xff}, // rgb(176, 196, 222) - "lightyellow": {0xff, 0xff, 0xe0, 0xff}, // rgb(255, 255, 224) - "lime": {0x00, 0xff, 0x00, 0xff}, // rgb(0, 255, 0) - "limegreen": {0x32, 0xcd, 0x32, 0xff}, // rgb(50, 205, 50) - "linen": {0xfa, 0xf0, 0xe6, 0xff}, // rgb(250, 240, 230) - "magenta": {0xff, 0x00, 0xff, 0xff}, // rgb(255, 0, 255) - "maroon": {0x80, 0x00, 0x00, 0xff}, // rgb(128, 0, 0) - "mediumaquamarine": {0x66, 0xcd, 0xaa, 0xff}, // rgb(102, 205, 170) - "mediumblue": {0x00, 0x00, 0xcd, 0xff}, // rgb(0, 0, 205) - "mediumorchid": {0xba, 0x55, 0xd3, 0xff}, // rgb(186, 85, 211) - "mediumpurple": {0x93, 0x70, 0xdb, 0xff}, // rgb(147, 112, 219) - "mediumseagreen": {0x3c, 0xb3, 0x71, 0xff}, // rgb(60, 179, 113) - "mediumslateblue": {0x7b, 0x68, 0xee, 0xff}, // rgb(123, 104, 238) - "mediumspringgreen": {0x00, 0xfa, 0x9a, 0xff}, // rgb(0, 250, 154) - "mediumturquoise": {0x48, 0xd1, 0xcc, 0xff}, // rgb(72, 209, 204) - "mediumvioletred": {0xc7, 0x15, 0x85, 0xff}, // rgb(199, 21, 133) - "midnightblue": {0x19, 0x19, 0x70, 0xff}, // rgb(25, 25, 112) - "mintcream": {0xf5, 0xff, 0xfa, 0xff}, // rgb(245, 255, 250) - "mistyrose": {0xff, 0xe4, 0xe1, 0xff}, // rgb(255, 228, 225) - "moccasin": {0xff, 0xe4, 0xb5, 0xff}, // rgb(255, 228, 181) - "navajowhite": {0xff, 0xde, 0xad, 0xff}, // rgb(255, 222, 173) - "navy": {0x00, 0x00, 0x80, 0xff}, // rgb(0, 0, 128) - "oldlace": {0xfd, 0xf5, 0xe6, 0xff}, // rgb(253, 245, 230) - "olive": {0x80, 0x80, 0x00, 0xff}, // rgb(128, 128, 0) - "olivedrab": {0x6b, 0x8e, 0x23, 0xff}, // rgb(107, 142, 35) - "orange": {0xff, 0xa5, 0x00, 0xff}, // rgb(255, 165, 0) - "orangered": {0xff, 0x45, 0x00, 0xff}, // rgb(255, 69, 0) - "orchid": {0xda, 0x70, 0xd6, 0xff}, // rgb(218, 112, 214) - "palegoldenrod": {0xee, 0xe8, 0xaa, 0xff}, // rgb(238, 232, 170) - "palegreen": {0x98, 0xfb, 0x98, 0xff}, // rgb(152, 251, 152) - "paleturquoise": {0xaf, 0xee, 0xee, 0xff}, // rgb(175, 238, 238) - "palevioletred": {0xdb, 0x70, 0x93, 0xff}, // rgb(219, 112, 147) - "papayawhip": {0xff, 0xef, 0xd5, 0xff}, // rgb(255, 239, 213) - "peachpuff": {0xff, 0xda, 0xb9, 0xff}, // rgb(255, 218, 185) - "peru": {0xcd, 0x85, 0x3f, 0xff}, // rgb(205, 133, 63) - "pink": {0xff, 0xc0, 0xcb, 0xff}, // rgb(255, 192, 203) - "plum": {0xdd, 0xa0, 0xdd, 0xff}, // rgb(221, 160, 221) - "powderblue": {0xb0, 0xe0, 0xe6, 0xff}, // rgb(176, 224, 230) - "purple": {0x80, 0x00, 0x80, 0xff}, // rgb(128, 0, 128) - "red": {0xff, 0x00, 0x00, 0xff}, // rgb(255, 0, 0) - "rosybrown": {0xbc, 0x8f, 0x8f, 0xff}, // rgb(188, 143, 143) - "royalblue": {0x41, 0x69, 0xe1, 0xff}, // rgb(65, 105, 225) - "saddlebrown": {0x8b, 0x45, 0x13, 0xff}, // rgb(139, 69, 19) - "salmon": {0xfa, 0x80, 0x72, 0xff}, // rgb(250, 128, 114) - "sandybrown": {0xf4, 0xa4, 0x60, 0xff}, // rgb(244, 164, 96) - "seagreen": {0x2e, 0x8b, 0x57, 0xff}, // rgb(46, 139, 87) - "seashell": {0xff, 0xf5, 0xee, 0xff}, // rgb(255, 245, 238) - "sienna": {0xa0, 0x52, 0x2d, 0xff}, // rgb(160, 82, 45) - "silver": {0xc0, 0xc0, 0xc0, 0xff}, // rgb(192, 192, 192) - "skyblue": {0x87, 0xce, 0xeb, 0xff}, // rgb(135, 206, 235) - "slateblue": {0x6a, 0x5a, 0xcd, 0xff}, // rgb(106, 90, 205) - "slategray": {0x70, 0x80, 0x90, 0xff}, // rgb(112, 128, 144) - "slategrey": {0x70, 0x80, 0x90, 0xff}, // rgb(112, 128, 144) - "snow": {0xff, 0xfa, 0xfa, 0xff}, // rgb(255, 250, 250) - "springgreen": {0x00, 0xff, 0x7f, 0xff}, // rgb(0, 255, 127) - "steelblue": {0x46, 0x82, 0xb4, 0xff}, // rgb(70, 130, 180) - "tan": {0xd2, 0xb4, 0x8c, 0xff}, // rgb(210, 180, 140) - "teal": {0x00, 0x80, 0x80, 0xff}, // rgb(0, 128, 128) - "thistle": {0xd8, 0xbf, 0xd8, 0xff}, // rgb(216, 191, 216) - "tomato": {0xff, 0x63, 0x47, 0xff}, // rgb(255, 99, 71) - "turquoise": {0x40, 0xe0, 0xd0, 0xff}, // rgb(64, 224, 208) - "violet": {0xee, 0x82, 0xee, 0xff}, // rgb(238, 130, 238) - "wheat": {0xf5, 0xde, 0xb3, 0xff}, // rgb(245, 222, 179) - "white": {0xff, 0xff, 0xff, 0xff}, // rgb(255, 255, 255) - "whitesmoke": {0xf5, 0xf5, 0xf5, 0xff}, // rgb(245, 245, 245) - "yellow": {0xff, 0xff, 0x00, 0xff}, // rgb(255, 255, 0) - "yellowgreen": {0x9a, 0xcd, 0x32, 0xff}, // rgb(154, 205, 50) -} diff --git a/ui/messages/parser/doc.go b/ui/messages/parser/doc.go deleted file mode 100644 index 8f91a1d..0000000 --- a/ui/messages/parser/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package parser contains the functions for parsing Matrix events into UIMessage objects. -package parser diff --git a/ui/messages/parser/htmlparser.go b/ui/messages/parser/htmlparser.go deleted file mode 100644 index 728a201..0000000 --- a/ui/messages/parser/htmlparser.go +++ /dev/null @@ -1,376 +0,0 @@ -// 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 . - -package parser - -import ( - "regexp" - "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" - - "maunium.net/go/mautrix" - "maunium.net/go/tcell" - - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/gomuks/ui/messages" - "maunium.net/go/gomuks/ui/widget" -) - -var matrixToURL = regexp.MustCompile("^(?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!].*)") - -type htmlParser struct { - room *rooms.Room - - keepLinebreak bool -} - -func AdjustStyleBold(style tcell.Style) tcell.Style { - return style.Bold(true) -} - -func AdjustStyleItalic(style tcell.Style) tcell.Style { - return style.Italic(true) -} - -func AdjustStyleUnderline(style tcell.Style) tcell.Style { - return style.Underline(true) -} - -func AdjustStyleStrikethrough(style tcell.Style) tcell.Style { - return style.Strikethrough(true) -} - -func AdjustStyleTextColor(color tcell.Color) func(tcell.Style) tcell.Style { - return func(style tcell.Style) tcell.Style { - return style.Foreground(color) - } -} - -func AdjustStyleBackgroundColor(color tcell.Color) func(tcell.Style) tcell.Style { - return func(style tcell.Style) tcell.Style { - return style.Background(color) - } -} - -func (parser *htmlParser) getAttribute(node *html.Node, attribute string) string { - for _, attr := range node.Attr { - if attr.Key == attribute { - return attr.Val - } - } - return "" -} - -func (parser *htmlParser) listToEntity(node *html.Node) messages.HTMLEntity { - children := parser.nodeToEntities(node.FirstChild) - ordered := node.Data == "ol" - start := 1 - if ordered { - if startRaw := parser.getAttribute(node, "start"); len(startRaw) > 0 { - var err error - start, err = strconv.Atoi(startRaw) - if err != nil { - start = 1 - } - } - } - listItems := children[:0] - for _, child := range children { - if child.GetTag() == "li" { - listItems = append(listItems, child) - } - } - return messages.NewListEntity(ordered, start, listItems) -} - -func (parser *htmlParser) basicFormatToEntity(node *html.Node) messages.HTMLEntity { - entity := &messages.BaseHTMLEntity{ - Tag: node.Data, - Children: parser.nodeToEntities(node.FirstChild), - } - switch node.Data { - case "b", "strong": - entity.AdjustStyle(AdjustStyleBold) - case "i", "em": - entity.AdjustStyle(AdjustStyleItalic) - case "s", "del": - entity.AdjustStyle(AdjustStyleStrikethrough) - case "u", "ins": - 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 entity -} - -func (parser *htmlParser) parseColor(node *html.Node, mainName, altName string) (color tcell.Color, ok bool) { - hex := parser.getAttribute(node, mainName) - if len(hex) == 0 { - hex = parser.getAttribute(node, altName) - if len(hex) == 0 { - return - } - } - - cful, err := colorful.Hex(hex) - if err != nil { - color2, found := ColorMap[strings.ToLower(hex)] - if !found { - return - } - cful, _ = colorful.MakeColor(color2) - } - - r, g, b := cful.RGB255() - return tcell.NewRGBColor(int32(r), int32(g), int32(b)), true -} - -func (parser *htmlParser) headerToEntity(node *html.Node) messages.HTMLEntity { - length := int(node.Data[1] - '0') - prefix := strings.Repeat("#", length) + " " - return (&messages.BaseHTMLEntity{ - Tag: node.Data, - Text: prefix, - Children: parser.nodeToEntities(node.FirstChild), - }).AdjustStyle(AdjustStyleBold) -} - -func (parser *htmlParser) blockquoteToEntity(node *html.Node) messages.HTMLEntity { - return messages.NewBlockquoteEntity(parser.nodeToEntities(node.FirstChild)) -} - -func (parser *htmlParser) linkToEntity(node *html.Node) messages.HTMLEntity { - entity := &messages.BaseHTMLEntity{ - Tag: "a", - Children: parser.nodeToEntities(node.FirstChild), - } - href := parser.getAttribute(node, "href") - if len(href) == 0 { - return entity - } - match := matrixToURL.FindStringSubmatch(href) - if len(match) == 2 { - entity.Children = nil - pillTarget := match[1] - entity.Text = pillTarget - if pillTarget[0] == '@' { - if member := parser.room.GetMember(pillTarget); member != nil { - entity.Text = member.Displayname - entity.Style = entity.Style.Foreground(widget.GetHashColor(pillTarget)) - } - } - } - // TODO add click action and underline on hover for links - return entity -} - -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(strings.ToLower(language)) - if lexer == nil { - return nil - } - iter, err := lexer.Tokenise(nil, text) - if err != nil { - return nil - } - // TODO allow changing theme - 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.NewCodeBlockEntity(children, styleEntryToStyle(style.Get(chroma.Background))) -} - -func (parser *htmlParser) codeblockToEntity(node *html.Node) messages.HTMLEntity { - lang := "plaintext" - // TODO allow disabling syntax highlighting - if node.FirstChild.Type == html.ElementNode && node.FirstChild.Data == "code" { - node = node.FirstChild - attr := parser.getAttribute(node, "class") - for _, class := range strings.Split(attr, " ") { - if strings.HasPrefix(class, "language-") { - lang = class[len("language-"):] - break - } - } - } - parser.keepLinebreak = true - text := (&messages.BaseHTMLEntity{ - Children: parser.nodeToEntities(node.FirstChild), - }).PlainText() - parser.keepLinebreak = false - return parser.syntaxHighlight(text, lang) -} - -func (parser *htmlParser) tagNodeToEntity(node *html.Node) messages.HTMLEntity { - switch node.Data { - case "blockquote": - return parser.blockquoteToEntity(node) - case "ol", "ul": - return parser.listToEntity(node) - case "h1", "h2", "h3", "h4", "h5", "h6": - return parser.headerToEntity(node) - case "br": - return messages.NewBreakEntity() - case "b", "strong", "i", "em", "s", "del", "u", "ins", "font": - return parser.basicFormatToEntity(node) - case "a": - return parser.linkToEntity(node) - case "img": - return parser.imageToEntity(node) - case "pre": - return parser.codeblockToEntity(node) - default: - return &messages.BaseHTMLEntity{ - Tag: node.Data, - Children: parser.nodeToEntities(node.FirstChild), - Block: parser.isBlockTag(node.Data), - } - } -} - -func (parser *htmlParser) singleNodeToEntity(node *html.Node) messages.HTMLEntity { - switch node.Type { - case html.TextNode: - if !parser.keepLinebreak { - node.Data = strings.ReplaceAll(node.Data, "\n", "") - } - return &messages.BaseHTMLEntity{ - Tag: "text", - Text: node.Data, - } - case html.ElementNode: - return parser.tagNodeToEntity(node) - case html.DocumentNode: - if node.FirstChild.Data == "html" && node.FirstChild.NextSibling == nil { - return parser.singleNodeToEntity(node.FirstChild) - } - return &messages.BaseHTMLEntity{ - Tag: "html", - Children: parser.nodeToEntities(node.FirstChild), - Block: true, - } - default: - return nil - } -} - -func (parser *htmlParser) nodeToEntities(node *html.Node) (entities []messages.HTMLEntity) { - for ; node != nil; node = node.NextSibling { - if entity := parser.singleNodeToEntity(node); entity != nil { - entities = append(entities, entity) - } - } - return -} - -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 { - for _, blockTag := range BlockTags { - if tag == blockTag { - return true - } - } - return false -} - -func (parser *htmlParser) Parse(htmlData string) messages.HTMLEntity { - node, _ := html.Parse(strings.NewReader(htmlData)) - return parser.singleNodeToEntity(node) -} - -// ParseHTMLMessage parses a HTML-formatted Matrix event into a UIMessage. -func ParseHTMLMessage(room *rooms.Room, evt *mautrix.Event, senderDisplayname string) messages.HTMLEntity { - htmlData := evt.Content.FormattedBody - htmlData = strings.Replace(htmlData, "\t", " ", -1) - - parser := htmlParser{room: room} - root := parser.Parse(htmlData) - root.(*messages.BaseHTMLEntity).Block = false - - if evt.Content.MsgType == mautrix.MsgEmote { - root = &messages.BaseHTMLEntity{ - Tag: "emote", - Children: []messages.HTMLEntity{ - messages.NewHTMLTextEntity("* "), - messages.NewHTMLTextEntity(senderDisplayname).AdjustStyle(AdjustStyleTextColor(widget.GetHashColor(evt.Sender))), - messages.NewHTMLTextEntity(" "), - root, - }, - } - } - - return root -} diff --git a/ui/messages/parser/parser.go b/ui/messages/parser/parser.go deleted file mode 100644 index e48bd5f..0000000 --- a/ui/messages/parser/parser.go +++ /dev/null @@ -1,287 +0,0 @@ -// 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 . - -package parser - -import ( - "fmt" - "html" - "strings" - "time" - - "maunium.net/go/gomuks/debug" - "maunium.net/go/gomuks/interface" - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/gomuks/ui/messages" - "maunium.net/go/gomuks/ui/messages/tstring" - "maunium.net/go/gomuks/ui/widget" - "maunium.net/go/mautrix" - "maunium.net/go/tcell" -) - -func ParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) messages.UIMessage { - switch evt.Type { - case mautrix.EventSticker: - evt.Content.MsgType = mautrix.MsgImage - fallthrough - case mautrix.EventMessage: - return ParseMessage(matrix, room, evt) - case mautrix.StateTopic, mautrix.StateRoomName, mautrix.StateAliases, mautrix.StateCanonicalAlias: - return ParseStateEvent(matrix, room, evt) - case mautrix.StateMember: - return ParseMembershipEvent(room, evt) - } - return nil -} - -func unixToTime(unix int64) time.Time { - timestamp := time.Now() - if unix != 0 { - timestamp = time.Unix(unix/1000, unix%1000*1000) - } - return timestamp -} - -func ParseStateEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) messages.UIMessage { - displayname := evt.Sender - member := room.GetMember(evt.Sender) - if member != nil { - displayname = member.Displayname - } - text := tstring.NewColorTString(displayname, widget.GetHashColor(evt.Sender)) - switch evt.Type { - case mautrix.StateTopic: - if len(evt.Content.Topic) == 0 { - text = text.AppendColor(" removed the topic.", tcell.ColorGreen) - } else { - text = text.AppendColor(" changed the topic to ", tcell.ColorGreen). - AppendStyle(evt.Content.Topic, tcell.StyleDefault.Underline(true)). - AppendColor(".", tcell.ColorGreen) - } - case mautrix.StateRoomName: - if len(evt.Content.Name) == 0 { - text = text.AppendColor(" removed the room name.", tcell.ColorGreen) - } else { - text = text.AppendColor(" changed the room name to ", tcell.ColorGreen). - AppendStyle(evt.Content.Name, tcell.StyleDefault.Underline(true)). - AppendColor(".", tcell.ColorGreen) - } - case mautrix.StateCanonicalAlias: - if len(evt.Content.Alias) == 0 { - text = text.AppendColor(" removed the main address of the room.", tcell.ColorGreen) - } else { - text = text.AppendColor(" changed the main address of the room to ", tcell.ColorGreen). - AppendStyle(evt.Content.Alias, tcell.StyleDefault.Underline(true)). - AppendColor(".", tcell.ColorGreen) - } - case mautrix.StateAliases: - text = ParseAliasEvent(evt, displayname) - } - ts := unixToTime(evt.Timestamp) - return messages.NewExpandedTextMessage(evt.ID, evt.Sender, displayname, mautrix.MessageType(evt.Type.Type), text, ts) -} - -func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) messages.UIMessage { - displayname := evt.Sender - member := room.GetMember(evt.Sender) - if member != nil { - displayname = member.Displayname - } - if len(evt.Content.GetReplyTo()) > 0 { - evt.Content.RemoveReplyFallback() - roomID := evt.Content.RelatesTo.InReplyTo.RoomID - if len(roomID) == 0 { - roomID = room.ID - } - replyToEvt, _ := matrix.GetEvent(room, evt.Content.GetReplyTo()) - if replyToEvt != nil { - replyToEvt.Content.RemoveReplyFallback() - if len(replyToEvt.Content.FormattedBody) == 0 { - replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body) - } - evt.Content.FormattedBody = fmt.Sprintf( - "In reply to %[1]s
%[2]s


%[3]s", - replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody) - } else { - evt.Content.FormattedBody = fmt.Sprintf( - "In reply to unknown event https://matrix.to/#/%[1]s/%[2]s
%[3]s", - roomID, evt.Content.GetReplyTo(), evt.Content.FormattedBody) - } - } - ts := unixToTime(evt.Timestamp) - switch evt.Content.MsgType { - case "m.text", "m.notice", "m.emote": - if evt.Content.Format == mautrix.FormatHTML { - return messages.NewHTMLMessage(evt.ID, evt.Sender, displayname, evt.Content.MsgType, ParseHTMLMessage(room, evt, displayname), ts) - } - 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) - case "m.image": - data, hs, id, err := matrix.Download(evt.Content.URL) - if err != nil { - debug.Printf("Failed to download %s: %v", evt.Content.URL, err) - } - return messages.NewImageMessage(matrix, evt.ID, evt.Sender, displayname, evt.Content.MsgType, evt.Content.Body, hs, id, data, ts) - } - return nil -} - -func getMembershipChangeMessage(evt *mautrix.Event, membership, prevMembership mautrix.Membership, senderDisplayname, displayname, prevDisplayname string) (sender string, text tstring.TString) { - switch membership { - case "invite": - sender = "---" - text = tstring.NewColorTString(fmt.Sprintf("%s invited %s.", senderDisplayname, displayname), tcell.ColorGreen) - text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) - text.Colorize(len(senderDisplayname)+len(" invited "), len(displayname), widget.GetHashColor(*evt.StateKey)) - case "join": - sender = "-->" - text = tstring.NewColorTString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen) - text.Colorize(0, len(displayname), widget.GetHashColor(*evt.StateKey)) - case "leave": - sender = "<--" - if evt.Sender != *evt.StateKey { - if prevMembership == mautrix.MembershipBan { - text = tstring.NewColorTString(fmt.Sprintf("%s unbanned %s", senderDisplayname, displayname), tcell.ColorGreen) - text.Colorize(len(senderDisplayname)+len(" unbanned "), len(displayname), widget.GetHashColor(*evt.StateKey)) - } else { - text = tstring.NewColorTString(fmt.Sprintf("%s kicked %s: %s", senderDisplayname, displayname, evt.Content.Reason), tcell.ColorRed) - text.Colorize(len(senderDisplayname)+len(" kicked "), len(displayname), widget.GetHashColor(*evt.StateKey)) - } - text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) - } else { - if displayname == *evt.StateKey { - displayname = prevDisplayname - } - text = tstring.NewColorTString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed) - text.Colorize(0, len(displayname), widget.GetHashColor(*evt.StateKey)) - } - case "ban": - text = tstring.NewColorTString(fmt.Sprintf("%s banned %s: %s", senderDisplayname, displayname, evt.Content.Reason), tcell.ColorRed) - text.Colorize(len(senderDisplayname)+len(" banned "), len(displayname), widget.GetHashColor(*evt.StateKey)) - text.Colorize(0, len(senderDisplayname), widget.GetHashColor(evt.Sender)) - } - return -} - -func getMembershipEventContent(room *rooms.Room, evt *mautrix.Event) (sender string, text tstring.TString) { - member := room.GetMember(evt.Sender) - senderDisplayname := evt.Sender - if member != nil { - senderDisplayname = member.Displayname - } - - membership := evt.Content.Membership - displayname := evt.Content.Displayname - if len(displayname) == 0 { - displayname = *evt.StateKey - } - - prevMembership := mautrix.MembershipLeave - prevDisplayname := *evt.StateKey - if evt.Unsigned.PrevContent != nil { - prevMembership = evt.Unsigned.PrevContent.Membership - prevDisplayname = evt.Unsigned.PrevContent.Displayname - if len(prevDisplayname) == 0 { - prevDisplayname = *evt.StateKey - } - } - - if membership != prevMembership { - sender, text = getMembershipChangeMessage(evt, membership, prevMembership, senderDisplayname, displayname, prevDisplayname) - } else if displayname != prevDisplayname { - sender = "---" - color := widget.GetHashColor(*evt.StateKey) - text = tstring.NewBlankTString(). - AppendColor(prevDisplayname, color). - AppendColor(" changed their display name to ", tcell.ColorGreen). - AppendColor(displayname, color). - AppendColor(".", tcell.ColorGreen) - } - return -} - -func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) messages.UIMessage { - displayname, text := getMembershipEventContent(room, evt) - if len(text) == 0 { - return nil - } - - ts := unixToTime(evt.Timestamp) - return messages.NewExpandedTextMessage(evt.ID, evt.Sender, displayname, "m.room.member", text, ts) -} - -func ParseAliasEvent(evt *mautrix.Event, displayname string) tstring.TString { - var prevAliases []string - if evt.Unsigned.PrevContent != nil { - prevAliases = evt.Unsigned.PrevContent.Aliases - } - aliases := evt.Content.Aliases - var added, removed []tstring.TString -Outer1: - for _, oldAlias := range prevAliases { - for _, newAlias := range aliases { - if oldAlias == newAlias { - continue Outer1 - } - } - removed = append(removed, tstring.NewStyleTString(oldAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(oldAlias)).Underline(true))) - } -Outer2: - for _, newAlias := range aliases { - for _, oldAlias := range prevAliases { - if oldAlias == newAlias { - continue Outer2 - } - } - added = append(added, tstring.NewStyleTString(newAlias, tcell.StyleDefault.Foreground(widget.GetHashColor(newAlias)).Underline(true))) - } - var addedStr, removedStr tstring.TString - if len(added) == 1 { - addedStr = added[0] - } else if len(added) > 1 { - addedStr = tstring. - Join(added[:len(added)-1], ", "). - Append(" and "). - AppendTString(added[len(added)-1]) - } - if len(removed) == 1 { - removedStr = removed[0] - } else if len(removed) > 1 { - removedStr = tstring. - Join(removed[:len(removed)-1], ", "). - Append(" and "). - AppendTString(removed[len(removed)-1]) - } - text := tstring.NewBlankTString() - if len(addedStr) > 0 && len(removedStr) > 0 { - text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). - AppendTString(addedStr). - AppendColor(" and removed ", tcell.ColorGreen). - AppendTString(removedStr). - AppendColor(" as addresses for this room.", tcell.ColorGreen) - } else if len(addedStr) > 0 { - text = text.AppendColor(fmt.Sprintf("%s added ", displayname), tcell.ColorGreen). - AppendTString(addedStr). - AppendColor(" as addresses for this room.", tcell.ColorGreen) - } else if len(removedStr) > 0 { - text = text.AppendColor(fmt.Sprintf("%s removed ", displayname), tcell.ColorGreen). - AppendTString(removedStr). - AppendColor(" as addresses for this room.", tcell.ColorGreen) - } else { - return nil - } - return text -} diff --git a/ui/view-main.go b/ui/view-main.go index 382d9c8..ff46c79 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -35,7 +35,7 @@ import ( "maunium.net/go/gomuks/lib/notification" "maunium.net/go/gomuks/matrix/pushrules" "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/gomuks/ui/messages/parser" + "maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/widget" ) @@ -485,5 +485,5 @@ func (view *MainView) LoadHistory(room string) { } func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *mautrix.Event) ifc.Message { - return parser.ParseEvent(view.matrix, roomView.MxRoom(), evt) + return messages.ParseEvent(view.matrix, roomView.MxRoom(), evt) } -- cgit v1.2.3