1
1
mirror of https://github.com/walles/moar.git synced 2024-09-19 07:58:00 +03:00
moar/m/ansiTokenizer.go

446 lines
9.9 KiB
Go
Raw Normal View History

package m
import (
2019-10-27 23:40:30 +03:00
"fmt"
"os"
"regexp"
2019-10-28 22:18:07 +03:00
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/gdamore/tcell"
)
2019-06-17 22:39:57 +03:00
const _TabSize = 4
2019-10-30 21:52:49 +03:00
var manPageBold = tcell.StyleDefault.Bold(true)
var manPageUnderline = tcell.StyleDefault.Underline(true)
2019-06-16 21:57:03 +03:00
// Token is a rune with a style to be written to a cell on screen
type Token struct {
Rune rune
Style tcell.Style
}
// SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and
// adapts the moar output accordingly.
func SetManPageFormatFromEnv() {
// Requested here: https://github.com/walles/moar/issues/14
lessTermcapMd := os.Getenv("LESS_TERMCAP_md")
if lessTermcapMd != "" {
manPageBold = _TermcapToStyle(lessTermcapMd)
}
lessTermcapUs := os.Getenv("LESS_TERMCAP_us")
if lessTermcapUs != "" {
manPageUnderline = _TermcapToStyle(lessTermcapUs)
}
}
2019-11-05 10:32:06 +03:00
// Used from tests
func _ResetManPageFormatForTesting() {
manPageBold = tcell.StyleDefault.Bold(true)
manPageUnderline = tcell.StyleDefault.Underline(true)
}
func _TermcapToStyle(termcap string) tcell.Style {
// Add a character to be sure we have one to take the format from
tokens, _ := TokensFromString(termcap + "x")
return tokens[len(tokens)-1].Style
}
// TokensFromString turns a (formatted) string into a series of tokens,
// and an unformatted string
func TokensFromString(s string) ([]Token, *string) {
var tokens []Token
styleBrokenUtf8 := tcell.StyleDefault.Background(7).Foreground(1)
for _, styledString := range _StyledStringsFromString(s) {
2019-06-27 22:39:46 +03:00
for _, token := range _TokensFromStyledString(styledString) {
switch token.Rune {
case '\x09': // TAB
2019-06-17 22:39:57 +03:00
for {
tokens = append(tokens, Token{
Rune: ' ',
Style: styledString.Style,
})
if (len(tokens))%_TabSize == 0 {
// We arrived at the next tab stop
break
}
}
case '<27>': // Go's broken-UTF8 marker
tokens = append(tokens, Token{
Rune: '?',
Style: styleBrokenUtf8,
})
2019-06-27 22:39:46 +03:00
case '\x08': // Backspace
2019-06-17 22:39:57 +03:00
tokens = append(tokens, Token{
2019-06-27 22:39:46 +03:00
Rune: '<',
Style: styleBrokenUtf8,
2019-06-17 22:39:57 +03:00
})
2019-06-27 22:39:46 +03:00
default:
tokens = append(tokens, token)
}
}
}
var stringBuilder strings.Builder
stringBuilder.Grow(len(tokens))
for _, token := range tokens {
stringBuilder.WriteRune(token.Rune)
}
plainString := stringBuilder.String()
return tokens, &plainString
2019-06-27 22:39:46 +03:00
}
// Consume 'x<x', where '<' is backspace and the result is a bold 'x'
func _ConsumeBold(runes []rune, index int) (int, *Token) {
if index+2 >= len(runes) {
// Not enough runes left for a bold
return index, nil
}
2019-06-27 22:39:46 +03:00
if runes[index+1] != '\b' {
// No backspace in the middle, never mind
return index, nil
}
2019-06-27 22:39:46 +03:00
if runes[index] != runes[index+2] {
// First and last rune not the same, never mind
return index, nil
}
// We have a match!
return index + 3, &Token{
Rune: runes[index],
Style: manPageBold,
}
}
2019-06-27 22:39:46 +03:00
// Consume '_<x', where '<' is backspace and the result is an underlined 'x'
func _ConsumeUnderline(runes []rune, index int) (int, *Token) {
if index+2 >= len(runes) {
// Not enough runes left for a underline
return index, nil
}
if runes[index+1] != '\b' {
// No backspace in the middle, never mind
return index, nil
}
if runes[index] != '_' {
// No underline, never mind
return index, nil
}
// We have a match!
return index + 3, &Token{
Rune: runes[index+2],
Style: manPageUnderline,
}
}
// Consume '+<+<o<o' / '+<o', where '<' is backspace and the result is a unicode bullet.
//
// Used on man pages, try "man printf" on macOS for one example.
func _ConsumeBullet(runes []rune, index int) (int, *Token) {
patterns := []string{"+\bo", "+\b+\bo\bo"}
for _, pattern := range patterns {
if index+len(pattern) > len(runes) {
// Not enough runes left for a bullet
continue
}
mismatch := false
for delta, patternChar := range pattern {
if rune(patternChar) != runes[index+delta] {
// Bullet pattern mismatch, never mind
mismatch = true
}
}
if mismatch {
continue
}
// We have a match!
return index + len(pattern), &Token{
Rune: '•', // Unicode bullet point
Style: tcell.StyleDefault,
}
}
return index, nil
}
func _TokensFromStyledString(styledString _StyledString) []Token {
runes := []rune(styledString.String)
tokens := make([]Token, 0, len(runes))
for index := 0; index < len(runes); index++ {
nextIndex, token := _ConsumeBullet(runes, index)
if nextIndex != index {
tokens = append(tokens, *token)
index = nextIndex - 1
continue
}
nextIndex, token = _ConsumeBold(runes, index)
if nextIndex != index {
tokens = append(tokens, *token)
index = nextIndex - 1
continue
}
nextIndex, token = _ConsumeUnderline(runes, index)
if nextIndex != index {
tokens = append(tokens, *token)
index = nextIndex - 1
continue
}
2019-06-27 22:39:46 +03:00
tokens = append(tokens, Token{
Rune: runes[index],
2019-06-27 22:39:46 +03:00
Style: styledString.Style,
})
}
return tokens
}
type _StyledString struct {
String string
Style tcell.Style
}
func _StyledStringsFromString(s string) []_StyledString {
// This function was inspired by the
// https://golang.org/pkg/regexp/#Regexp.Split source code
pattern := regexp.MustCompile("\x1b\\[([0-9;]*m)")
matches := pattern.FindAllStringIndex(s, -1)
styledStrings := make([]_StyledString, 0, len(matches)+1)
style := tcell.StyleDefault
beg := 0
end := 0
for _, match := range matches {
end = match[0]
2019-06-16 10:23:25 +03:00
if end > beg {
2019-06-27 22:39:46 +03:00
// Found non-zero length string
styledStrings = append(styledStrings, _StyledString{
String: s[beg:end],
Style: style,
})
}
matchedPart := s[match[0]:match[1]]
style = _UpdateStyle(style, matchedPart)
beg = match[1]
}
if end != len(s) {
styledStrings = append(styledStrings, _StyledString{
String: s[beg:],
Style: style,
})
}
return styledStrings
}
// _UpdateStyle parses a string of the form "ESC[33m" into changes to style
func _UpdateStyle(style tcell.Style, escapeSequence string) tcell.Style {
2019-10-27 11:15:16 +03:00
numbers := strings.Split(escapeSequence[2:len(escapeSequence)-1], ";")
index := 0
for index < len(numbers) {
number := numbers[index]
index++
switch strings.TrimLeft(number, "0") {
case "":
style = tcell.StyleDefault
2019-06-16 21:57:03 +03:00
2019-06-16 21:58:19 +03:00
case "1":
style = style.Bold(true)
2020-04-13 17:43:56 +03:00
case "3":
style = style.Italic(true)
case "4":
style = style.Underline(true)
2019-06-16 22:39:27 +03:00
case "7":
style = style.Reverse(true)
2020-04-13 17:43:56 +03:00
case "23":
style = style.Italic(false)
case "24":
style = style.Underline(false)
2019-06-16 22:39:27 +03:00
case "27":
style = style.Reverse(false)
2019-06-16 21:57:03 +03:00
// Foreground colors
case "30":
style = style.Foreground(0)
case "31":
style = style.Foreground(1)
case "32":
style = style.Foreground(2)
case "33":
style = style.Foreground(3)
case "34":
style = style.Foreground(4)
case "35":
style = style.Foreground(5)
case "36":
style = style.Foreground(6)
case "37":
style = style.Foreground(7)
2019-10-27 11:15:16 +03:00
case "38":
var err error = nil
var color *tcell.Color
2019-10-27 23:40:30 +03:00
index, color, err = consumeCompositeColor(numbers, index-1)
2019-10-27 11:15:16 +03:00
if err != nil {
log.Warnf("Foreground: %s", err.Error())
2019-10-27 11:15:16 +03:00
return style
}
style = style.Foreground(*color)
2019-07-15 14:34:42 +03:00
case "39":
style = style.Foreground(tcell.ColorDefault)
2019-06-16 21:57:03 +03:00
// Background colors
case "40":
style = style.Background(0)
2019-06-16 21:57:03 +03:00
case "41":
style = style.Background(1)
2019-06-16 21:57:03 +03:00
case "42":
style = style.Background(2)
2019-06-16 21:57:03 +03:00
case "43":
style = style.Background(3)
2019-06-16 21:57:03 +03:00
case "44":
style = style.Background(4)
2019-06-16 21:57:03 +03:00
case "45":
style = style.Background(5)
2019-06-16 21:57:03 +03:00
case "46":
style = style.Background(6)
2019-06-16 21:57:03 +03:00
case "47":
style = style.Background(7)
2019-10-27 11:15:16 +03:00
case "48":
var err error = nil
var color *tcell.Color
2019-10-27 23:40:30 +03:00
index, color, err = consumeCompositeColor(numbers, index-1)
2019-10-27 11:15:16 +03:00
if err != nil {
log.Warnf("Background: %s", err.Error())
2019-10-27 11:15:16 +03:00
return style
}
style = style.Background(*color)
2019-07-15 14:34:42 +03:00
case "49":
style = style.Background(tcell.ColorDefault)
2019-06-16 21:57:03 +03:00
default:
log.Warnf("Unrecognized ANSI SGR code <%s>", number)
}
}
return style
}
2019-10-27 11:15:16 +03:00
2019-10-27 23:40:30 +03:00
// numbers is a list of numbers from a ANSI SGR string
2019-10-27 11:15:16 +03:00
// index points to either 38 or 48 in that string
//
// This method will return:
// * The first index in the string that this function did not consume
// * A color value that can be applied to a style
2019-10-27 23:40:30 +03:00
func consumeCompositeColor(numbers []string, index int) (int, *tcell.Color, error) {
2019-10-28 22:09:08 +03:00
baseIndex := index
2019-10-27 23:40:30 +03:00
if numbers[index] != "38" && numbers[index] != "48" {
err := fmt.Errorf(
"Unknown start of color sequence <%s>, expected 38 (foreground) or 48 (background): <CSI %sm>",
numbers[index],
2019-10-28 22:09:08 +03:00
strings.Join(numbers[baseIndex:], ";"))
return -1, nil, err
}
index++
if index >= len(numbers) {
err := fmt.Errorf(
"Incomplete color sequence: <CSI %sm>",
strings.Join(numbers[baseIndex:], ";"))
2019-10-27 23:40:30 +03:00
return -1, nil, err
}
2019-10-28 22:18:07 +03:00
if numbers[index] == "5" {
2019-10-28 22:30:04 +03:00
// Handle 8 bit color
2019-10-28 22:18:07 +03:00
index++
if index >= len(numbers) {
err := fmt.Errorf(
"Incomplete 8 bit color sequence: <CSI %sm>",
strings.Join(numbers[baseIndex:], ";"))
return -1, nil, err
}
colorNumber, err := strconv.Atoi(numbers[index])
if err != nil {
return -1, nil, err
}
colorValue := tcell.Color(colorNumber)
return index + 1, &colorValue, nil
}
2019-10-28 22:21:39 +03:00
if numbers[index] == "2" {
2019-10-28 22:30:04 +03:00
// Handle 24 bit color
rIndex := index + 1
gIndex := index + 2
bIndex := index + 3
if bIndex >= len(numbers) {
err := fmt.Errorf(
"Incomplete 24 bit color sequence, expected N8;2;R;G;Bm: <CSI %sm>",
strings.Join(numbers[baseIndex:], ";"))
return -1, nil, err
}
rValueX, err := strconv.ParseInt(numbers[rIndex], 10, 32)
if err != nil {
return -1, nil, err
}
rValue := int32(rValueX)
gValueX, err := strconv.Atoi(numbers[gIndex])
if err != nil {
return -1, nil, err
}
gValue := int32(gValueX)
bValueX, err := strconv.Atoi(numbers[bIndex])
if err != nil {
return -1, nil, err
}
bValue := int32(bValueX)
colorValue := tcell.NewRGBColor(rValue, gValue, bValue)
return bIndex + 1, &colorValue, nil
2019-10-28 22:21:39 +03:00
}
err := fmt.Errorf(
"Unknown color type <%s>, expected 5 (8 bit color) or 2 (24 bit color): <CSI %sm>",
numbers[index],
strings.Join(numbers[baseIndex:], ";"))
return -1, nil, err
2019-10-27 11:15:16 +03:00
}