Refactor templates

pull/6/head
Mickaël Menu 3 years ago
parent fdf6718317
commit 9b7d5eca1e
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -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
}

@ -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",
)
}

@ -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
}

@ -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
}

@ -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")

@ -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)
}
Loading…
Cancel
Save