diff --git a/cmd/new.go b/cmd/new.go index af6f147..5b0bcbf 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -3,15 +3,43 @@ package cmd import ( "fmt" + "github.com/mickael-menu/zk/core/note" "github.com/mickael-menu/zk/core/zk" + "github.com/mickael-menu/zk/util/opt" ) +// New adds a new note to the slip box. type New struct { - Directory string `arg optional name:"directory" default:"."` + Directory string `arg optional type:"path" default:"." help:"Directory in which to create the note"` + ShowPath bool `help:"Shows the path of the created note instead of editing it"` + Title string `short:"t" help:"Title of the new note" placeholder:"TITLE"` + Template string `type:"path" help:"Custom template to use to render the note" placeholder:"PATH"` + Extra map[string]string `help:"Extra variables passed to the templates"` } -func (cmd *New) Run() error { - zk, err := zk.Open(cmd.Directory) - fmt.Printf("%+v\n", zk) - return err +func (cmd *New) Run(container *Container) error { + zk, err := zk.Open(".") + if err != nil { + return err + } + + dir, err := zk.DirAt(cmd.Directory) + if err != nil { + return err + } + + opts := note.CreateOpts{ + Dir: *dir, + Title: opt.NewNotEmptyString(cmd.Title), + Content: opt.NullString, + Template: opt.NewNotEmptyString(cmd.Template), + Extra: cmd.Extra, + } + file, err := note.Create(zk, opts, container.Renderer()) + if err != nil { + return err + } + + fmt.Printf("%+v\n", file) + return nil } diff --git a/core/note/note.go b/core/note/note.go new file mode 100644 index 0000000..16a66ec --- /dev/null +++ b/core/note/note.go @@ -0,0 +1,124 @@ +package note + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/mickael-menu/zk/core/zk" + "github.com/mickael-menu/zk/util/errors" + "github.com/mickael-menu/zk/util/opt" + "github.com/mickael-menu/zk/util/paths" + "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. + Dir zk.Dir + // Title of the note. + Title opt.String + // Initial content of the note, which will be injected in the template. + Content opt.String + // Custom template to use for the note, overriding the one declared in the config. + Template opt.String + // Extra template variables to expand. + Extra map[string]string +} + +// 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) { + wrap := errors.Wrapper("note creation failed") + + exists, err := paths.Exists(opts.Dir.Path) + if err != nil { + return "", wrap(err) + } + if !exists { + return "", wrap(fmt.Errorf("directory not found at %v", opts.Dir.Path)) + } + + extra := zk.Extra(opts.Dir) + for k, v := range opts.Extra { + extra[k] = v + } + + context := renderContext{ + // FIXME Customize default title in config + Title: opts.Title.OrDefault("Untitled"), + Content: opts.Content.Unwrap(), + Extra: extra, + } + + file, err := genFilepath(zk, opts.Dir, renderer, &context) + if err != nil { + return "", wrap(err) + } + + template := opts.Template.OrDefault( + zk.Template(opts.Dir).OrDefault(""), + ) + if template != "" { + content, err := renderer.RenderFile(template, context) + if err != nil { + return "", wrap(err) + } + err = paths.WriteString(path, content) + if err != nil { + return "", wrap(err) + } + } + + return file, nil +} + +// renderContext holds the placeholder values which will be expanded in the templates. +type renderContext struct { + Title string + Content string + Filename string + FilenameStem string `handlebars:"filename-stem"` + RandomID string `handlebars:"random-id"` + Extra map[string]string +} + +func genFilepath(zk *zk.Zk, dir zk.Dir, renderer Renderer, context *renderContext) (string, error) { + template := zk.FilenameTemplate(dir) + isRandom := strings.Contains(template, "random-id") + + i := 0 + for { + context.RandomID = rand.GenID(zk.RandIDOpts(dir)) + + filename, err := renderer.Render(template, context) + if err != nil { + return "", err + } + + // FIXME Customize extension in config + path := filepath.Join(dir.Path, filename+".md") + exists, err := paths.Exists(path) + if err != nil { + return "", err + } + + if !exists { + context.Filename = filepath.Base(path) + context.FilenameStem = paths.FilenameStem(path) + return path, nil + + } else if !isRandom || i > 50 { // Attempts 50 tries if the filename template contains a random ID before failing. + return "", fmt.Errorf("%v: file already exists", path) + } + + i++ + } +} diff --git a/util/opt/opt.go b/util/opt/opt.go index 87baaf0..1b542ef 100644 --- a/util/opt/opt.go +++ b/util/opt/opt.go @@ -41,3 +41,7 @@ func (s String) OrDefault(def string) string { func (s String) Unwrap() string { return s.OrDefault("") } + +func (s String) String() string { + return s.OrDefault("") +} diff --git a/util/paths/paths.go b/util/paths/paths.go new file mode 100644 index 0000000..3945ead --- /dev/null +++ b/util/paths/paths.go @@ -0,0 +1,37 @@ +package paths + +import ( + "os" + "path/filepath" + "strings" +) + +// Exists returns whether the given path exists on the file system. +func Exists(path string) (bool, error) { + if _, err := os.Stat(path); err == nil { + return true, nil + } else if os.IsNotExist(err) { + return false, nil + } else { + return false, err + } +} + +// FilenameStem returns the filename component of the given path, +// after removing its file extension. +func FilenameStem(path string) string { + filename := filepath.Base(path) + ext := filepath.Ext(filename) + return strings.TrimSuffix(filename, ext) +} + +// WriteString writes the given content into a new file at the given path. +func WriteString(path string, content string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(content) + return err +}