Split container/text HTML entities and add support for <hr>
This commit is contained in:
		
							
								
								
									
										95
									
								
								ui/messages/html/base.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								ui/messages/html/base.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
			
		||||
// 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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
package html
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
	"maunium.net/go/tcell"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BaseEntity struct {
 | 
			
		||||
	// The HTML tag of this entity.
 | 
			
		||||
	Tag string
 | 
			
		||||
	// Style for this entity.
 | 
			
		||||
	Style tcell.Style
 | 
			
		||||
	// Whether or not this is a block-type entity.
 | 
			
		||||
	Block bool
 | 
			
		||||
	// Height to use for entity if both text and children are empty.
 | 
			
		||||
	DefaultHeight int
 | 
			
		||||
 | 
			
		||||
	prevWidth int
 | 
			
		||||
	startX    int
 | 
			
		||||
	height    int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AdjustStyle changes the style of this text entity.
 | 
			
		||||
func (be *BaseEntity) AdjustStyle(fn AdjustStyleFunc) Entity {
 | 
			
		||||
	be.Style = fn(be.Style)
 | 
			
		||||
	return be
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsBlock returns whether or not this is a block-type entity.
 | 
			
		||||
func (be *BaseEntity) IsBlock() bool {
 | 
			
		||||
	return be.Block
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTag returns the HTML tag of this entity.
 | 
			
		||||
func (be *BaseEntity) GetTag() string {
 | 
			
		||||
	return be.Tag
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Height returns the render height of this entity.
 | 
			
		||||
func (be *BaseEntity) Height() int {
 | 
			
		||||
	return be.height
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (be *BaseEntity) getStartX() int {
 | 
			
		||||
	return be.startX
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clone creates a copy of this base entity.
 | 
			
		||||
func (be *BaseEntity) Clone() Entity {
 | 
			
		||||
	return &BaseEntity{
 | 
			
		||||
		Tag:           be.Tag,
 | 
			
		||||
		Style:         be.Style,
 | 
			
		||||
		Block:         be.Block,
 | 
			
		||||
		DefaultHeight: be.DefaultHeight,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (be *BaseEntity) PlainText() string {
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// String returns a textual representation of this BaseEntity struct.
 | 
			
		||||
func (be *BaseEntity) String() string {
 | 
			
		||||
	return fmt.Sprintf(`&html.BaseEntity{Tag="%s", Style=%d, Block=%t, startX=%d, height=%d},`,
 | 
			
		||||
		be.Tag, be.Style, be.Block, be.startX, be.height)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CalculateBuffer prepares this entity for rendering with the given parameters.
 | 
			
		||||
func (be *BaseEntity) CalculateBuffer(width, startX int, bare bool) int {
 | 
			
		||||
	be.height = be.DefaultHeight
 | 
			
		||||
	be.startX = startX
 | 
			
		||||
	if be.Block {
 | 
			
		||||
		be.startX = 0
 | 
			
		||||
	}
 | 
			
		||||
	return be.startX
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (be *BaseEntity) Draw(screen mauview.Screen) {}
 | 
			
		||||
@@ -24,26 +24,28 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BlockquoteEntity struct {
 | 
			
		||||
	*BaseEntity
 | 
			
		||||
	*ContainerEntity
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const BlockQuoteChar = '>'
 | 
			
		||||
 | 
			
		||||
func NewBlockquoteEntity(children []Entity) *BlockquoteEntity {
 | 
			
		||||
	return &BlockquoteEntity{&BaseEntity{
 | 
			
		||||
		Tag:      "blockquote",
 | 
			
		||||
	return &BlockquoteEntity{&ContainerEntity{
 | 
			
		||||
		BaseEntity: &BaseEntity{
 | 
			
		||||
			Tag:   "blockquote",
 | 
			
		||||
			Block: true,
 | 
			
		||||
		},
 | 
			
		||||
		Children: children,
 | 
			
		||||
		Block:    true,
 | 
			
		||||
		Indent:   2,
 | 
			
		||||
	}}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (be *BlockquoteEntity) Clone() Entity {
 | 
			
		||||
	return &BlockquoteEntity{BaseEntity: be.BaseEntity.Clone().(*BaseEntity)}
 | 
			
		||||
	return &BlockquoteEntity{ContainerEntity: be.ContainerEntity.Clone().(*ContainerEntity)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (be *BlockquoteEntity) Draw(screen mauview.Screen) {
 | 
			
		||||
	be.BaseEntity.Draw(screen)
 | 
			
		||||
	be.ContainerEntity.Draw(screen)
 | 
			
		||||
	for y := 0; y < be.height; y++ {
 | 
			
		||||
		screen.SetContent(0, y, BlockQuoteChar, nil, be.Style)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,15 +22,17 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type CodeBlockEntity struct {
 | 
			
		||||
	*BaseEntity
 | 
			
		||||
	*ContainerEntity
 | 
			
		||||
	Background tcell.Style
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCodeBlockEntity(children []Entity, background tcell.Style) *CodeBlockEntity {
 | 
			
		||||
	return &CodeBlockEntity{
 | 
			
		||||
		BaseEntity: &BaseEntity{
 | 
			
		||||
			Tag:      "pre",
 | 
			
		||||
			Block:    true,
 | 
			
		||||
		ContainerEntity: &ContainerEntity{
 | 
			
		||||
			BaseEntity: &BaseEntity{
 | 
			
		||||
				Tag:   "pre",
 | 
			
		||||
				Block: true,
 | 
			
		||||
			},
 | 
			
		||||
			Children: children,
 | 
			
		||||
		},
 | 
			
		||||
		Background: background,
 | 
			
		||||
@@ -39,8 +41,8 @@ func NewCodeBlockEntity(children []Entity, background tcell.Style) *CodeBlockEnt
 | 
			
		||||
 | 
			
		||||
func (ce *CodeBlockEntity) Clone() Entity {
 | 
			
		||||
	return &CodeBlockEntity{
 | 
			
		||||
		BaseEntity: ce.BaseEntity.Clone().(*BaseEntity),
 | 
			
		||||
		Background: ce.Background,
 | 
			
		||||
		ContainerEntity: ce.ContainerEntity.Clone().(*ContainerEntity),
 | 
			
		||||
		Background:      ce.Background,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										144
									
								
								ui/messages/html/container.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								ui/messages/html/container.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
// 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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package html
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ContainerEntity struct {
 | 
			
		||||
	*BaseEntity
 | 
			
		||||
 | 
			
		||||
	// The children of this container entity.
 | 
			
		||||
	Children []Entity
 | 
			
		||||
	// Number of cells to indent children.
 | 
			
		||||
	Indent int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PlainText returns the plaintext content in this entity and all its children.
 | 
			
		||||
func (ce *ContainerEntity) PlainText() string {
 | 
			
		||||
	if len(ce.Children) == 0 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	var buf strings.Builder
 | 
			
		||||
	newlined := false
 | 
			
		||||
	for _, child := range ce.Children {
 | 
			
		||||
		text := child.PlainText()
 | 
			
		||||
		if !strings.HasPrefix(text, "\n") && child.IsBlock() && !newlined {
 | 
			
		||||
			buf.WriteRune('\n')
 | 
			
		||||
		}
 | 
			
		||||
		newlined = false
 | 
			
		||||
		buf.WriteString(text)
 | 
			
		||||
		if child.IsBlock() {
 | 
			
		||||
			if !strings.HasSuffix(text, "\n") {
 | 
			
		||||
				buf.WriteRune('\n')
 | 
			
		||||
			}
 | 
			
		||||
			newlined = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return strings.TrimSpace(buf.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AdjustStyle recursively changes the style of this entity and all its children.
 | 
			
		||||
func (ce *ContainerEntity) AdjustStyle(fn AdjustStyleFunc) Entity {
 | 
			
		||||
	for _, child := range ce.Children {
 | 
			
		||||
		child.AdjustStyle(fn)
 | 
			
		||||
	}
 | 
			
		||||
	ce.Style = fn(ce.Style)
 | 
			
		||||
	return ce
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// clone creates a deep copy of this base entity.
 | 
			
		||||
func (ce *ContainerEntity) Clone() Entity {
 | 
			
		||||
	children := make([]Entity, len(ce.Children))
 | 
			
		||||
	for i, child := range ce.Children {
 | 
			
		||||
		children[i] = child.Clone()
 | 
			
		||||
	}
 | 
			
		||||
	return &ContainerEntity{
 | 
			
		||||
		BaseEntity: ce.BaseEntity.Clone().(*BaseEntity),
 | 
			
		||||
		Children:   children,
 | 
			
		||||
		Indent:     ce.Indent,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// String returns a textual representation of this BaseEntity struct.
 | 
			
		||||
func (ce *ContainerEntity) String() string {
 | 
			
		||||
	if len(ce.Children) == 0 {
 | 
			
		||||
		return fmt.Sprintf(`&html.ContainerEntity{Base=%s, Indent=%d, Children=[]}`, ce.BaseEntity, ce.Indent)
 | 
			
		||||
	}
 | 
			
		||||
	var buf strings.Builder
 | 
			
		||||
	_, _ = fmt.Fprintf(&buf, `&html.ContainerEntity{Base=%s, Indent=%d, Children=[`, ce.BaseEntity, ce.Indent)
 | 
			
		||||
	for _, child := range ce.Children {
 | 
			
		||||
		buf.WriteString("\n    ")
 | 
			
		||||
		buf.WriteString(strings.Join(strings.Split(strings.TrimRight(child.String(), "\n"), "\n"), "\n    "))
 | 
			
		||||
	}
 | 
			
		||||
	buf.WriteString("\n]}\n,")
 | 
			
		||||
	return buf.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Draw draws this entity onto the given mauview Screen.
 | 
			
		||||
func (ce *ContainerEntity) Draw(screen mauview.Screen) {
 | 
			
		||||
	if len(ce.Children) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	width, _ := screen.Size()
 | 
			
		||||
	prevBreak := false
 | 
			
		||||
	proxyScreen := &mauview.ProxyScreen{Parent: screen, OffsetX: ce.Indent, Width: width - ce.Indent, Style: ce.Style}
 | 
			
		||||
	for i, entity := range ce.Children {
 | 
			
		||||
		if i != 0 && entity.getStartX() == 0 {
 | 
			
		||||
			proxyScreen.OffsetY++
 | 
			
		||||
		}
 | 
			
		||||
		proxyScreen.Height = entity.Height()
 | 
			
		||||
		entity.Draw(proxyScreen)
 | 
			
		||||
		proxyScreen.SetStyle(ce.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 (ce *ContainerEntity) CalculateBuffer(width, startX int, bare bool) int {
 | 
			
		||||
	ce.BaseEntity.CalculateBuffer(width, startX, bare)
 | 
			
		||||
	if len(ce.Children) > 0 {
 | 
			
		||||
		ce.height = 0
 | 
			
		||||
		childStartX := ce.startX
 | 
			
		||||
		prevBreak := false
 | 
			
		||||
		for _, entity := range ce.Children {
 | 
			
		||||
			if entity.IsBlock() || childStartX == 0 || ce.height == 0 {
 | 
			
		||||
				ce.height++
 | 
			
		||||
			}
 | 
			
		||||
			childStartX = entity.CalculateBuffer(width-ce.Indent, childStartX, bare)
 | 
			
		||||
			ce.height += entity.Height() - 1
 | 
			
		||||
			_, isBreak := entity.(*BreakEntity)
 | 
			
		||||
			if prevBreak && isBreak {
 | 
			
		||||
				ce.height++
 | 
			
		||||
			}
 | 
			
		||||
			prevBreak = isBreak
 | 
			
		||||
		}
 | 
			
		||||
		if !ce.Block {
 | 
			
		||||
			return childStartX
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ce.startX
 | 
			
		||||
}
 | 
			
		||||
@@ -17,13 +17,6 @@
 | 
			
		||||
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"
 | 
			
		||||
)
 | 
			
		||||
@@ -54,262 +47,3 @@ type Entity interface {
 | 
			
		||||
 | 
			
		||||
	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 {
 | 
			
		||||
		text := child.PlainText()
 | 
			
		||||
		if !strings.HasPrefix(text, "\n") && child.IsBlock() && !newlined {
 | 
			
		||||
			buf.WriteRune('\n')
 | 
			
		||||
		}
 | 
			
		||||
		newlined = false
 | 
			
		||||
		buf.WriteString(text)
 | 
			
		||||
		if child.IsBlock() {
 | 
			
		||||
			if !strings.HasSuffix(text, "\n") {
 | 
			
		||||
				buf.WriteRune('\n')
 | 
			
		||||
			}
 | 
			
		||||
			newlined = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return strings.TrimSpace(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] == ' '
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ListEntity struct {
 | 
			
		||||
	*BaseEntity
 | 
			
		||||
	*ContainerEntity
 | 
			
		||||
	Ordered bool
 | 
			
		||||
	Start   int
 | 
			
		||||
}
 | 
			
		||||
@@ -40,11 +40,13 @@ func digits(num int) int {
 | 
			
		||||
 | 
			
		||||
func NewListEntity(ordered bool, start int, children []Entity) *ListEntity {
 | 
			
		||||
	entity := &ListEntity{
 | 
			
		||||
		BaseEntity: &BaseEntity{
 | 
			
		||||
			Tag:      "ul",
 | 
			
		||||
		ContainerEntity: &ContainerEntity{
 | 
			
		||||
			BaseEntity: &BaseEntity{
 | 
			
		||||
				Tag:    "ul",
 | 
			
		||||
				Block:  true,
 | 
			
		||||
			},
 | 
			
		||||
			Indent: 2,
 | 
			
		||||
			Children: children,
 | 
			
		||||
			Block:    true,
 | 
			
		||||
			Indent:   2,
 | 
			
		||||
		},
 | 
			
		||||
		Ordered: ordered,
 | 
			
		||||
		Start:   start,
 | 
			
		||||
@@ -58,7 +60,7 @@ func NewListEntity(ordered bool, start int, children []Entity) *ListEntity {
 | 
			
		||||
 | 
			
		||||
func (le *ListEntity) Clone() Entity {
 | 
			
		||||
	return &ListEntity{
 | 
			
		||||
		BaseEntity: le.BaseEntity.Clone().(*BaseEntity),
 | 
			
		||||
		ContainerEntity: le.ContainerEntity.Clone().(*ContainerEntity),
 | 
			
		||||
		Ordered:    le.Ordered,
 | 
			
		||||
		Start:      le.Start,
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -102,8 +102,10 @@ func (parser *htmlParser) listToEntity(node *html.Node) Entity {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser *htmlParser) basicFormatToEntity(node *html.Node) Entity {
 | 
			
		||||
	entity := &BaseEntity{
 | 
			
		||||
		Tag:      node.Data,
 | 
			
		||||
	entity := &ContainerEntity{
 | 
			
		||||
		BaseEntity: &BaseEntity{
 | 
			
		||||
			Tag: node.Data,
 | 
			
		||||
		},
 | 
			
		||||
		Children: parser.nodeToEntities(node.FirstChild),
 | 
			
		||||
	}
 | 
			
		||||
	switch node.Data {
 | 
			
		||||
@@ -151,12 +153,14 @@ func (parser *htmlParser) parseColor(node *html.Node, mainName, altName string)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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),
 | 
			
		||||
	return (&ContainerEntity{
 | 
			
		||||
		BaseEntity: &BaseEntity{
 | 
			
		||||
			Tag: node.Data,
 | 
			
		||||
		},
 | 
			
		||||
		Children: append(
 | 
			
		||||
			[]Entity{NewTextEntity(strings.Repeat("#", int(node.Data[1]-'0')) + " ")},
 | 
			
		||||
			parser.nodeToEntities(node.FirstChild)...
 | 
			
		||||
		),
 | 
			
		||||
	}).AdjustStyle(AdjustStyleBold)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -164,29 +168,36 @@ 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",
 | 
			
		||||
func (parser *htmlParser) linkToEntity(node *html.Node) (entity Entity) {
 | 
			
		||||
	entity = &ContainerEntity{
 | 
			
		||||
		BaseEntity: &BaseEntity{
 | 
			
		||||
			Tag: "a",
 | 
			
		||||
		},
 | 
			
		||||
		Children: parser.nodeToEntities(node.FirstChild),
 | 
			
		||||
	}
 | 
			
		||||
	href := parser.getAttribute(node, "href")
 | 
			
		||||
	if len(href) == 0 {
 | 
			
		||||
		return entity
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	match := matrixToURL.FindStringSubmatch(href)
 | 
			
		||||
	if len(match) == 2 {
 | 
			
		||||
		entity.Children = nil
 | 
			
		||||
		pillTarget := match[1]
 | 
			
		||||
		entity.Text = pillTarget
 | 
			
		||||
		textEntity := &TextEntity{
 | 
			
		||||
			BaseEntity: &BaseEntity{
 | 
			
		||||
				Tag: "a",
 | 
			
		||||
			},
 | 
			
		||||
			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))
 | 
			
		||||
				textEntity.Text = member.Displayname
 | 
			
		||||
				textEntity.Style = textEntity.Style.Foreground(widget.GetHashColor(pillTarget))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		entity = textEntity
 | 
			
		||||
	}
 | 
			
		||||
	// TODO add click action and underline on hover for links
 | 
			
		||||
	return entity
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (parser *htmlParser) imageToEntity(node *html.Node) Entity {
 | 
			
		||||
@@ -197,8 +208,10 @@ func (parser *htmlParser) imageToEntity(node *html.Node) Entity {
 | 
			
		||||
			alt = "[inline image]"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	entity := &BaseEntity{
 | 
			
		||||
		Tag:  "img",
 | 
			
		||||
	entity := &TextEntity{
 | 
			
		||||
		BaseEntity: &BaseEntity{
 | 
			
		||||
			Tag: "img",
 | 
			
		||||
		},
 | 
			
		||||
		Text: alt,
 | 
			
		||||
	}
 | 
			
		||||
	// TODO add click action and underline on hover for inline images
 | 
			
		||||
@@ -239,12 +252,13 @@ func (parser *htmlParser) syntaxHighlight(text, language string) Entity {
 | 
			
		||||
		if token.Value == "\n" {
 | 
			
		||||
			children[i] = NewBreakEntity()
 | 
			
		||||
		} else {
 | 
			
		||||
			children[i] = &BaseEntity{
 | 
			
		||||
				Tag:   token.Type.String(),
 | 
			
		||||
				Text:  token.Value,
 | 
			
		||||
				Style: styleEntryToStyle(style.Get(token.Type)),
 | 
			
		||||
 | 
			
		||||
				DefaultHeight: 1,
 | 
			
		||||
			children[i] = &TextEntity{
 | 
			
		||||
				BaseEntity: &BaseEntity{
 | 
			
		||||
					Tag:           token.Type.String(),
 | 
			
		||||
					Style:         styleEntryToStyle(style.Get(token.Type)),
 | 
			
		||||
					DefaultHeight: 1,
 | 
			
		||||
				},
 | 
			
		||||
				Text: token.Value,
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -265,7 +279,7 @@ func (parser *htmlParser) codeblockToEntity(node *html.Node) Entity {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	parser.keepLinebreak = true
 | 
			
		||||
	text := (&BaseEntity{
 | 
			
		||||
	text := (&ContainerEntity{
 | 
			
		||||
		Children: parser.nodeToEntities(node.FirstChild),
 | 
			
		||||
	}).PlainText()
 | 
			
		||||
	parser.keepLinebreak = false
 | 
			
		||||
@@ -295,10 +309,12 @@ func (parser *htmlParser) tagNodeToEntity(node *html.Node) Entity {
 | 
			
		||||
	case "mx-reply":
 | 
			
		||||
		return nil
 | 
			
		||||
	default:
 | 
			
		||||
		return &BaseEntity{
 | 
			
		||||
			Tag:      node.Data,
 | 
			
		||||
		return &ContainerEntity{
 | 
			
		||||
			BaseEntity: &BaseEntity{
 | 
			
		||||
				Tag:   node.Data,
 | 
			
		||||
				Block: parser.isBlockTag(node.Data),
 | 
			
		||||
			},
 | 
			
		||||
			Children: parser.nodeToEntities(node.FirstChild),
 | 
			
		||||
			Block:    parser.isBlockTag(node.Data),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -312,20 +328,19 @@ func (parser *htmlParser) singleNodeToEntity(node *html.Node) Entity {
 | 
			
		||||
		if len(node.Data) == 0 {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		return &BaseEntity{
 | 
			
		||||
			Tag:  "text",
 | 
			
		||||
			Text: node.Data,
 | 
			
		||||
		}
 | 
			
		||||
		return NewTextEntity(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",
 | 
			
		||||
		return &ContainerEntity{
 | 
			
		||||
			BaseEntity: &BaseEntity{
 | 
			
		||||
				Tag:   "html",
 | 
			
		||||
				Block: true,
 | 
			
		||||
			},
 | 
			
		||||
			Children: parser.nodeToEntities(node.FirstChild),
 | 
			
		||||
			Block:    true,
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return nil
 | 
			
		||||
@@ -376,10 +391,10 @@ func Parse(room *rooms.Room, evt *mautrix.Event, senderDisplayname string) Entit
 | 
			
		||||
 | 
			
		||||
	parser := htmlParser{room: room}
 | 
			
		||||
	root := parser.Parse(htmlData)
 | 
			
		||||
	beRoot := root.(*BaseEntity)
 | 
			
		||||
	beRoot := root.(*ContainerEntity)
 | 
			
		||||
	beRoot.Block = false
 | 
			
		||||
	if len(beRoot.Children) > 0 {
 | 
			
		||||
		beChild, ok := beRoot.Children[0].(*BaseEntity)
 | 
			
		||||
		beChild, ok := beRoot.Children[0].(*ContainerEntity)
 | 
			
		||||
		if ok && beChild.Tag == "p" {
 | 
			
		||||
			// Hacky fix for m.emote
 | 
			
		||||
			beChild.Block = false
 | 
			
		||||
@@ -387,8 +402,10 @@ func Parse(room *rooms.Room, evt *mautrix.Event, senderDisplayname string) Entit
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if evt.Content.MsgType == mautrix.MsgEmote {
 | 
			
		||||
		root = &BaseEntity{
 | 
			
		||||
			Tag: "emote",
 | 
			
		||||
		root = &ContainerEntity{
 | 
			
		||||
			BaseEntity: &BaseEntity{
 | 
			
		||||
				Tag: "emote",
 | 
			
		||||
			},
 | 
			
		||||
			Children: []Entity{
 | 
			
		||||
				NewTextEntity("* "),
 | 
			
		||||
				NewTextEntity(senderDisplayname).AdjustStyle(AdjustStyleTextColor(widget.GetHashColor(evt.Sender))),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										142
									
								
								ui/messages/html/text.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								ui/messages/html/text.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
// 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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
package html
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"regexp"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattn/go-runewidth"
 | 
			
		||||
 | 
			
		||||
	"maunium.net/go/gomuks/ui/widget"
 | 
			
		||||
	"maunium.net/go/mauview"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TextEntity struct {
 | 
			
		||||
	*BaseEntity
 | 
			
		||||
	// Text in this entity.
 | 
			
		||||
	Text string
 | 
			
		||||
 | 
			
		||||
	buffer []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewTextEntity creates a new text-only Entity.
 | 
			
		||||
func NewTextEntity(text string) *TextEntity {
 | 
			
		||||
	return &TextEntity{
 | 
			
		||||
		BaseEntity: &BaseEntity{
 | 
			
		||||
			Tag: "text",
 | 
			
		||||
		},
 | 
			
		||||
		Text: text,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (te *TextEntity) Clone() Entity {
 | 
			
		||||
	return &TextEntity{
 | 
			
		||||
		BaseEntity: te.BaseEntity.Clone().(*BaseEntity),
 | 
			
		||||
		Text:       te.Text,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (te *TextEntity) PlainText() string {
 | 
			
		||||
	return te.Text
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (te *TextEntity) String() string {
 | 
			
		||||
	return fmt.Sprintf("&html.TextEntity{Text=%s, Base=%s},\n", te.Text, te.BaseEntity)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (te *TextEntity) Draw(screen mauview.Screen) {
 | 
			
		||||
	width, _ := screen.Size()
 | 
			
		||||
	x := te.startX
 | 
			
		||||
	for y, line := range te.buffer {
 | 
			
		||||
		widget.WriteLine(screen, mauview.AlignLeft, line, x, y, width, te.Style)
 | 
			
		||||
		x = 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (te *TextEntity) CalculateBuffer(width, startX int, bare bool) int {
 | 
			
		||||
	te.BaseEntity.CalculateBuffer(width, startX, bare)
 | 
			
		||||
	te.prevWidth = width
 | 
			
		||||
	if te.buffer == nil {
 | 
			
		||||
		te.buffer = []string{}
 | 
			
		||||
	}
 | 
			
		||||
	bufPtr := 0
 | 
			
		||||
	text := te.Text
 | 
			
		||||
	textStartX := te.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(te.buffer) {
 | 
			
		||||
				te.buffer[bufPtr] = ""
 | 
			
		||||
			} else {
 | 
			
		||||
				te.buffer = append(te.buffer, "")
 | 
			
		||||
			}
 | 
			
		||||
			bufPtr++
 | 
			
		||||
			textStartX = 0
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if bufPtr < len(te.buffer) {
 | 
			
		||||
			te.buffer[bufPtr] = extract
 | 
			
		||||
		} else {
 | 
			
		||||
			te.buffer = append(te.buffer, extract)
 | 
			
		||||
		}
 | 
			
		||||
		bufPtr++
 | 
			
		||||
		text = text[len(extract):]
 | 
			
		||||
		if len(text) == 0 {
 | 
			
		||||
			te.buffer = te.buffer[:bufPtr]
 | 
			
		||||
			te.height += len(te.buffer)
 | 
			
		||||
			// This entity is over, return the startX for the next entity
 | 
			
		||||
			if te.Block {
 | 
			
		||||
				// ...except if it's a block entity
 | 
			
		||||
				return 0
 | 
			
		||||
			}
 | 
			
		||||
			return textStartX + runewidth.StringWidth(extract)
 | 
			
		||||
		}
 | 
			
		||||
		textStartX = 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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] == ' '
 | 
			
		||||
}
 | 
			
		||||
@@ -54,7 +54,6 @@ func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.R
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if replyToMsg := getCachedEvent(mainView, replyToRoom.ID, evt.Content.GetReplyTo()); replyToMsg != nil {
 | 
			
		||||
			debug.Print("Cloning cached UIMessage", replyToMsg)
 | 
			
		||||
			replyToMsg = replyToMsg.Clone()
 | 
			
		||||
			replyToMsg.SetReplyTo(nil)
 | 
			
		||||
			msg.SetReplyTo(replyToMsg)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user