diff --git a/adapter/handlebars/handlebars_test.go b/adapter/handlebars/handlebars_test.go
index 2517551..dde963d 100644
--- a/adapter/handlebars/handlebars_test.go
+++ b/adapter/handlebars/handlebars_test.go
@@ -7,8 +7,8 @@ import (
"github.com/mickael-menu/zk/core/style"
"github.com/mickael-menu/zk/util"
- "github.com/mickael-menu/zk/util/test/assert"
"github.com/mickael-menu/zk/util/fixtures"
+ "github.com/mickael-menu/zk/util/test/assert"
)
func init() {
diff --git a/core/note/create_test.go b/core/note/create_test.go
index 794dde0..0a62b8e 100644
--- a/core/note/create_test.go
+++ b/core/note/create_test.go
@@ -3,19 +3,16 @@ package note
import (
"fmt"
"testing"
- "time"
"github.com/mickael-menu/zk/core/templ"
"github.com/mickael-menu/zk/core/zk"
- "github.com/mickael-menu/zk/util/test/assert"
"github.com/mickael-menu/zk/util/opt"
+ "github.com/mickael-menu/zk/util/test/assert"
)
-var now = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
-
func TestCreate(t *testing.T) {
- filenameTemplate := spyTemplateString("filename")
- bodyTemplate := spyTemplateString("body")
+ filenameTemplate := NewRendererSpyString("filename")
+ bodyTemplate := NewRendererSpyString("body")
res, err := create(
CreateOpts{
@@ -33,11 +30,11 @@ func TestCreate(t *testing.T) {
Content: opt.NewString("Note content"),
},
createDeps{
- filenameTemplate: &filenameTemplate,
- bodyTemplate: &bodyTemplate,
+ filenameTemplate: filenameTemplate,
+ bodyTemplate: bodyTemplate,
genId: func() string { return "abc" },
validatePath: func(path string) (bool, error) { return true, nil },
- now: now,
+ now: Now,
},
)
@@ -49,7 +46,7 @@ func TestCreate(t *testing.T) {
})
// Check that the templates received the proper render contexts.
- assert.Equal(t, filenameTemplate.Contexts, []renderContext{{
+ assert.Equal(t, filenameTemplate.Contexts, []interface{}{renderContext{
ID: "abc",
Title: "Note title",
Content: "Note content",
@@ -57,9 +54,9 @@ func TestCreate(t *testing.T) {
Extra: map[string]string{
"hello": "world",
},
- Now: now,
+ Now: Now,
}})
- assert.Equal(t, bodyTemplate.Contexts, []renderContext{{
+ assert.Equal(t, bodyTemplate.Contexts, []interface{}{renderContext{
ID: "abc",
Title: "Note title",
Content: "Note content",
@@ -69,15 +66,15 @@ func TestCreate(t *testing.T) {
Extra: map[string]string{
"hello": "world",
},
- Now: now,
+ Now: Now,
}})
}
func TestCreateTriesUntilValidPath(t *testing.T) {
- filenameTemplate := spyTemplate(func(context renderContext) string {
- return context.ID
+ filenameTemplate := NewRendererSpy(func(context interface{}) string {
+ return context.(renderContext).ID
})
- bodyTemplate := spyTemplateString("body")
+ bodyTemplate := NewRendererSpyString("body")
res, err := create(
CreateOpts{
@@ -91,13 +88,13 @@ func TestCreateTriesUntilValidPath(t *testing.T) {
Title: opt.NewString("Note title"),
},
createDeps{
- filenameTemplate: &filenameTemplate,
- bodyTemplate: &bodyTemplate,
+ filenameTemplate: filenameTemplate,
+ bodyTemplate: bodyTemplate,
genId: incrementingID(),
validatePath: func(path string) (bool, error) {
return path == "/test/log/3.md", nil
},
- now: now,
+ now: Now,
},
)
@@ -108,24 +105,24 @@ func TestCreateTriesUntilValidPath(t *testing.T) {
content: "body",
})
- assert.Equal(t, filenameTemplate.Contexts, []renderContext{
- {
+ assert.Equal(t, filenameTemplate.Contexts, []interface{}{
+ renderContext{
ID: "1",
Title: "Note title",
Dir: "log",
- Now: now,
+ Now: Now,
},
- {
+ renderContext{
ID: "2",
Title: "Note title",
Dir: "log",
- Now: now,
+ Now: Now,
},
- {
+ renderContext{
ID: "3",
Title: "Note title",
Dir: "log",
- Now: now,
+ Now: Now,
},
})
}
@@ -148,38 +145,14 @@ func TestCreateErrorWhenNoValidPaths(t *testing.T) {
bodyTemplate: templ.NullRenderer,
genId: func() string { return "abc" },
validatePath: func(path string) (bool, error) { return false, nil },
- now: now,
+ now: Now,
},
)
assert.Err(t, err, "/test/log/filename.md: note already exists")
}
-func spyTemplate(result func(renderContext) string) TemplateSpy {
- return TemplateSpy{
- Contexts: make([]renderContext, 0),
- Result: result,
- }
-}
-
-func spyTemplateString(result string) TemplateSpy {
- return TemplateSpy{
- Contexts: make([]renderContext, 0),
- Result: func(_ renderContext) string { return result },
- }
-}
-
-type TemplateSpy struct {
- Result func(renderContext) string
- Contexts []renderContext
-}
-
-func (m *TemplateSpy) Render(context interface{}) (string, error) {
- renderContext := context.(renderContext)
- m.Contexts = append(m.Contexts, renderContext)
- return m.Result(renderContext), nil
-}
-
+// incrementingID returns a generator of incrementing string ID.
func incrementingID() func() string {
i := 0
return func() string {
diff --git a/core/note/format.go b/core/note/format.go
index 044b202..45d41cf 100644
--- a/core/note/format.go
+++ b/core/note/format.go
@@ -63,28 +63,24 @@ var formatTemplates = map[string]string{
"short": `{{style "title" title}} {{style "path" path}} ({{date created "elapsed"}})
-{{prepend " " snippet}}
-`,
+{{prepend " " snippet}}`,
"medium": `{{style "title" title}} {{style "path" path}}
Created: {{date created "short"}}
-{{prepend " " snippet}}
-`,
+{{prepend " " snippet}}`,
"long": `{{style "title" title}} {{style "path" path}}
Created: {{date created "short"}}
Modified: {{date created "short"}}
-{{prepend " " snippet}}
-`,
+{{prepend " " snippet}}`,
"full": `{{style "title" title}} {{style "path" path}}
Created: {{date created "short"}}
Modified: {{date created "short"}}
-{{prepend " " body}}
-`,
+{{prepend " " body}}`,
}
var termRegex = regexp.MustCompile(`(.*?)`)
@@ -108,6 +104,7 @@ func (f *Formatter) Format(match Match) (string, error) {
WordCount: match.WordCount,
Created: match.Created,
Modified: match.Modified,
+ Checksum: match.Checksum,
})
}
@@ -121,4 +118,5 @@ type formatRenderContext struct {
WordCount int `handlebars:"word-count"`
Created time.Time
Modified time.Time
+ Checksum string
}
diff --git a/core/note/format_test.go b/core/note/format_test.go
new file mode 100644
index 0000000..33588fd
--- /dev/null
+++ b/core/note/format_test.go
@@ -0,0 +1,164 @@
+package note
+
+import (
+ "testing"
+ "time"
+
+ "github.com/mickael-menu/zk/util/opt"
+ "github.com/mickael-menu/zk/util/test/assert"
+)
+
+func TestEmptyFormat(t *testing.T) {
+ f, _ := newFormatter(t, opt.NewString(""))
+ res, err := f.Format(Match{})
+ assert.Nil(t, err)
+ assert.Equal(t, res, "")
+}
+
+func TestDefaultFormat(t *testing.T) {
+ f, _ := newFormatter(t, opt.NullString)
+ res, err := f.Format(Match{})
+ assert.Nil(t, err)
+ assert.Equal(t, res, `{{style "title" title}} {{style "path" path}} ({{date created "elapsed"}})
+
+{{prepend " " snippet}}`)
+}
+
+func TestFormats(t *testing.T) {
+ test := func(format string, expected string) {
+ f, _ := newFormatter(t, opt.NewString(format))
+ actual, err := f.Format(Match{})
+ assert.Nil(t, err)
+ assert.Equal(t, actual, expected)
+ }
+
+ // Known formats
+ test("path", `{{path}}`)
+
+ test("oneline", `{{style "title" title}} {{style "path" path}} ({{date created "elapsed"}})`)
+
+ test("short", `{{style "title" title}} {{style "path" path}} ({{date created "elapsed"}})
+
+{{prepend " " snippet}}`)
+
+ test("medium", `{{style "title" title}} {{style "path" path}}
+Created: {{date created "short"}}
+
+{{prepend " " snippet}}`)
+
+ test("long", `{{style "title" title}} {{style "path" path}}
+Created: {{date created "short"}}
+Modified: {{date created "short"}}
+
+{{prepend " " snippet}}`)
+
+ test("full", `{{style "title" title}} {{style "path" path}}
+Created: {{date created "short"}}
+Modified: {{date created "short"}}
+
+{{prepend " " body}}`)
+
+ // Known formats are case sensitive.
+ test("Path", "Path")
+
+ // Custom formats are used literally.
+ test("{{title}}", "{{title}}")
+
+ // \n and \t in custom formats are expanded.
+ test(`{{title}}\t{{path}}\n{{snippet}}`, "{{title}}\t{{path}}\n{{snippet}}")
+}
+
+func TestFormatRenderContext(t *testing.T) {
+ f, templs := newFormatter(t, opt.NewString("path"))
+
+ _, err := f.Format(Match{
+ Snippet: "Note snippet",
+ Metadata: Metadata{
+ Path: "dir/note.md",
+ Title: "Note title",
+ Lead: "Lead paragraph",
+ Body: "Note body",
+ RawContent: "Raw content",
+ WordCount: 42,
+ Created: Now,
+ Modified: Now.Add(48 * time.Hour),
+ Checksum: "Note checksum",
+ },
+ })
+ assert.Nil(t, err)
+
+ // Check that the template was provided with the proper information in the
+ // render context.
+ assert.Equal(t, templs.Contexts, []interface{}{
+ formatRenderContext{
+ Path: "dir/note.md",
+ Title: "Note title",
+ Lead: "Lead paragraph",
+ Body: "Note body",
+ Snippet: "Note snippet",
+ RawContent: "Raw content",
+ WordCount: 42,
+ Created: Now,
+ Modified: Now.Add(48 * time.Hour),
+ Checksum: "Note checksum",
+ },
+ })
+}
+
+func TestFormatPath(t *testing.T) {
+ test := func(basePath, currentPath, path string, expected string) {
+ f, templs := newFormatterWithPaths(t, basePath, currentPath, opt.NullString)
+ _, err := f.Format(Match{
+ Metadata: Metadata{Path: path},
+ })
+ assert.Nil(t, err)
+ assert.Equal(t, templs.Contexts, []interface{}{
+ formatRenderContext{
+ Path: expected,
+ },
+ })
+ }
+
+ // Check that the path is relative to the current directory.
+ test("", "", "note.md", "note.md")
+ test("", "", "dir/note.md", "dir/note.md")
+ test("/abs/zk", "/abs/zk", "note.md", "note.md")
+ test("/abs/zk", "/abs/zk", "dir/note.md", "dir/note.md")
+ test("/abs/zk", "/abs/zk/dir", "note.md", "../note.md")
+ test("/abs/zk", "/abs/zk/dir", "dir/note.md", "note.md")
+ test("/abs/zk", "/abs", "note.md", "zk/note.md")
+ test("/abs/zk", "/abs", "dir/note.md", "zk/dir/note.md")
+}
+
+func TestFormatStylesSnippetTerm(t *testing.T) {
+ test := func(snippet string, expected string) {
+ f, templs := newFormatter(t, opt.NullString)
+ _, err := f.Format(Match{
+ Snippet: snippet,
+ })
+ assert.Nil(t, err)
+ assert.Equal(t, templs.Contexts, []interface{}{
+ formatRenderContext{
+ Path: ".",
+ Snippet: expected,
+ },
+ })
+ }
+
+ test("Hello world!", "Hello world!")
+ test("Hello world!", "Hello term(world)!")
+ test("Hello world with several matches!", "Hello term(world) with term(several matches)!")
+ test("Hello world with several matches!", "Hello term(world) with term(several matches)!")
+}
+
+func newFormatter(t *testing.T, format opt.String) (*Formatter, *TemplLoaderSpy) {
+ return newFormatterWithPaths(t, "", "", format)
+}
+
+func newFormatterWithPaths(t *testing.T, basePath, currentPath string, format opt.String) (*Formatter, *TemplLoaderSpy) {
+ loader := NewTemplLoaderSpy()
+ styler := &StylerMock{}
+ formatter, err := NewFormatter(basePath, currentPath, format, loader, styler)
+ assert.Nil(t, err)
+ return formatter, loader
+}
diff --git a/core/note/util_test.go b/core/note/util_test.go
new file mode 100644
index 0000000..cc6dc25
--- /dev/null
+++ b/core/note/util_test.go
@@ -0,0 +1,73 @@
+package note
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/mickael-menu/zk/core/style"
+ "github.com/mickael-menu/zk/core/templ"
+)
+
+var Now = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
+
+// TemplLoaderSpy implements templ.Loader and saves the render contexts
+// provided to the templates it creates.
+//
+// The generated Renderer returns the template used to create them without
+// modification.
+type TemplLoaderSpy struct {
+ Contexts []interface{}
+}
+
+func NewTemplLoaderSpy() *TemplLoaderSpy {
+ return &TemplLoaderSpy{
+ Contexts: make([]interface{}, 0),
+ }
+}
+
+func (l *TemplLoaderSpy) Load(template string) (templ.Renderer, error) {
+ return NewRendererSpy(func(context interface{}) string {
+ l.Contexts = append(l.Contexts, context)
+ return template
+ }), nil
+}
+
+func (l *TemplLoaderSpy) LoadFile(path string) (templ.Renderer, error) {
+ panic("not implemented")
+}
+
+// RendererSpy implements templ.Renderer and saves the provided render contexts.
+type RendererSpy struct {
+ Result func(interface{}) string
+ Contexts []interface{}
+}
+
+func NewRendererSpy(result func(interface{}) string) *RendererSpy {
+ return &RendererSpy{
+ Contexts: make([]interface{}, 0),
+ Result: result,
+ }
+}
+
+func NewRendererSpyString(result string) *RendererSpy {
+ return &RendererSpy{
+ Contexts: make([]interface{}, 0),
+ Result: func(_ interface{}) string { return result },
+ }
+}
+
+func (m *RendererSpy) Render(context interface{}) (string, error) {
+ m.Contexts = append(m.Contexts, context)
+ return m.Result(context), nil
+}
+
+// StylerMock implements core.Styler by doing the transformation:
+// "hello", "red" -> "red(hello)"
+type StylerMock struct{}
+
+func (s *StylerMock) Style(text string, rules ...style.Rule) (string, error) {
+ for _, rule := range rules {
+ text = fmt.Sprintf("%s(%s)", rule, text)
+ }
+ return text, nil
+}