update whole structure : each syntax feature implements now the 'internal/transform/Transformer' interface ; to apply all transforms on the same input text, use a 'internal/transform/Registry' that dispatches errors | update all transformers' code to the new structure | still pass tests

This commit is contained in:
xdrm-brackets 2019-01-27 20:31:06 +01:00
parent e553670836
commit 7ee7710e74
15 changed files with 298 additions and 330 deletions

View File

@ -3,8 +3,12 @@ package clifmt
import ( import (
"fmt" "fmt"
"git.xdrm.io/go/clifmt/internal/color" "git.xdrm.io/go/clifmt/internal/color"
colorTransform "git.xdrm.io/go/clifmt/internal/transform/color" tbold "git.xdrm.io/go/clifmt/internal/syntax/bold"
mdTransform "git.xdrm.io/go/clifmt/internal/transform/markdown" tcolor "git.xdrm.io/go/clifmt/internal/syntax/color"
thyperlink "git.xdrm.io/go/clifmt/internal/syntax/hyperlink"
titalic "git.xdrm.io/go/clifmt/internal/syntax/italic"
tunderline "git.xdrm.io/go/clifmt/internal/syntax/underline"
"git.xdrm.io/go/clifmt/internal/transform"
"strings" "strings"
) )
@ -28,26 +32,27 @@ func Sprintf(format string, a ...interface{}) (string, error) {
formatted = strings.Replace(formatted, "\\_", underscoreToken, -1) formatted = strings.Replace(formatted, "\\_", underscoreToken, -1)
formatted = strings.Replace(formatted, "\\[", squareBracketToken, -1) formatted = strings.Replace(formatted, "\\[", squareBracketToken, -1)
// 3. Colorize // 3. create transformation registry
colorized, err := colorTransform.Transform(formatted, theme) reg := transform.Registry{Transformers: make([]transform.Transformer, 0, 10)}
if err != nil { reg.Transformers = append(reg.Transformers, tcolor.Export)
return "", err reg.Transformers = append(reg.Transformers, tbold.Export)
} reg.Transformers = append(reg.Transformers, titalic.Export)
reg.Transformers = append(reg.Transformers, tunderline.Export)
reg.Transformers = append(reg.Transformers, thyperlink.Export)
// 4. Markdown format transformed, err := reg.Transform(formatted)
markdown, err := mdTransform.Transform(colorized)
if err != nil { if err != nil {
return "", err return "", err
} }
// 5. Restore token-protected characters // 5. Restore token-protected characters
markdown = strings.Replace(markdown, dollarToken, "$", -1) transformed = strings.Replace(transformed, dollarToken, "$", -1)
markdown = strings.Replace(markdown, asteriskToken, "*", -1) transformed = strings.Replace(transformed, asteriskToken, "*", -1)
markdown = strings.Replace(markdown, underscoreToken, "_", -1) transformed = strings.Replace(transformed, underscoreToken, "_", -1)
markdown = strings.Replace(markdown, squareBracketToken, "[", -1) transformed = strings.Replace(transformed, squareBracketToken, "[", -1)
// 6. return final output // 6. return final output
return markdown, nil return transformed, nil
} }
// Printf prints a terminal-colorized output following the coloring format // Printf prints a terminal-colorized output following the coloring format

View File

@ -0,0 +1,24 @@
package bold
import (
"fmt"
"regexp"
"strings"
)
type export string
var Export = export("bold")
func (syn export) Regex() *regexp.Regexp {
return regexp.MustCompile(`(?m)\*\*((?:[^\*]+\*?)+)\*\*`)
}
func (syn export) Transform(args ...string) (string, error) {
// no arg, empty -> ignore
if len(args) < 1 || len(args[0]) < 1 {
return "", nil
}
return fmt.Sprintf("\x1b[1m%s\x1b[22m", strings.Replace(args[0], "\x1b[0m", "\x1b[0m\x1b[1m", -1)), nil
}

View File

@ -0,0 +1,68 @@
package color
import (
"fmt"
"git.xdrm.io/go/clifmt/internal/color"
"regexp"
)
var theme = color.DefaultTheme()
type export string
var Export = export("color")
func (syn export) Regex() *regexp.Regexp {
return regexp.MustCompile(`(?m)\${([^$]+)}\(((?:[a-z]+|#(?:[0-9a-f]{3}|[0-9a-f]{6})))?(?:\:((?:[a-z]+|#(?:[0-9a-f]{3}|[0-9a-f]{6}))))?\)`)
}
func (syn export) Transform(args ...string) (string, error) {
// no arg, no color -> error
if len(args) < 3 {
return "", fmt.Errorf("invalid format")
}
// extract colors
var (
fg *color.T = nil
bg *color.T = nil
)
if len(args[1]) > 0 {
tmp, err := color.Parse(theme, args[1])
if err != nil {
return "", err
}
fg = &tmp
}
if len(args[2]) > 0 {
tmp, err := color.Parse(theme, args[2])
if err != nil {
return "", err
}
bg = &tmp
}
return colorize(args[0], fg, bg), nil
}
// 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)
}

View File

@ -0,0 +1,23 @@
package hyperlink
import (
"fmt"
"regexp"
)
type export string
var Export = export("hyperlink")
func (syn export) Regex() *regexp.Regexp {
return regexp.MustCompile(`(?m)\[([^\[]+)\]\(([^\)]+)\)`)
}
func (syn export) Transform(args ...string) (string, error) {
// no arg, empty -> ignore
if len(args) < 2 || len(args[0]) < 1 {
return "", nil
}
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", args[1], args[0]), nil
}

View File

@ -0,0 +1,24 @@
package italic
import (
"fmt"
"regexp"
"strings"
)
type export string
var Export = export("italic")
func (syn export) Regex() *regexp.Regexp {
return regexp.MustCompile(`(?m)\*([^\*]+)\*`)
}
func (syn export) Transform(args ...string) (string, error) {
// no arg, empty -> ignore
if len(args) < 1 || len(args[0]) < 1 {
return "", nil
}
return fmt.Sprintf("\x1b[3m%s\x1b[23m", strings.Replace(args[0], "\x1b[0m", "\x1b[0m\x1b[3m", -1)), nil
}

View File

@ -0,0 +1,24 @@
package underline
import (
"fmt"
"regexp"
"strings"
)
type export string
var Export = export("underline")
func (syn export) Regex() *regexp.Regexp {
return regexp.MustCompile(`(?m)_([^_]+)_`)
}
func (syn export) Transform(args ...string) (string, error) {
// no arg, empty -> ignore
if len(args) < 1 || len(args[0]) < 1 {
return "", nil
}
return fmt.Sprintf("\x1b[4m%s\x1b[24m", strings.Replace(args[0], "\x1b[0m", "\x1b[0m\x1b[4m", -1)), nil
}

View File

@ -1,92 +0,0 @@
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) {
output += input[cursor:]
}
// 3. print final output
return output, nil
}

View File

@ -0,0 +1,23 @@
package transform
import (
"fmt"
)
type TransformerError struct {
// Transformer that returned the error
Transformer Transformer
// Err is the actual error
Err error
// Input is the input string to be transformed
Input string
}
func (err *TransformerError) Error() string {
return fmt.Sprintf("Transformer <%T> failed on input '%s': %s",
err.Transformer,
err.Input,
err.Err)
}

View File

@ -1,48 +0,0 @@
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) {
output += input[cursor:]
}
// 3. print final output
return output, nil
}

View File

@ -1,50 +0,0 @@
package markdown
import (
"fmt"
"regexp"
)
var hyperlinkRe = regexp.MustCompile(`(?m)\[([^\[]+)\]\(([^\)]+)\)`)
// linkify returns the terminal-formatted hyperlink for @url with the text : @label
func linkify(url, label string) string {
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, label)
}
// hyperlinkTransform the @input text using markdown-like syntax :
// - "normal [link label](link url) normal"
func hyperlinkTransform(input string) (string, error) {
output := ""
cursor := int(0)
// 1. Replace for each match
for _, match := range hyperlinkRe.FindAllStringSubmatchIndex(input, -1) {
// (1) add gap between input start OR previous match
output += input[cursor:match[0]]
cursor = match[1]
// (2) extract features
var label, url string
if match[3]-match[2] > 0 {
label = input[match[2]:match[3]]
}
if match[5]-match[4] > 0 {
url = input[match[4]:match[5]]
}
// (3) replace with hyperlink
output += linkify(url, label)
}
// 2. Add end of input
if cursor < len(input) {
output += input[cursor:]
}
// 3. print final output
return output, nil
}

View File

@ -1,48 +0,0 @@
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) {
output += input[cursor:]
}
// 3. print final output
return output, nil
}

View File

@ -1,30 +0,0 @@
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. underline
underline, err := underlineTransform(italic)
if err != nil {
return "", err
}
// 4. hyperlink
hyperlinked, err := hyperlinkTransform(underline)
if err != nil {
return "", err
}
return hyperlinked, nil
}

View File

@ -1,48 +0,0 @@
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) {
output += input[cursor:]
}
// 3. print final output
return output, nil
}

View File

@ -0,0 +1,79 @@
package transform
// Registry is used to apply a stack of transformations
// over an input string
type Registry struct {
// cursor is the current transformer
cursor uint
// Transformers represents the transformer stack
// ; each one will be executed in ascending order
Transformers []Transformer
}
// Transform executes each transformer of the stack in ascending order feeding
// each one with the output of its predecessor (@input for the first). Note that if one returns an error
// the process stops here and the error is directly returned.
func (r *Registry) Transform(input string) (string, error) {
in := input
// execute each transformer by order
for _, t := range r.Transformers {
// 1. execute ; dispatch error on failure
out, err := execute(t, in)
if err != nil {
return "", err
}
// 2. replace next input with current output
in = out
}
return in, nil
}
// execute 1 given transformer @t with its @input string and returns the output,
// and the error if one.
func execute(t Transformer, input string) (string, error) {
var (
output string
cursor int
)
// apply transformatione for each match
for _, match := range t.Regex().FindAllStringSubmatchIndex(input, -1) {
// (1) append gap between input start OR previous match
output += input[cursor:match[0]]
cursor = match[1]
// (2) build transformation arguments
args := make([]string, 0, len(match)/2+1)
for i, l := 2, len(match); i < l; i += 2 {
// match exists (not both -1, nor negative length)
if match[i+1]-match[i] > 0 {
args = append(args, input[match[i]:match[i+1]])
continue
}
args = append(args, "")
}
// (3) execute transformation
transformed, err := t.Transform(args...)
if err != nil {
return "", &TransformerError{t, err, input[match[0]:match[1]]}
}
// (4) apply transformation
output += transformed
}
// Add end of input (if not covered by matches)
if cursor < len(input) {
output += input[cursor:]
}
// return final output
return output, nil
}

View File

@ -0,0 +1,14 @@
package transform
import (
"regexp"
)
type Transformer interface {
// Regex returns the regex matching text to replace
Regex() *regexp.Regexp
// Transform is called to replace a match by its transformation
// ; it takes as arguments the matched string chunks from the Regex()
Transform(...string) (string, error)
}