From 9b7d5eca1e47f22c78cc720a9b5cca15d122051d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Tue, 29 Dec 2020 17:22:19 +0100 Subject: [PATCH] Refactor templates --- adapter/handlebars/handlebars.go | 77 ++++++++---------- adapter/handlebars/handlebars_test.go | 111 ++++++++++++++++---------- cmd/container.go | 14 ++-- cmd/new.go | 2 +- core/note/create.go | 48 +++++------ core/template.go | 12 +++ 6 files changed, 148 insertions(+), 116 deletions(-) create mode 100644 core/template.go diff --git a/adapter/handlebars/handlebars.go b/adapter/handlebars/handlebars.go index 051d8c9..d65e7fa 100644 --- a/adapter/handlebars/handlebars.go +++ b/adapter/handlebars/handlebars.go @@ -6,6 +6,7 @@ import ( "github.com/aymerick/raymond" "github.com/mickael-menu/zk/adapter/handlebars/helpers" + "github.com/mickael-menu/zk/core" "github.com/mickael-menu/zk/util" "github.com/mickael-menu/zk/util/date" "github.com/mickael-menu/zk/util/errors" @@ -17,67 +18,56 @@ func Init(lang string, logger util.Logger, date date.Provider) { helpers.RegisterShell(logger) } -// HandlebarsRenderer holds parsed handlebars template and renders them. -type HandlebarsRenderer struct { - templates map[string]*raymond.Template +// Template renders a parsed handlebars template. +type Template struct { + template *raymond.Template } -// NewRenderer creates a new instance of HandlebarsRenderer. -func NewRenderer() *HandlebarsRenderer { - return &HandlebarsRenderer{ - templates: make(map[string]*raymond.Template), - } -} - -// Render renders a handlebars string template with the given context. -func (hr *HandlebarsRenderer) Render(template string, context interface{}) (string, error) { - templ, err := hr.loadTemplate(template) +// Render renders the template with the given context. +func (t *Template) Render(context interface{}) (string, error) { + res, err := t.template.Exec(context) if err != nil { - return "", err + return "", errors.Wrap(err, "render template failed") } - return hr.render(templ, context) + return html.UnescapeString(res), nil } -// RenderFile renders a handlebars template file with the given context. -func (hr *HandlebarsRenderer) RenderFile(path string, context interface{}) (string, error) { - templ, err := hr.loadFileTemplate(path) - if err != nil { - return "", err - } - return hr.render(templ, context) +// Loader loads and holds parsed handlebars templates. +type Loader struct { + strings map[string]*Template + files map[string]*Template } -func (hr *HandlebarsRenderer) render(template *raymond.Template, context interface{}) (string, error) { - res, err := template.Exec(context) - if err != nil { - return "", errors.Wrap(err, "render template failed") +// NewLoader creates a new instance of Loader. +func NewLoader() *Loader { + return &Loader{ + strings: make(map[string]*Template), + files: make(map[string]*Template), } - return html.UnescapeString(res), nil } -// loadTemplate loads the template with the given content into the renderer if needed. -// Returns the parsed template. -func (hr *HandlebarsRenderer) loadTemplate(content string) (*raymond.Template, error) { +// Load retrieves or parses a handlebars string template. +func (l *Loader) Load(content string) (core.Template, error) { wrap := errors.Wrapperf("load template failed") // Already loaded? - templ, ok := hr.templates[content] + template, ok := l.strings[content] if ok { - return templ, nil + return template, nil } // Load new template. - templ, err := raymond.Parse(content) + vendorTempl, err := raymond.Parse(content) if err != nil { return nil, wrap(err) } - hr.templates[content] = templ - return templ, nil + template = &Template{vendorTempl} + l.strings[content] = template + return template, nil } -// loadFileTemplate loads the template at given path into the renderer if needed. -// Returns the parsed template. -func (hr *HandlebarsRenderer) loadFileTemplate(path string) (*raymond.Template, error) { +// LoadFile retrieves or parses a handlebars file template. +func (l *Loader) LoadFile(path string) (core.Template, error) { wrap := errors.Wrapper("load template file failed") path, err := filepath.Abs(path) @@ -86,16 +76,17 @@ func (hr *HandlebarsRenderer) loadFileTemplate(path string) (*raymond.Template, } // Already loaded? - templ, ok := hr.templates[path] + template, ok := l.files[path] if ok { - return templ, nil + return template, nil } // Load new template. - templ, err = raymond.ParseFile(path) + vendorTempl, err := raymond.ParseFile(path) if err != nil { return nil, wrap(err) } - hr.templates[path] = templ - return templ, nil + template = &Template{vendorTempl} + l.files[path] = template + return template, nil } diff --git a/adapter/handlebars/handlebars_test.go b/adapter/handlebars/handlebars_test.go index 55358ec..98c43af 100644 --- a/adapter/handlebars/handlebars_test.go +++ b/adapter/handlebars/handlebars_test.go @@ -16,63 +16,87 @@ func init() { Init("en", &util.NullLogger, &date) } -func TestRenderString(t *testing.T) { - sut := NewRenderer() - res, err := sut.Render("Goodbye, {{name}}", map[string]string{"name": "Ed"}) +func testString(t *testing.T, template string, context interface{}, expected string) { + sut := NewLoader() + + templ, err := sut.Load(template) + assert.Nil(t, err) + + actual, err := templ.Render(context) assert.Nil(t, err) - assert.Equal(t, res, "Goodbye, Ed") + assert.Equal(t, actual, expected) } -func TestRenderFile(t *testing.T) { - sut := NewRenderer() - res, err := sut.RenderFile(fixtures.Path("template.txt"), map[string]string{"name": "Thom"}) +func testFile(t *testing.T, name string, context interface{}, expected string) { + sut := NewLoader() + + templ, err := sut.LoadFile(fixtures.Path(name)) assert.Nil(t, err) - assert.Equal(t, res, "Hello, Thom\n") + + actual, err := templ.Render(context) + assert.Nil(t, err) + assert.Equal(t, actual, expected) +} + +func TestRenderString(t *testing.T) { + testString(t, + "Goodbye, {{name}}", + map[string]string{"name": "Ed"}, + "Goodbye, Ed", + ) +} + +func TestRenderFile(t *testing.T) { + testFile(t, + "template.txt", + map[string]string{"name": "Thom"}, + "Hello, Thom\n", + ) } func TestUnknownVariable(t *testing.T) { - sut := NewRenderer() - res, err := sut.Render("Hi, {{unknown}}!", nil) - assert.Nil(t, err) - assert.Equal(t, res, "Hi, !") + testString(t, + "Hi, {{unknown}}!", + nil, + "Hi, !", + ) } func TestDoesntEscapeHTML(t *testing.T) { - sut := NewRenderer() + testString(t, + "Salut, {{name}}!", + map[string]string{"name": "l'ami"}, + "Salut, l'ami!", + ) - res, err := sut.Render("Salut, {{name}}!", map[string]string{"name": "l'ami"}) - assert.Nil(t, err) - assert.Equal(t, res, "Salut, l'ami!") - - res, err = sut.RenderFile(fixtures.Path("unescape.txt"), map[string]string{"name": "l'ami"}) - assert.Nil(t, err) - assert.Equal(t, res, "Salut, l'ami!\n") + testFile(t, + "unescape.txt", + map[string]string{"name": "l'ami"}, + "Salut, l'ami!\n", + ) } func TestSlugHelper(t *testing.T) { - sut := NewRenderer() // block - res, err := sut.Render("{{#slug}}This will be slugified!{{/slug}}", nil) - assert.Nil(t, err) - assert.Equal(t, res, "this-will-be-slugified") + testString(t, + "{{#slug}}This will be slugified!{{/slug}}", + nil, + "this-will-be-slugified", + ) // inline - res, err = sut.Render(`{{slug "This will be slugified!"}}`, nil) - assert.Nil(t, err) - assert.Equal(t, res, "this-will-be-slugified") + testString(t, + `{{slug "This will be slugified!"}}`, + nil, + "this-will-be-slugified", + ) } func TestDateHelper(t *testing.T) { - sut := NewRenderer() - // Default - res, err := sut.Render("{{date}}", nil) - assert.Nil(t, err) - assert.Equal(t, res, "2009-11-17") + testString(t, "{{date}}", nil, "2009-11-17") test := func(format string, expected string) { - res, err := sut.Render(fmt.Sprintf("{{date '%s'}}", format), nil) - assert.Nil(t, err) - assert.Equal(t, res, expected) + testString(t, fmt.Sprintf("{{date '%s'}}", format), nil, expected) } test("short", "11/17/2009") @@ -87,13 +111,16 @@ func TestDateHelper(t *testing.T) { } func TestShellHelper(t *testing.T) { - sut := NewRenderer() // block is passed as piped input - res, err := sut.Render(`{{#sh "tr '[a-z]' '[A-Z]'"}}Hello, world!{{/sh}}`, nil) - assert.Nil(t, err) - assert.Equal(t, res, "HELLO, WORLD!") + testString(t, + `{{#sh "tr '[a-z]' '[A-Z]'"}}Hello, world!{{/sh}}`, + nil, + "HELLO, WORLD!", + ) // inline - res, err = sut.Render(`{{sh "echo 'Hello, world!'"}}`, nil) - assert.Nil(t, err) - assert.Equal(t, res, "Hello, world!\n") + testString(t, + `{{sh "echo 'Hello, world!'"}}`, + nil, + "Hello, world!\n", + ) } diff --git a/cmd/container.go b/cmd/container.go index 9c0db00..efd3f2d 100644 --- a/cmd/container.go +++ b/cmd/container.go @@ -10,9 +10,9 @@ import ( ) type Container struct { - Date date.Provider - Logger util.Logger - renderer *handlebars.HandlebarsRenderer + Date date.Provider + Logger util.Logger + templateLoader *handlebars.Loader } func NewContainer() *Container { @@ -26,11 +26,11 @@ func NewContainer() *Container { } } -func (c *Container) Renderer() *handlebars.HandlebarsRenderer { - if c.renderer == nil { +func (c *Container) TemplateLoader() *handlebars.Loader { + if c.templateLoader == nil { // FIXME take the language from the config handlebars.Init("en", c.Logger, c.Date) - c.renderer = handlebars.NewRenderer() + c.templateLoader = handlebars.NewLoader() } - return c.renderer + return c.templateLoader } diff --git a/cmd/new.go b/cmd/new.go index 5b0bcbf..2696ca7 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -35,7 +35,7 @@ func (cmd *New) Run(container *Container) error { Template: opt.NewNotEmptyString(cmd.Template), Extra: cmd.Extra, } - file, err := note.Create(zk, opts, container.Renderer()) + file, err := note.Create(zk, opts, container.TemplateLoader()) if err != nil { return err } diff --git a/core/note/create.go b/core/note/create.go index 6ac6f1b..17fa98b 100644 --- a/core/note/create.go +++ b/core/note/create.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" + "github.com/mickael-menu/zk/core" "github.com/mickael-menu/zk/core/zk" "github.com/mickael-menu/zk/util/errors" "github.com/mickael-menu/zk/util/opt" @@ -12,14 +13,6 @@ import ( "github.com/mickael-menu/zk/util/rand" ) -// Renderer renders templates. -type Renderer interface { - // Render renders a handlebars string template with the given context. - Render(template string, context interface{}) (string, error) - // RenderFile renders a handlebars template file with the given context. - RenderFile(path string, context interface{}) (string, error) -} - // CreateOpts holds the options to create a new note. type CreateOpts struct { // Parent directory for the new note. @@ -35,7 +28,7 @@ type CreateOpts struct { } // Create generates a new note in the given slip box from the given options. -func Create(zk *zk.Zk, opts CreateOpts, renderer Renderer) (string, error) { +func Create(zk *zk.Zk, opts CreateOpts, templateLoader core.TemplateLoader) (string, error) { wrap := errors.Wrapper("note creation failed") exists, err := paths.Exists(opts.Dir.Path) @@ -46,16 +39,20 @@ func Create(zk *zk.Zk, opts CreateOpts, renderer Renderer) (string, error) { return "", wrap(fmt.Errorf("directory not found at %v", opts.Dir.Path)) } - context, err := newRenderContext(zk, opts, renderer) + context, err := newRenderContext(zk, opts, templateLoader) if err != nil { return "", wrap(err) } - template := opts.Template.OrDefault( + templatePath := opts.Template.OrDefault( zk.Template(opts.Dir).OrDefault(""), ) - if template != "" { - content, err := renderer.RenderFile(template, context) + if templatePath != "" { + template, err := templateLoader.LoadFile(templatePath) + if err != nil { + return "", wrap(err) + } + content, err := template.Render(context) if err != nil { return "", wrap(err) } @@ -81,7 +78,7 @@ type renderContext struct { Extra map[string]string } -func newRenderContext(zk *zk.Zk, opts CreateOpts, renderer Renderer) (renderContext, error) { +func newRenderContext(zk *zk.Zk, opts CreateOpts, templateLoader core.TemplateLoader) (renderContext, error) { if opts.Extra == nil { opts.Extra = make(map[string]string) } @@ -91,9 +88,13 @@ func newRenderContext(zk *zk.Zk, opts CreateOpts, renderer Renderer) (renderCont } } - template := zk.FilenameTemplate(opts.Dir) + template, err := templateLoader.Load(zk.FilenameTemplate(opts.Dir)) + if err != nil { + return renderContext{}, err + } + idGenerator := rand.NewIDGenerator(zk.RandIDOpts(opts.Dir)) - contextGenerator := newRenderContextGenerator(template, opts, renderer) + contextGenerator := newRenderContextGenerator(template, opts) for { context, err := contextGenerator(idGenerator()) if err != nil { @@ -112,9 +113,8 @@ func newRenderContext(zk *zk.Zk, opts CreateOpts, renderer Renderer) (renderCont type renderContextGenerator func(randomID string) (renderContext, error) func newRenderContextGenerator( - filenameTemplate string, + filenameTemplate core.Template, opts CreateOpts, - renderer Renderer, ) renderContextGenerator { context := renderContext{ // FIXME Customize default title in config @@ -123,22 +123,24 @@ func newRenderContextGenerator( Extra: opts.Extra, } - isRandom := strings.Contains(filenameTemplate, "random-id") - i := 0 + isRandom := false + return func(randomID string) (renderContext, error) { + i++ + // Attempts 50ish tries if the filename template contains a random ID before failing. - if i > 0 && !isRandom || i >= 50 { + if i > 1 && !isRandom || i >= 50 { return context, fmt.Errorf("%v: file already exists", context.Path) } - i++ context.RandomID = randomID - filename, err := renderer.Render(filenameTemplate, context) + filename, err := filenameTemplate.Render(context) if err != nil { return context, err } + isRandom = strings.Contains(filename, randomID) // FIXME Customize extension in config path := filepath.Join(opts.Dir.Path, filename+".md") diff --git a/core/template.go b/core/template.go new file mode 100644 index 0000000..d5e9b04 --- /dev/null +++ b/core/template.go @@ -0,0 +1,12 @@ +package core + +// Template renders strings using a given context. +type Template interface { + Render(context interface{}) (string, error) +} + +// TemplateLoader parses a given string template. +type TemplateLoader interface { + Load(template string) (Template, error) + LoadFile(path string) (Template, error) +}