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