1
0
mirror of https://github.com/gizak/termui.git synced 2025-04-26 13:48:54 +08:00
termui/style_parser.go
2022-11-09 05:56:36 -08:00

215 lines
5.0 KiB
Go

// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"strings"
)
const (
tokenFg = "fg"
tokenBg = "bg"
tokenModifier = "mod"
tokenItemSeparator = ","
tokenValueSeparator = ":"
tokenBeginStyledText = '['
tokenEndStyledText = ']'
tokenBeginStyle = '('
tokenEndStyle = ')'
tokenStyleKey = "]("
)
type parserState uint
type StyleBlock struct {
Start int
End int
}
const (
parserStateDefault parserState = iota
parserStateStyleItems
parserStateStyledText
)
// StyleParserColorMap can be modified to add custom color parsing to text
var StyleParserColorMap = map[string]Color{
"red": ColorRed,
"blue": ColorBlue,
"black": ColorBlack,
"cyan": ColorCyan,
"yellow": ColorYellow,
"white": ColorWhite,
"clear": ColorClear,
"green": ColorGreen,
"magenta": ColorMagenta,
}
var modifierMap = map[string]Modifier{
"bold": ModifierBold,
"underline": ModifierUnderline,
"reverse": ModifierReverse,
}
// readStyle translates an []rune like `fg:red,mod:bold,bg:white` to a style
func readStyle(runes []rune, defaultStyle Style) Style {
style := defaultStyle
split := strings.Split(string(runes), tokenItemSeparator)
for _, item := range split {
pair := strings.Split(item, tokenValueSeparator)
if len(pair) == 2 {
switch pair[0] {
case tokenFg:
style.Fg = StyleParserColorMap[pair[1]]
case tokenBg:
style.Bg = StyleParserColorMap[pair[1]]
case tokenModifier:
style.Modifier = modifierMap[pair[1]]
}
}
}
return style
}
// this will start at ]( and look backwards to find the [ and forward
// to find the ) and record these Start and End indexes in a StyleBlock
func findStartEndOfStyle(pos int, runes []rune) StyleBlock {
current := pos
sb := StyleBlock{0, 0}
for {
current--
if runes[current] == tokenBeginStyledText {
sb.Start = current
break
}
}
current = pos
for {
current++
if runes[current] == tokenEndStyle {
sb.End = current
break
}
}
return sb
}
// if are string is "foo [thing](style) foo [more](style)"
// this will return "foo ", "[thing](style)", " foo ", "[more](style)"
func breakBlocksIntoStrings(s string) []string {
buff := []string{}
blocks := findStyleBlocks(s)
if len(blocks) == 0 {
return buff
}
startEnd := len(s)
for i := len(blocks) - 1; i >= 0; i-- {
b := blocks[i]
item := s[b.End+1 : startEnd]
if item != "" {
buff = append([]string{item}, buff...)
}
item = s[b.Start : b.End+1]
buff = append([]string{item}, buff...)
startEnd = b.Start
}
item := s[0:startEnd]
if item != "" {
buff = append([]string{item}, buff...)
}
return buff
}
// loop through positions and make [] of StyleBlocks
func findStyleBlocks(s string) []StyleBlock {
items := []StyleBlock{}
runes := []rune(s)
positions := findStylePositions(s)
for _, pos := range positions {
sb := findStartEndOfStyle(pos, runes)
items = append(items, sb)
}
return items
}
// uses tokenStyleKey ]( which tells us we have both a [text] and a (style)
// if are string is "foo [thing](style) foo [more](style)"
// this func will return a list of two ints: the index of the first ]( and
// the index of the next one
func findStylePositions(s string) []int {
index := strings.Index(s, tokenStyleKey)
if index == -1 {
return []int{}
}
buff := []int{}
toProcess := s
offset := 0
for {
buff = append(buff, index+offset)
toProcess = toProcess[index+1:]
offset += index + 1
index = strings.Index(toProcess, tokenStyleKey)
if index == -1 {
break
}
}
return buff
}
func containsStyle(s string) bool {
if strings.HasPrefix(s, string(tokenBeginStyledText)) &&
strings.HasSuffix(s, string(tokenEndStyle)) &&
strings.Contains(s, string(tokenEndStyledText)) &&
strings.Contains(s, string(tokenBeginStyle)) {
return true
}
return false
}
// [text](style) will return text
func extractTextFromBlock(item string) string {
index := strings.Index(item, string(tokenEndStyledText))
return item[1:index]
}
// [text](style) will return style
func extractStyleFromBlock(item string) string {
index := strings.Index(item, string(tokenBeginStyle))
return item[index+1 : len(item)-1]
}
// ParseStyles parses a string for embedded Styles and returns []Cell with the correct styling.
// Uses defaultStyle for any text without an embedded style.
// Syntax is of the form [text](fg:<color>,mod:<attribute>,bg:<color>).
// Ordering does not matter. All fields are optional.
func ParseStyles(s string, defaultStyle Style) []Cell {
cells := []Cell{}
items := breakBlocksIntoStrings(s)
if len(items) == 0 {
return RunesToStyledCells([]rune(s), defaultStyle)
}
for _, item := range items {
if containsStyle(item) {
text := extractTextFromBlock(item)
styleText := extractStyleFromBlock(item)
style := readStyle([]rune(styleText), defaultStyle)
cells = append(cells, RunesToStyledCells([]rune(text), style)...)
} else {
cells = append(cells, RunesToStyledCells([]rune(item), defaultStyle)...)
}
}
return cells
}