termui/textRender.go

246 lines
7.1 KiB
Go
Raw Normal View History

2015-04-01 15:40:22 -06:00
package termui
import (
"regexp"
"strings"
)
// TextRenderer adds common methods for rendering a text on screeen.
type TextRenderer interface {
2015-04-05 13:36:41 -06:00
NormalizedText() string
Render(lastColor, background Attribute) RenderedSequence
2015-04-03 08:02:19 -06:00
RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence
2015-04-01 15:40:22 -06:00
}
2015-04-05 13:46:27 -06:00
// TextRendererFactory is factory for creating text renderers.
type TextRendererFactory interface {
TextRenderer(text string) TextRenderer
2015-04-05 13:46:27 -06:00
}
2015-04-01 15:40:22 -06:00
// MarkdownRegex is used by MarkdownTextRenderer to determine how to format the
// text.
const MarkdownRegex = `(?:\[([^]]+)\])\(([a-z\s,]+)\)`
2015-04-01 15:40:22 -06:00
// unexported because a pattern can't be a constant and we don't want anyone
// messing with the regex.
var markdownPattern = regexp.MustCompile(MarkdownRegex)
// MarkdownTextRenderer is used for rendering the text with colors using
// markdown-like syntax.
// See: https://github.com/gizak/termui/issues/4#issuecomment-87270635
2015-04-03 08:02:19 -06:00
type MarkdownTextRenderer struct {
Text string
}
2015-04-01 15:40:22 -06:00
// NormalizedText returns the text the user will see (without colors).
// It strips out all formatting option and only preserves plain text.
2015-04-03 08:02:19 -06:00
func (r MarkdownTextRenderer) NormalizedText() string {
return r.normalizeText(r.Text)
}
func (r MarkdownTextRenderer) normalizeText(text string) string {
lText := strings.ToLower(text)
indexes := markdownPattern.FindAllStringSubmatchIndex(lText, -1)
// Interate through indexes in reverse order.
for i := len(indexes) - 1; i >= 0; i-- {
theIndex := indexes[i]
start, end := theIndex[0], theIndex[1]
contentStart, contentEnd := theIndex[2], theIndex[3]
text = text[:start] + text[contentStart:contentEnd] + text[end:]
}
return text
}
// Returns the position considering unicode characters.
func posUnicode(text string, pos int) int {
return len([]rune(text[:pos]))
}
/*
Render renders the sequence `text` using a markdown-like syntax:
`[hello](red) world` will become: `hello world` where hello is red.
You may also specify other attributes such as bold text:
`[foo](YELLOW, BOLD)` will become `foo` in yellow, bold text.
2015-04-01 15:40:22 -06:00
For all available combinations, colors, and attribute, see: `StringToAttribute`.
2015-04-01 15:40:22 -06:00
This method returns a RenderedSequence
*/
func (r MarkdownTextRenderer) Render(lastColor, background Attribute) RenderedSequence {
return r.RenderSequence(0, -1, lastColor, background)
}
// RenderSequence renders the text just like Render but the start and end can
// be specified.
2015-04-03 08:02:19 -06:00
func (r MarkdownTextRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence {
text := r.Text
if end == -1 {
end = len([]rune(r.NormalizedText()))
2015-04-03 08:02:19 -06:00
}
getMatch := func(s string) []int {
return markdownPattern.FindStringSubmatchIndex(strings.ToLower(s))
2015-04-01 15:40:22 -06:00
}
var sequences []ColorSubsequence
for match := getMatch(text); match != nil; match = getMatch(text) {
2015-04-03 08:02:19 -06:00
// Check if match is in the start/end range.
matchStart, matchEnd := match[0], match[1]
colorStart, colorEnd := match[4], match[5]
contentStart, contentEnd := match[2], match[3]
color := StringToAttribute(text[colorStart:colorEnd])
content := text[contentStart:contentEnd]
theSequence := ColorSubsequence{color, contentStart - 1, contentEnd - 1}
theSequence.Start = posUnicode(text, contentStart) - 1
theSequence.End = posUnicode(text, contentEnd) - 1
2015-04-03 08:02:19 -06:00
if start < theSequence.End && end > theSequence.Start {
// Make the sequence relative and append.
theSequence.Start -= start
if theSequence.Start < 0 {
theSequence.Start = 0
}
theSequence.End -= start
if theSequence.End < 0 {
theSequence.End = 0
} else if theSequence.End > end-start {
theSequence.End = end - start
}
sequences = append(sequences, theSequence)
}
text = text[:matchStart] + content + text[matchEnd:]
}
2015-04-03 08:02:19 -06:00
if end == -1 {
end = len(text)
}
runes := []rune(text)[start:end]
return RenderedSequence{string(runes), lastColor, background, sequences, nil}
2015-04-01 15:40:22 -06:00
}
2015-04-05 13:46:27 -06:00
// MarkdownTextRendererFactory is a TextRendererFactory for
// the MarkdownTextRenderer.
type MarkdownTextRendererFactory struct{}
// TextRenderer returns a MarkdownTextRenderer instance.
func (f MarkdownTextRendererFactory) TextRenderer(text string) TextRenderer {
2015-04-05 13:46:27 -06:00
return MarkdownTextRenderer{text}
}
// RenderedSequence is a string sequence that is capable of returning the
2015-04-01 15:40:22 -06:00
// Buffer used by termui for displaying the colorful string.
type RenderedSequence struct {
2015-04-01 15:40:22 -06:00
NormalizedText string
LastColor Attribute
BackgroundColor Attribute
Sequences []ColorSubsequence
// Use the color() method for getting the correct value.
mapSequences map[int]Attribute
}
2015-04-01 15:40:22 -06:00
// A ColorSubsequence represents a color for the given text span.
type ColorSubsequence struct {
Color Attribute
Start int
End int
2015-04-01 15:40:22 -06:00
}
2015-04-03 08:02:19 -06:00
// ColorSubsequencesToMap creates a map with all colors that from the
// subsequences.
func ColorSubsequencesToMap(sequences []ColorSubsequence) map[int]Attribute {
result := make(map[int]Attribute)
for _, theSequence := range sequences {
for i := theSequence.Start; i < theSequence.End; i++ {
result[i] = theSequence.Color
}
}
return result
}
func (s *RenderedSequence) colors() map[int]Attribute {
if s.mapSequences == nil {
s.mapSequences = ColorSubsequencesToMap(s.Sequences)
}
return s.mapSequences
}
2015-04-01 15:40:22 -06:00
// Buffer returns the colorful formatted buffer and the last color that was
// used.
func (s *RenderedSequence) Buffer(x, y int) ([]Point, Attribute) {
2015-04-03 08:02:19 -06:00
buffer := make([]Point, 0, len(s.NormalizedText)) // This is just an assumtion
for i := range []rune(s.NormalizedText) {
p, width := s.PointAt(i, x, y)
2015-04-03 08:02:19 -06:00
buffer = append(buffer, p)
x += width
2015-04-03 08:02:19 -06:00
}
return buffer, s.LastColor
2015-04-01 15:40:22 -06:00
}
// PointAt returns the point at the position of n. The x, and y coordinates
// are used for placing the point at the right position.
// Since some charaters are wider (like `一`), this method also returns the
// width the point actually took.
// This is important for increasing the x property properly.
func (s *RenderedSequence) PointAt(n, x, y int) (Point, int) {
color, ok := s.colors()[n]
if !ok {
color = s.LastColor
}
char := []rune(s.NormalizedText)[n]
return Point{char, s.BackgroundColor, color, x, y}, charWidth(char)
}
2015-04-05 13:36:41 -06:00
// A PlainRenderer does not render the text at all.
type PlainRenderer struct {
2015-04-05 13:36:41 -06:00
Text string
}
// NormalizedText returns the text given in
func (r PlainRenderer) NormalizedText() string {
2015-04-05 13:36:41 -06:00
return r.Text
}
// RenderSequence returns a RenderedSequence that does not have any color
// sequences.
func (r PlainRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence {
2015-04-05 13:36:41 -06:00
runes := []rune(r.Text)
if end < 0 {
end = len(runes)
}
runes = runes[start:end]
var s []ColorSubsequence
return RenderedSequence{string(runes), lastColor, background, s, nil}
}
// Render just like RenderSequence
func (r PlainRenderer) Render(lastColor, background Attribute) RenderedSequence {
2015-04-05 13:36:41 -06:00
return r.RenderSequence(0, -1, lastColor, background)
}
2015-04-05 13:46:27 -06:00
// PlainRendererFactory is a TextRendererFactory for
// the PlainRenderer.
type PlainRendererFactory struct{}
2015-04-05 13:46:27 -06:00
// TextRenderer returns a PlainRenderer instance.
func (f PlainRendererFactory) TextRenderer(text string) TextRenderer {
return PlainRenderer{text}
2015-04-05 13:46:27 -06:00
}