diff --git a/adapter/handlebars/handlebars.go b/adapter/handlebars/handlebars.go index 437a1e1..7094af8 100644 --- a/adapter/handlebars/handlebars.go +++ b/adapter/handlebars/handlebars.go @@ -6,16 +6,18 @@ import ( "github.com/aymerick/raymond" "github.com/mickael-menu/zk/adapter/handlebars/helpers" + "github.com/mickael-menu/zk/core" "github.com/mickael-menu/zk/core/templ" "github.com/mickael-menu/zk/util" "github.com/mickael-menu/zk/util/errors" ) -func Init(lang string, logger util.Logger) { +func Init(lang string, logger util.Logger, styler core.Styler) { helpers.RegisterDate(logger) helpers.RegisterPrepend(logger) helpers.RegisterShell(logger) - helpers.RegisterSlug(logger, lang) + helpers.RegisterSlug(lang, logger) + helpers.RegisterStyle(styler, logger) } // Template renders a parsed handlebars template. diff --git a/adapter/handlebars/handlebars_test.go b/adapter/handlebars/handlebars_test.go index 0e12b81..944b166 100644 --- a/adapter/handlebars/handlebars_test.go +++ b/adapter/handlebars/handlebars_test.go @@ -1,16 +1,29 @@ package handlebars import ( + "fmt" "testing" "time" + "github.com/mickael-menu/zk/core" "github.com/mickael-menu/zk/util" "github.com/mickael-menu/zk/util/assert" "github.com/mickael-menu/zk/util/fixtures" ) func init() { - Init("en", &util.NullLogger) + Init("en", &util.NullLogger, &styler{}) +} + +// styler is a test double for core.Styler +// "hello", "red" -> "red(hello)" +type styler struct{} + +func (s *styler) Style(text string, rules ...core.StyleRule) (string, error) { + for _, rule := range rules { + text = fmt.Sprintf("%s(%s)", rule, text) + } + return text, nil } func testString(t *testing.T, template string, context interface{}, expected string) { @@ -126,3 +139,12 @@ func TestShellHelper(t *testing.T) { "Hello, world!\n", ) } + +func TestStyleHelper(t *testing.T) { + // inline + testString(t, "{{style 'single' 'Some text'}}", nil, "single(Some text)") + testString(t, "{{style 'red bold' 'Another text'}}", nil, "bold(red(Another text))") + + // block + testString(t, "{{#style 'single'}}A multiline\ntext{{/style}}", nil, "single(A multiline\ntext)") +} diff --git a/adapter/handlebars/helpers/slug.go b/adapter/handlebars/helpers/slug.go index 4d68ece..38cac8c 100644 --- a/adapter/handlebars/helpers/slug.go +++ b/adapter/handlebars/helpers/slug.go @@ -10,7 +10,7 @@ import ( // // {{slug "This will be slugified!"}} -> this-will-be-slugified // {{#slug}}This will be slugified!{{/slug}} -> this-will-be-slugified -func RegisterSlug(logger util.Logger, lang string) { +func RegisterSlug(lang string, logger util.Logger) { raymond.RegisterHelper("slug", func(opt interface{}) string { switch arg := opt.(type) { case *raymond.Options: diff --git a/adapter/handlebars/helpers/style.go b/adapter/handlebars/helpers/style.go new file mode 100644 index 0000000..00ac6e7 --- /dev/null +++ b/adapter/handlebars/helpers/style.go @@ -0,0 +1,42 @@ +package helpers + +import ( + "strings" + + "github.com/aymerick/raymond" + "github.com/mickael-menu/zk/core" + "github.com/mickael-menu/zk/util" +) + +// RegisterStyle register the {{style}} template helpers which stylizes the +// text input according to predefined styling rules. +// +// {{style "date" created}} +// {{#style "red"}}Hello, world{{/style}} +func RegisterStyle(styler core.Styler, logger util.Logger) { + style := func(keys string, text string) string { + rules := make([]core.StyleRule, 0) + for _, key := range strings.Fields(keys) { + rules = append(rules, core.StyleRule(key)) + } + res, err := styler.Style(text, rules...) + if err != nil { + logger.Err(err) + return text + } else { + return res + } + } + + raymond.RegisterHelper("style", func(rules string, opt interface{}) string { + switch arg := opt.(type) { + case *raymond.Options: + return style(rules, arg.Fn()) + case string: + return style(rules, arg) + default: + logger.Printf("the {{style}} template helper is expecting a string as input, received: %v", opt) + return "" + } + }) +} diff --git a/adapter/tty/styler.go b/adapter/tty/styler.go new file mode 100644 index 0000000..d671565 --- /dev/null +++ b/adapter/tty/styler.go @@ -0,0 +1,90 @@ +package tty + +import ( + "fmt" + + "github.com/fatih/color" + "github.com/mickael-menu/zk/core" +) + +// Styler is a text styler using ANSI escape codes to be used with a TTY. +type Styler struct{} + +func NewStyler() *Styler { + return &Styler{} +} + +// FIXME: Semantic rules +func (s *Styler) Style(text string, rules ...core.StyleRule) (string, error) { + attrs, err := s.attributes(rules) + if err != nil { + return "", err + } + if len(attrs) == 0 { + return text, nil + } + return color.New(attrs...).Sprint(text), nil +} + +var attrsMapping = map[core.StyleRule]color.Attribute{ + "reset": color.Reset, + "bold": color.Bold, + "faint": color.Faint, + "italic": color.Italic, + "underline": color.Underline, + "blink-slow": color.BlinkSlow, + "blink-fast": color.BlinkRapid, + "hidden": color.Concealed, + "strikethrough": color.CrossedOut, + + "black": color.FgBlack, + "red": color.FgRed, + "green": color.FgGreen, + "yellow": color.FgYellow, + "blue": color.FgBlue, + "magenta": color.FgMagenta, + "cyan": color.FgCyan, + "white": color.FgWhite, + + "black-bg": color.BgBlack, + "red-bg": color.BgRed, + "green-bg": color.BgGreen, + "yellow-bg": color.BgYellow, + "blue-bg": color.BgBlue, + "magenta-bg": color.BgMagenta, + "cyan-bg": color.BgCyan, + "white-bg": color.BgWhite, + + "bright-black": color.FgHiBlack, + "bright-red": color.FgHiRed, + "bright-green": color.FgHiGreen, + "bright-yellow": color.FgHiYellow, + "bright-blue": color.FgHiBlue, + "bright-magenta": color.FgHiMagenta, + "bright-cyan": color.FgHiCyan, + "bright-white": color.FgHiWhite, + + "bright-black-bg": color.BgHiBlack, + "bright-red-bg": color.BgHiRed, + "bright-green-bg": color.BgHiGreen, + "bright-yellow-bg": color.BgHiYellow, + "bright-blue-bg": color.BgHiBlue, + "bright-magenta-bg": color.BgHiMagenta, + "bright-cyan-bg": color.BgHiCyan, + "bright-white-bg": color.BgHiWhite, +} + +func (s *Styler) attributes(rules []core.StyleRule) ([]color.Attribute, error) { + attrs := make([]color.Attribute, 0) + + for _, rule := range rules { + attr, ok := attrsMapping[rule] + if !ok { + return attrs, fmt.Errorf("unknown styling rule: %v", rule) + } else { + attrs = append(attrs, attr) + } + } + + return attrs, nil +} diff --git a/adapter/tty/styler_test.go b/adapter/tty/styler_test.go new file mode 100644 index 0000000..84a5751 --- /dev/null +++ b/adapter/tty/styler_test.go @@ -0,0 +1,92 @@ +package tty + +import ( + "testing" + + "github.com/fatih/color" + "github.com/mickael-menu/zk/core" + "github.com/mickael-menu/zk/util/assert" +) + +func createStyler() *Styler { + color.NoColor = false // Otherwise the color codes are not injected during tests + return &Styler{} +} + +func TestStyleNoRule(t *testing.T) { + res, err := createStyler().Style("Hello") + assert.Nil(t, err) + assert.Equal(t, res, "Hello") +} + +func TestStyleOneRule(t *testing.T) { + res, err := createStyler().Style("Hello", core.StyleRule("red")) + assert.Nil(t, err) + assert.Equal(t, res, "\033[31mHello\033[0m") +} + +func TestStyleMultipleRule(t *testing.T) { + res, err := createStyler().Style("Hello", core.StyleRule("red"), core.StyleRule("bold")) + assert.Nil(t, err) + assert.Equal(t, res, "\033[31;1mHello\033[0m") +} + +func TestStyleUnknownRule(t *testing.T) { + _, err := createStyler().Style("Hello", core.StyleRule("unknown")) + assert.Err(t, err, "unknown styling rule: unknown") +} + +func TestStyleAllRules(t *testing.T) { + styler := createStyler() + test := func(rule string, expected string) { + res, err := styler.Style("Hello", core.StyleRule(rule)) + assert.Nil(t, err) + assert.Equal(t, res, "\033["+expected+"Hello\033[0m") + } + + test("reset", "0m") + test("bold", "1m") + test("faint", "2m") + test("italic", "3m") + test("underline", "4m") + test("blink-slow", "5m") + test("blink-fast", "6m") + test("hidden", "8m") + test("strikethrough", "9m") + + test("black", "30m") + test("red", "31m") + test("green", "32m") + test("yellow", "33m") + test("blue", "34m") + test("magenta", "35m") + test("cyan", "36m") + test("white", "37m") + + test("black-bg", "40m") + test("red-bg", "41m") + test("green-bg", "42m") + test("yellow-bg", "43m") + test("blue-bg", "44m") + test("magenta-bg", "45m") + test("cyan-bg", "46m") + test("white-bg", "47m") + + test("bright-black", "90m") + test("bright-red", "91m") + test("bright-green", "92m") + test("bright-yellow", "93m") + test("bright-blue", "94m") + test("bright-magenta", "95m") + test("bright-cyan", "96m") + test("bright-white", "97m") + + test("bright-black-bg", "100m") + test("bright-red-bg", "101m") + test("bright-green-bg", "102m") + test("bright-yellow-bg", "103m") + test("bright-blue-bg", "104m") + test("bright-magenta-bg", "105m") + test("bright-cyan-bg", "106m") + test("bright-white-bg", "107m") +} diff --git a/cmd/container.go b/cmd/container.go index a48ba1b..634c2ef 100644 --- a/cmd/container.go +++ b/cmd/container.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/mickael-menu/zk/adapter/handlebars" "github.com/mickael-menu/zk/adapter/sqlite" + "github.com/mickael-menu/zk/adapter/tty" "github.com/mickael-menu/zk/util" "github.com/mickael-menu/zk/util/date" ) @@ -26,7 +27,7 @@ func NewContainer() *Container { func (c *Container) TemplateLoader(lang string) *handlebars.Loader { if c.templateLoader == nil { - handlebars.Init(lang, c.Logger) + handlebars.Init(lang, c.Logger, tty.NewStyler()) c.templateLoader = handlebars.NewLoader() } return c.templateLoader diff --git a/core/style.go b/core/style.go new file mode 100644 index 0000000..5f36ce9 --- /dev/null +++ b/core/style.go @@ -0,0 +1,20 @@ +package core + +// Styler stylizes text according to predefined styling rules. +// +// A rule key can be either semantic, e.g. "title" or explicit, e.g. "red". +type Styler interface { + Style(text string, rules ...StyleRule) (string, error) +} + +// StyleRule is a key representing a single styling rule. +type StyleRule string + +// NullStyler is a Styler with no styling rules. +var NullStyler = nullStyler{} + +type nullStyler struct{} + +func (s nullStyler) Style(text string, rule ...StyleRule) (string, error) { + return text, nil +}