update structure | 'internal/color' for color type and theme | 'internal/transform/color' for colorization | 'internal/transform/markdown' for markdown-like formatting | 'clifmt.go' tests | make colorization + markdown-like format available for nesting (and cross-nesting?)
This commit is contained in:
parent
699b413b2a
commit
7c12b9a9db
|
@ -0,0 +1,42 @@
|
|||
package clifmt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.xdrm.io/go/clifmt/internal/color"
|
||||
colorTransform "git.xdrm.io/go/clifmt/internal/transform/color"
|
||||
mdTransform "git.xdrm.io/go/clifmt/internal/transform/markdown"
|
||||
)
|
||||
|
||||
var theme = color.DefaultTheme()
|
||||
|
||||
// Sprintf returns a terminal-colorized output following the coloring format
|
||||
func Sprintf(format string, a ...interface{}) (string, error) {
|
||||
// 1. Pre-process format with 'fmt'
|
||||
formatted := fmt.Sprintf(format, a...)
|
||||
|
||||
// 2. Colorize
|
||||
colorized, err := colorTransform.Transform(formatted, theme)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3. Markdown format
|
||||
markdown, err := mdTransform.Transform(colorized)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3. return final output
|
||||
return markdown, nil
|
||||
}
|
||||
|
||||
// Printf prints a terminal-colorized output following the coloring format
|
||||
func Printf(format string, a ...interface{}) error {
|
||||
s, err := Sprintf(format, a...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(s)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package clifmt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestColoring(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
Input string
|
||||
Output string
|
||||
}{
|
||||
// foreground + background
|
||||
{
|
||||
"start ${some text input}(#ff0000:#00ff00) end\n",
|
||||
"start \x1b[38;2;255;0;0;48;2;0;255;0msome text input\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#f00:#0f0) end\n",
|
||||
"start \x1b[38;2;255;0;0;48;2;0;255;0msome text input\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(red:green) end\n",
|
||||
"start \x1b[38;2;255;0;0;48;2;0;255;0msome text input\x1b[0m end\n",
|
||||
},
|
||||
|
||||
// mixed notations
|
||||
{
|
||||
"start ${some text input}(red:#00ff00) end\n",
|
||||
"start \x1b[38;2;255;0;0;48;2;0;255;0msome text input\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(red:#0f0) end\n",
|
||||
"start \x1b[38;2;255;0;0;48;2;0;255;0msome text input\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#ff0000:green) end\n",
|
||||
"start \x1b[38;2;255;0;0;48;2;0;255;0msome text input\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#f00:green) end\n",
|
||||
"start \x1b[38;2;255;0;0;48;2;0;255;0msome text input\x1b[0m end\n",
|
||||
},
|
||||
|
||||
// foreground only
|
||||
{
|
||||
"start ${some text input}(red) end\n",
|
||||
"start \x1b[38;2;255;0;0msome text input\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#ff0000) end\n",
|
||||
"start \x1b[38;2;255;0;0msome text input\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#f00) end\n",
|
||||
"start \x1b[38;2;255;0;0msome text input\x1b[0m end\n",
|
||||
},
|
||||
|
||||
// background only
|
||||
{
|
||||
"start ${some text input}(:blue) end\n",
|
||||
"start \x1b[48;2;0;0;255msome text input\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(:#0000ff) end\n",
|
||||
"start \x1b[48;2;0;0;255msome text input\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(:#00f) end\n",
|
||||
"start \x1b[48;2;0;0;255msome text input\x1b[0m end\n",
|
||||
},
|
||||
|
||||
// multi matches
|
||||
{
|
||||
"start ${text1}(red) separation ${text2}(#0f0) end\n",
|
||||
"start \x1b[38;2;255;0;0mtext1\x1b[0m separation \x1b[38;2;0;255;0mtext2\x1b[0m end\n",
|
||||
}, {
|
||||
"start ${text1}(:red) separation ${text2}(:#0f0) end\n",
|
||||
"start \x1b[48;2;255;0;0mtext1\x1b[0m separation \x1b[48;2;0;255;0mtext2\x1b[0m end\n",
|
||||
}}
|
||||
|
||||
for i, test := range tests {
|
||||
output, err := Sprintf(test.Input)
|
||||
if err != nil {
|
||||
t.Errorf("[%d] unexpected error <%v>", i, err)
|
||||
break
|
||||
}
|
||||
|
||||
if output != test.Output {
|
||||
t.Errorf("[%d] expected '%s', got '%s'", i, test.Output, output)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
Input string
|
||||
Output string
|
||||
}{
|
||||
// each notation
|
||||
{
|
||||
"start **bold text** end\n",
|
||||
"start \x1b[1mbold text\x1b[22m end\n",
|
||||
}, {
|
||||
"start *italic text* end\n",
|
||||
"start \x1b[3mitalic text\x1b[23m end\n",
|
||||
}, {
|
||||
"start _underlined text_ end\n",
|
||||
"start \x1b[4munderlined text\x1b[24m end\n",
|
||||
},
|
||||
|
||||
// mixed notations
|
||||
{
|
||||
"start ***bold italic*** end\n",
|
||||
"start \x1b[3m\x1b[1mbold italic\x1b[23m\x1b[22m end\n",
|
||||
}, {
|
||||
"start **_bold underline_** end\n",
|
||||
"start \x1b[1m\x1b[4mbold underline\x1b[24m\x1b[22m end\n",
|
||||
}, {
|
||||
"start _**bold underline**_ end\n",
|
||||
"start \x1b[4m\x1b[1mbold underline\x1b[22m\x1b[24m end\n",
|
||||
}, {
|
||||
"start *_italic underline_* end\n",
|
||||
"start \x1b[3m\x1b[4mitalic underline\x1b[24m\x1b[23m end\n",
|
||||
}, {
|
||||
"start _*italic underline*_ end\n",
|
||||
"start \x1b[4m\x1b[3mitalic underline\x1b[23m\x1b[24m end\n",
|
||||
}, {
|
||||
"start _***bold italic underline***_ end\n",
|
||||
"start \x1b[4m\x1b[3m\x1b[1mbold italic underline\x1b[23m\x1b[22m\x1b[24m end\n",
|
||||
}, {
|
||||
"start **_*bold italic underline*_** end\n",
|
||||
"start \x1b[1m\x1b[4m\x1b[3mbold italic underline\x1b[23m\x1b[24m\x1b[22m end\n",
|
||||
}, {
|
||||
"start *_**bold italic underline**_* end\n",
|
||||
"start \x1b[3m\x1b[4m\x1b[1mbold italic underline\x1b[22m\x1b[24m\x1b[23m end\n",
|
||||
}, {
|
||||
"start _***bold italic underline***_ end\n",
|
||||
"start \x1b[4m\x1b[3m\x1b[1mbold italic underline\x1b[23m\x1b[22m\x1b[24m end\n",
|
||||
},
|
||||
|
||||
// mixed notations not matching
|
||||
{
|
||||
"start ***bold** italic* end\n",
|
||||
"start \x1b[3m\x1b[1mbold\x1b[22m italic\x1b[23m end\n",
|
||||
}, {
|
||||
"start **_bold** underline_ end\n",
|
||||
"start \x1b[1m\x1b[4mbold\x1b[22m underline\x1b[24m end\n",
|
||||
}, {
|
||||
"start _**bold_ underline** end\n",
|
||||
"start \x1b[4m\x1b[1mbold\x1b[24m underline\x1b[22m end\n",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
output, err := Sprintf(test.Input)
|
||||
if err != nil {
|
||||
t.Errorf("[%d] unexpected error <%v>", i, err)
|
||||
break
|
||||
}
|
||||
|
||||
if output != test.Output {
|
||||
t.Errorf("[%d] expected '%s'\n", i, strings.Replace(strings.Replace(test.Output, "\n", "\\n", -1), "\x1b", "\\e", -1))
|
||||
t.Errorf("[%d] got '%s'\n", i, strings.Replace(strings.Replace(output, "\n", "\\n", -1), "\x1b", "\\e", -1))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
99
colors.go
99
colors.go
|
@ -1,99 +0,0 @@
|
|||
package clifmt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type terminalColor uint32
|
||||
|
||||
// Theme is used to associate color names to integer color values ;
|
||||
// the color names in the theme are available in the colorizing format
|
||||
type Theme map[string]terminalColor
|
||||
|
||||
var theme Theme = make(map[string]terminalColor)
|
||||
|
||||
func init() {
|
||||
theme["black"] = 0x000000
|
||||
theme["white"] = 0xffffff
|
||||
theme["red"] = 0xff0000
|
||||
theme["green"] = 0x00ff00
|
||||
theme["blue"] = 0x0000ff
|
||||
theme["yellow"] = 0xffff00
|
||||
theme["orange"] = 0xff8c00
|
||||
theme["purple"] = 0x800080
|
||||
theme["navy"] = 0x000080
|
||||
theme["aqua"] = 0x00ffff
|
||||
theme["gray"] = 0x808080
|
||||
theme["silver"] = 0xc0c0c0
|
||||
theme["fuchsia"] = 0xff00ff
|
||||
theme["olive"] = 0x808000
|
||||
theme["teal"] = 0x008080
|
||||
theme["brown"] = 0x800000
|
||||
}
|
||||
|
||||
// fromName returns the integer value of a color name
|
||||
// from the built-in color map ; it is case insensitive
|
||||
func fromName(s string) (terminalColor, error) {
|
||||
value, ok := theme[s]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unknown color name '%s'", s)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// fromHex returns the integer value associated with
|
||||
// an hexadecimal string (full-sized or short version)
|
||||
// the format is 'abc' or 'abcdef'
|
||||
func fromHex(s string) (terminalColor, error) {
|
||||
if len(s) != 3 && len(s) != 6 {
|
||||
return 0, fmt.Errorf("expect a size of 3 or 6 (remove the '#' prefix)")
|
||||
}
|
||||
|
||||
// short version
|
||||
input := s
|
||||
if len(s) == 3 {
|
||||
input = fmt.Sprintf("%c%c%c%c%c%c", s[0], s[0], s[1], s[1], s[2], s[2])
|
||||
}
|
||||
|
||||
n, err := strconv.ParseUint(input, 16, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return terminalColor(n), nil
|
||||
}
|
||||
|
||||
// parseColor tries to parse a color string (can be a name or an hexa value)
|
||||
func parseColor(s string) (terminalColor, error) {
|
||||
|
||||
// (0) ...
|
||||
if len(s) < 1 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
// (1) hexa
|
||||
if s[0] == '#' {
|
||||
return fromHex(s[1:])
|
||||
}
|
||||
|
||||
// (2) name
|
||||
return fromName(s)
|
||||
|
||||
}
|
||||
|
||||
// Red component of the color
|
||||
func (c terminalColor) Red() uint8 {
|
||||
return uint8((c >> 16) & 0xff)
|
||||
}
|
||||
|
||||
// Green component of the color
|
||||
func (c terminalColor) Green() uint8 {
|
||||
return uint8((c >> 8) & 0xff)
|
||||
}
|
||||
|
||||
// Blue component of the color
|
||||
func (c terminalColor) Blue() uint8 {
|
||||
return uint8(c & 0xff)
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package color
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// T represents a color
|
||||
type T uint32
|
||||
|
||||
// FromName returns the integer value of a color name
|
||||
// from the built-in color map ; it is case insensitive
|
||||
func FromName(t Theme, s string) (T, error) {
|
||||
value, ok := t[s]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unknown color name '%s'", s)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// FromHex returns the integer value associated with
|
||||
// an hexadecimal string (full-sized or short version)
|
||||
// the format is 'abc' or 'abcdef'
|
||||
func FromHex(s string) (T, error) {
|
||||
if len(s) != 3 && len(s) != 6 {
|
||||
return 0, fmt.Errorf("expect a size of 3 or 6 (without the '#' prefix)")
|
||||
}
|
||||
|
||||
// short version
|
||||
input := s
|
||||
if len(s) == 3 {
|
||||
input = fmt.Sprintf("%c%c%c%c%c%c", s[0], s[0], s[1], s[1], s[2], s[2])
|
||||
}
|
||||
|
||||
n, err := strconv.ParseUint(input, 16, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return T(n), nil
|
||||
}
|
||||
|
||||
// Parse tries to parse a color string (can be a name or an hexa value)
|
||||
func Parse(t Theme, s string) (T, error) {
|
||||
|
||||
// (0) ...
|
||||
if len(s) < 1 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
// (1) hexa
|
||||
if s[0] == '#' {
|
||||
return FromHex(s[1:])
|
||||
}
|
||||
|
||||
// (2) name
|
||||
return FromName(t, s)
|
||||
|
||||
}
|
||||
|
||||
// Red component of the color
|
||||
func (c T) Red() uint8 {
|
||||
return uint8((c >> 16) & 0xff)
|
||||
}
|
||||
|
||||
// Green component of the color
|
||||
func (c T) Green() uint8 {
|
||||
return uint8((c >> 8) & 0xff)
|
||||
}
|
||||
|
||||
// Blue component of the color
|
||||
func (c T) Blue() uint8 {
|
||||
return uint8(c & 0xff)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package color
|
||||
|
||||
// Theme is used to associate color names to integer color values ;
|
||||
// the color names in the theme are available in the colorizing format
|
||||
type Theme map[string]T
|
||||
|
||||
// Default loads sets the default theme associations
|
||||
func DefaultTheme() Theme {
|
||||
theme := make(Theme)
|
||||
theme["black"] = 0x000000
|
||||
theme["white"] = 0xffffff
|
||||
theme["red"] = 0xff0000
|
||||
theme["green"] = 0x00ff00
|
||||
theme["blue"] = 0x0000ff
|
||||
theme["yellow"] = 0xffff00
|
||||
theme["orange"] = 0xff8c00
|
||||
theme["purple"] = 0x800080
|
||||
theme["navy"] = 0x000080
|
||||
theme["aqua"] = 0x00ffff
|
||||
theme["gray"] = 0x808080
|
||||
theme["silver"] = 0xc0c0c0
|
||||
theme["fuchsia"] = 0xff00ff
|
||||
theme["olive"] = 0x808000
|
||||
theme["teal"] = 0x008080
|
||||
theme["brown"] = 0x800000
|
||||
|
||||
return theme
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package color
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.xdrm.io/go/clifmt/internal/color"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// extractor helps extract features from the coloring format defined as follows :
|
||||
//
|
||||
// - [Color] -> [a-z] # named color
|
||||
// - [Color] -> #[0-9a-f]{3} # hexa color (shortcode)
|
||||
// - [Color] -> #[0-9a-f]{6} # hexa color (full-sized)
|
||||
// - [Text] -> ANY
|
||||
// - [Format] -> ${Text}(Color:Color) # foreground, background colors
|
||||
// - [Format] -> ${Text}(Color) # foreground color only
|
||||
// - [Format] -> ${Text}(:Color) # background color only
|
||||
var extractor = regexp.MustCompile(`(?m)\${([^$]+)}\(((?:[a-z]+|#(?:[0-9a-f]{3}|[0-9a-f]{6})))?(?:\:((?:[a-z]+|#(?:[0-9a-f]{3}|[0-9a-f]{6}))))?\)`)
|
||||
|
||||
// colorize returns the terminal-formatted @text colorized with the @fg and @bg colors
|
||||
func colorize(t string, fg *color.T, bg *color.T) string {
|
||||
// no coloring
|
||||
if fg == nil && bg == nil {
|
||||
return t
|
||||
}
|
||||
|
||||
// only foreground
|
||||
if bg == nil {
|
||||
return fmt.Sprintf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", fg.Red(), fg.Green(), fg.Blue(), t)
|
||||
}
|
||||
// only background
|
||||
if fg == nil {
|
||||
return fmt.Sprintf("\x1b[48;2;%d;%d;%dm%s\x1b[0m", bg.Red(), bg.Green(), bg.Blue(), t)
|
||||
}
|
||||
|
||||
// both colors
|
||||
return fmt.Sprintf("\x1b[38;2;%d;%d;%d;48;2;%d;%d;%dm%s\x1b[0m", fg.Red(), fg.Green(), fg.Blue(), bg.Red(), bg.Green(), bg.Blue(), t)
|
||||
}
|
||||
|
||||
// Transform the @input text colorized according to the @extractor format
|
||||
func Transform(input string, theme color.Theme) (string, error) {
|
||||
output := ""
|
||||
cursor := int(0)
|
||||
|
||||
// 1. Replace for each match
|
||||
for _, match := range extractor.FindAllStringSubmatchIndex(input, -1) {
|
||||
|
||||
// (1) add gap between input start OR previous match
|
||||
output += input[cursor:match[0]]
|
||||
cursor = match[1]
|
||||
|
||||
// (2) extract features
|
||||
var (
|
||||
text = ""
|
||||
sFg = ""
|
||||
sBg = ""
|
||||
fg *color.T = nil
|
||||
bg *color.T = nil
|
||||
)
|
||||
|
||||
if match[3]-match[2] > 0 {
|
||||
text = input[match[2]:match[3]]
|
||||
}
|
||||
if match[5]-match[4] > 0 {
|
||||
sFg = input[match[4]:match[5]]
|
||||
fgv, err := color.Parse(theme, sFg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fg = &fgv
|
||||
}
|
||||
if match[7]-match[6] > 0 {
|
||||
sBg = input[match[6]:match[7]]
|
||||
bgv, err := color.Parse(theme, sBg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bg = &bgv
|
||||
}
|
||||
|
||||
// (3) replace text with colorized text
|
||||
output += colorize(text, fg, bg)
|
||||
}
|
||||
|
||||
// 2. Add end of input
|
||||
if cursor < len(input)-1 {
|
||||
output += input[cursor:]
|
||||
}
|
||||
|
||||
// 3. print final output
|
||||
return output, nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var boldRe = regexp.MustCompile(`(?m)\*\*((?:[^\*]+\*?)+)\*\*`)
|
||||
|
||||
// boldify returns the terminal-formatted bold text @t
|
||||
func boldify(t string) string {
|
||||
return fmt.Sprintf("\x1b[1m%s\x1b[22m", strings.Replace(t, "\x1b[0m", "\x1b[0m\x1b[1m", -1))
|
||||
}
|
||||
|
||||
// boldTransform the @input text using markdown-like syntax :
|
||||
// - "normal **bold** normal"
|
||||
func boldTransform(input string) (string, error) {
|
||||
output := ""
|
||||
cursor := int(0)
|
||||
|
||||
// 1. Replace for each match
|
||||
for _, match := range boldRe.FindAllStringSubmatchIndex(input, -1) {
|
||||
|
||||
// (1) add gap between input start OR previous match
|
||||
output += input[cursor:match[0]]
|
||||
cursor = match[1]
|
||||
|
||||
// (2) extract features
|
||||
text := ""
|
||||
|
||||
if match[3]-match[2] > 0 {
|
||||
text = input[match[2]:match[3]]
|
||||
}
|
||||
|
||||
// (3) replace text with bold text
|
||||
output += boldify(text)
|
||||
}
|
||||
|
||||
// 2. Add end of input
|
||||
if cursor < len(input)-1 {
|
||||
output += input[cursor:]
|
||||
}
|
||||
|
||||
// 3. print final output
|
||||
return output, nil
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var italicRe = regexp.MustCompile(`(?m)\*([^\*]+)\*`)
|
||||
|
||||
// italic returns the terminal-formatted italic text @t
|
||||
func italic(t string) string {
|
||||
return fmt.Sprintf("\x1b[3m%s\x1b[23m", strings.Replace(t, "\x1b[0m", "\x1b[0m\x1b[3m", -1))
|
||||
}
|
||||
|
||||
// italicTransform the @input text using markdown-like syntax :
|
||||
// - "normal *italic* normal"
|
||||
func italicTransform(input string) (string, error) {
|
||||
output := ""
|
||||
cursor := int(0)
|
||||
|
||||
// 1. Replace for each match
|
||||
for _, match := range italicRe.FindAllStringSubmatchIndex(input, -1) {
|
||||
|
||||
// (1) add gap between input start OR previous match
|
||||
output += input[cursor:match[0]]
|
||||
cursor = match[1]
|
||||
|
||||
// (2) extract features
|
||||
text := ""
|
||||
|
||||
if match[3]-match[2] > 0 {
|
||||
text = input[match[2]:match[3]]
|
||||
}
|
||||
|
||||
// (3) replace text with bold text
|
||||
output += italic(text)
|
||||
}
|
||||
|
||||
// 2. Add end of input
|
||||
if cursor < len(input)-1 {
|
||||
output += input[cursor:]
|
||||
}
|
||||
|
||||
// 3. print final output
|
||||
return output, nil
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package markdown
|
||||
|
||||
func Transform(input string) (string, error) {
|
||||
|
||||
// 1. bold
|
||||
bold, err := boldTransform(input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 2. italic
|
||||
italic, err := italicTransform(bold)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3. italic
|
||||
underline, err := underlineTransform(italic)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return underline, nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var underlineRe = regexp.MustCompile(`(?m)_([^_]+)_`)
|
||||
|
||||
// underline returns the terminal-formatted underline text @t
|
||||
func underline(t string) string {
|
||||
return fmt.Sprintf("\x1b[4m%s\x1b[24m", strings.Replace(t, "\x1b[0m", "\x1b[0m\x1b[4m", -1))
|
||||
}
|
||||
|
||||
// underlineTransform the @input text using markdown-like syntax :
|
||||
// - "normal _underline_ normal"
|
||||
func underlineTransform(input string) (string, error) {
|
||||
output := ""
|
||||
cursor := int(0)
|
||||
|
||||
// 1. Replace for each match
|
||||
for _, match := range underlineRe.FindAllStringSubmatchIndex(input, -1) {
|
||||
|
||||
// (1) add gap between input start OR previous match
|
||||
output += input[cursor:match[0]]
|
||||
cursor = match[1]
|
||||
|
||||
// (2) extract features
|
||||
text := ""
|
||||
|
||||
if match[3]-match[2] > 0 {
|
||||
text = input[match[2]:match[3]]
|
||||
}
|
||||
|
||||
// (3) replace text with bold text
|
||||
output += underline(text)
|
||||
}
|
||||
|
||||
// 2. Add end of input
|
||||
if cursor < len(input)-1 {
|
||||
output += input[cursor:]
|
||||
}
|
||||
|
||||
// 3. print final output
|
||||
return output, nil
|
||||
|
||||
}
|
96
printer.go
96
printer.go
|
@ -1,96 +0,0 @@
|
|||
package clifmt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// extractor helps extract features from the coloring format defined as follows :
|
||||
//
|
||||
// - [Color] -> [a-z] # named color
|
||||
// - [Color] -> #[0-9a-f]{3} # hexa color (shortcode)
|
||||
// - [Color] -> #[0-9a-f]{6} # hexa color (full-sized)
|
||||
// - [Text] -> ANY
|
||||
// - [Format] -> ${Text}(Color:Color) # foreground, background colors
|
||||
// - [Format] -> ${Text}(Color) # foreground color only
|
||||
// - [Format] -> ${Text}(:Color) # background color only
|
||||
var extractor = regexp.MustCompile(`(?m)\${([^$]+)}\(((?:[a-z]+|#(?:[0-9a-f]{3}|[0-9a-f]{6})))?(?:\:((?:[a-z]+|#(?:[0-9a-f]{3}|[0-9a-f]{6}))))?\)`)
|
||||
|
||||
// Sprintf returns a terminal-colorized output following the coloring format
|
||||
func Sprintf(format string, a ...interface{}) (string, error) {
|
||||
// 1. Pre-process format with 'fmt'
|
||||
input := fmt.Sprintf(format, a...)
|
||||
output := ""
|
||||
cursor := int(0)
|
||||
|
||||
// 2. extract color format matches
|
||||
for _, match := range extractor.FindAllStringSubmatchIndex(input, -1) {
|
||||
// (1) add gap between input start OR previous match
|
||||
output += input[cursor:match[0]]
|
||||
cursor = match[1]
|
||||
|
||||
// (2) extract features
|
||||
var err error
|
||||
var (
|
||||
text = ""
|
||||
sForeground = ""
|
||||
sBackground = ""
|
||||
foreground = terminalColor(0)
|
||||
background = terminalColor(0)
|
||||
)
|
||||
|
||||
if match[3]-match[2] > 0 {
|
||||
text = input[match[2]:match[3]]
|
||||
}
|
||||
if match[5]-match[4] > 0 {
|
||||
sForeground = input[match[4]:match[5]]
|
||||
foreground, err = parseColor(sForeground)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if match[7]-match[6] > 0 {
|
||||
sBackground = input[match[6]:match[7]]
|
||||
background, err = parseColor(sBackground)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// (3) replace text with colorized text
|
||||
if len(sForeground) > 0 {
|
||||
text = colorize(text, true, foreground)
|
||||
}
|
||||
if len(sBackground) > 0 {
|
||||
text = colorize(text, false, background)
|
||||
}
|
||||
output += text
|
||||
}
|
||||
|
||||
// 3. Add end of input
|
||||
if cursor < len(input)-1 {
|
||||
output += input[cursor:]
|
||||
}
|
||||
|
||||
// 3. print final output
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Printf prints a terminal-colorized output following the coloring format
|
||||
func Printf(format string, a ...interface{}) error {
|
||||
s, err := Sprintf(format, a...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func colorize(text string, foregound bool, color terminalColor) string {
|
||||
if foregound {
|
||||
return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", color.Red(), color.Green(), color.Blue(), text)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("\033[48;2;%d;%d;%dm%s\033[0m", color.Red(), color.Green(), color.Blue(), text)
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package clifmt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestColoring(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
Input string
|
||||
Output string
|
||||
}{
|
||||
// foreground + background
|
||||
{
|
||||
"start ${some text input}(#ff0000:#00ff00) end\n",
|
||||
"start \033[48;2;0;255;0m\033[38;2;255;0;0msome text input\033[0m\033[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#f00:#0f0) end\n",
|
||||
"start \033[48;2;0;255;0m\033[38;2;255;0;0msome text input\033[0m\033[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(red:green) end\n",
|
||||
"start \033[48;2;0;255;0m\033[38;2;255;0;0msome text input\033[0m\033[0m end\n",
|
||||
},
|
||||
|
||||
// mixed notations
|
||||
{
|
||||
"start ${some text input}(red:#00ff00) end\n",
|
||||
"start \033[48;2;0;255;0m\033[38;2;255;0;0msome text input\033[0m\033[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(red:#0f0) end\n",
|
||||
"start \033[48;2;0;255;0m\033[38;2;255;0;0msome text input\033[0m\033[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#ff0000:green) end\n",
|
||||
"start \033[48;2;0;255;0m\033[38;2;255;0;0msome text input\033[0m\033[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#f00:green) end\n",
|
||||
"start \033[48;2;0;255;0m\033[38;2;255;0;0msome text input\033[0m\033[0m end\n",
|
||||
},
|
||||
|
||||
// foreground only
|
||||
{
|
||||
"start ${some text input}(red) end\n",
|
||||
"start \033[38;2;255;0;0msome text input\033[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#ff0000) end\n",
|
||||
"start \033[38;2;255;0;0msome text input\033[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(#f00) end\n",
|
||||
"start \033[38;2;255;0;0msome text input\033[0m end\n",
|
||||
},
|
||||
|
||||
// background only
|
||||
{
|
||||
"start ${some text input}(:blue) end\n",
|
||||
"start \033[48;2;0;0;255msome text input\033[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(:#0000ff) end\n",
|
||||
"start \033[48;2;0;0;255msome text input\033[0m end\n",
|
||||
}, {
|
||||
"start ${some text input}(:#00f) end\n",
|
||||
"start \033[48;2;0;0;255msome text input\033[0m end\n",
|
||||
},
|
||||
|
||||
// multi matches
|
||||
{
|
||||
"start ${text1}(red) separation ${text2}(#0f0) end\n",
|
||||
"start \033[38;2;255;0;0mtext1\033[0m separation \033[38;2;0;255;0mtext2\033[0m end\n",
|
||||
}, {
|
||||
"start ${text1}(:red) separation ${text2}(:#0f0) end\n",
|
||||
"start \033[48;2;255;0;0mtext1\033[0m separation \033[48;2;0;255;0mtext2\033[0m end\n",
|
||||
}}
|
||||
|
||||
for i, test := range tests {
|
||||
output, err := Sprintf(test.Input)
|
||||
if err != nil {
|
||||
t.Errorf("[%d] unexpected error <%v>", i, err)
|
||||
break
|
||||
}
|
||||
|
||||
if output != test.Output {
|
||||
t.Errorf("[%d] expected '%s', got '%s'", i, test.Output, output)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue