From 7ee7710e74bdd67400f6306fd059909c1d8c10ae Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 27 Jan 2019 20:31:06 +0100 Subject: [PATCH] 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 --- clifmt.go | 33 +++++---- internal/syntax/bold/bold.go | 24 +++++++ internal/syntax/color/color.go | 68 ++++++++++++++++++ internal/syntax/hyperlink/hyperlink.go | 23 ++++++ internal/syntax/italic/italic.go | 24 +++++++ internal/syntax/underline/underline.go | 24 +++++++ internal/transform/color/color.go | 92 ------------------------ internal/transform/errors.go | 23 ++++++ internal/transform/markdown/bold.go | 48 ------------- internal/transform/markdown/hyperlink.go | 50 ------------- internal/transform/markdown/italic.go | 48 ------------- internal/transform/markdown/markdown.go | 30 -------- internal/transform/markdown/underline.go | 48 ------------- internal/transform/registry.go | 79 ++++++++++++++++++++ internal/transform/replacer.go | 14 ++++ 15 files changed, 298 insertions(+), 330 deletions(-) create mode 100644 internal/syntax/bold/bold.go create mode 100644 internal/syntax/color/color.go create mode 100644 internal/syntax/hyperlink/hyperlink.go create mode 100644 internal/syntax/italic/italic.go create mode 100644 internal/syntax/underline/underline.go delete mode 100644 internal/transform/color/color.go create mode 100644 internal/transform/errors.go delete mode 100644 internal/transform/markdown/bold.go delete mode 100644 internal/transform/markdown/hyperlink.go delete mode 100644 internal/transform/markdown/italic.go delete mode 100644 internal/transform/markdown/markdown.go delete mode 100644 internal/transform/markdown/underline.go create mode 100644 internal/transform/registry.go create mode 100644 internal/transform/replacer.go diff --git a/clifmt.go b/clifmt.go index f2c6134..bf9a668 100644 --- a/clifmt.go +++ b/clifmt.go @@ -3,8 +3,12 @@ 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" + tbold "git.xdrm.io/go/clifmt/internal/syntax/bold" + 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" ) @@ -28,26 +32,27 @@ func Sprintf(format string, a ...interface{}) (string, error) { formatted = strings.Replace(formatted, "\\_", underscoreToken, -1) formatted = strings.Replace(formatted, "\\[", squareBracketToken, -1) - // 3. Colorize - colorized, err := colorTransform.Transform(formatted, theme) - if err != nil { - return "", err - } + // 3. create transformation registry + reg := transform.Registry{Transformers: make([]transform.Transformer, 0, 10)} + reg.Transformers = append(reg.Transformers, tcolor.Export) + 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 - markdown, err := mdTransform.Transform(colorized) + transformed, err := reg.Transform(formatted) if err != nil { return "", err } // 5. Restore token-protected characters - markdown = strings.Replace(markdown, dollarToken, "$", -1) - markdown = strings.Replace(markdown, asteriskToken, "*", -1) - markdown = strings.Replace(markdown, underscoreToken, "_", -1) - markdown = strings.Replace(markdown, squareBracketToken, "[", -1) + transformed = strings.Replace(transformed, dollarToken, "$", -1) + transformed = strings.Replace(transformed, asteriskToken, "*", -1) + transformed = strings.Replace(transformed, underscoreToken, "_", -1) + transformed = strings.Replace(transformed, squareBracketToken, "[", -1) // 6. return final output - return markdown, nil + return transformed, nil } // Printf prints a terminal-colorized output following the coloring format diff --git a/internal/syntax/bold/bold.go b/internal/syntax/bold/bold.go new file mode 100644 index 0000000..93e96ea --- /dev/null +++ b/internal/syntax/bold/bold.go @@ -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 +} diff --git a/internal/syntax/color/color.go b/internal/syntax/color/color.go new file mode 100644 index 0000000..29d9984 --- /dev/null +++ b/internal/syntax/color/color.go @@ -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) +} diff --git a/internal/syntax/hyperlink/hyperlink.go b/internal/syntax/hyperlink/hyperlink.go new file mode 100644 index 0000000..20d4a95 --- /dev/null +++ b/internal/syntax/hyperlink/hyperlink.go @@ -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 +} diff --git a/internal/syntax/italic/italic.go b/internal/syntax/italic/italic.go new file mode 100644 index 0000000..2638e3a --- /dev/null +++ b/internal/syntax/italic/italic.go @@ -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 +} diff --git a/internal/syntax/underline/underline.go b/internal/syntax/underline/underline.go new file mode 100644 index 0000000..c6dfc27 --- /dev/null +++ b/internal/syntax/underline/underline.go @@ -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 +} diff --git a/internal/transform/color/color.go b/internal/transform/color/color.go deleted file mode 100644 index 2595b99..0000000 --- a/internal/transform/color/color.go +++ /dev/null @@ -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 -} diff --git a/internal/transform/errors.go b/internal/transform/errors.go new file mode 100644 index 0000000..d43ae65 --- /dev/null +++ b/internal/transform/errors.go @@ -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) +} diff --git a/internal/transform/markdown/bold.go b/internal/transform/markdown/bold.go deleted file mode 100644 index 32f8dc8..0000000 --- a/internal/transform/markdown/bold.go +++ /dev/null @@ -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 - -} diff --git a/internal/transform/markdown/hyperlink.go b/internal/transform/markdown/hyperlink.go deleted file mode 100644 index e1f2842..0000000 --- a/internal/transform/markdown/hyperlink.go +++ /dev/null @@ -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 - -} diff --git a/internal/transform/markdown/italic.go b/internal/transform/markdown/italic.go deleted file mode 100644 index 66a0b92..0000000 --- a/internal/transform/markdown/italic.go +++ /dev/null @@ -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 - -} diff --git a/internal/transform/markdown/markdown.go b/internal/transform/markdown/markdown.go deleted file mode 100644 index 94df0c1..0000000 --- a/internal/transform/markdown/markdown.go +++ /dev/null @@ -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 -} diff --git a/internal/transform/markdown/underline.go b/internal/transform/markdown/underline.go deleted file mode 100644 index 926669d..0000000 --- a/internal/transform/markdown/underline.go +++ /dev/null @@ -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 - -} diff --git a/internal/transform/registry.go b/internal/transform/registry.go new file mode 100644 index 0000000..272c877 --- /dev/null +++ b/internal/transform/registry.go @@ -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 +} diff --git a/internal/transform/replacer.go b/internal/transform/replacer.go new file mode 100644 index 0000000..3f3e9ab --- /dev/null +++ b/internal/transform/replacer.go @@ -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) +}