zk/internal/core/notebook_store.go
2021-04-24 15:17:38 +02:00

361 lines
9.5 KiB
Go

package core
import (
"fmt"
"path/filepath"
"github.com/mickael-menu/zk/internal/util/errors"
)
// NotebookStore retrieves or creates new notebooks.
type NotebookStore struct {
config Config
notebookFactory NotebookFactory
templateLoaderFactory TemplateLoaderFactory
fs FileStorage
// Cached opened notebooks.
notebooks map[string]*Notebook
}
type NotebookStorePorts struct {
NotebookFactory NotebookFactory
TemplateLoaderFactory TemplateLoaderFactory
FS FileStorage
}
// NewNotebookStore creates a new NotebookStore instance using the given
// options and port implementations.
func NewNotebookStore(config Config, ports NotebookStorePorts) *NotebookStore {
return &NotebookStore{
config: config,
notebookFactory: ports.NotebookFactory,
templateLoaderFactory: ports.TemplateLoaderFactory,
fs: ports.FS,
notebooks: map[string]*Notebook{},
}
}
// ErrNotebookNotFound is an error returned when a notebook cannot be found at the given path or its parents.
type ErrNotebookNotFound string
func (e ErrNotebookNotFound) Error() string {
return fmt.Sprintf("no notebook found in %s or a parent directory", string(e))
}
// Open returns a new Notebook instance for the notebook containing the
// given file path.
func (ns *NotebookStore) Open(path string) (*Notebook, error) {
wrap := errors.Wrapper("open failed")
path = ns.fs.Canonical(path)
nb := ns.cachedNotebookAt(path)
if nb != nil {
return nb, nil
}
path, err := ns.fs.Abs(path)
if err != nil {
return nil, wrap(err)
}
path, err = ns.locateNotebook(path)
if err != nil {
return nil, wrap(err)
}
configPath := filepath.Join(path, ".zk/config.toml")
config, err := OpenConfig(configPath, ns.config, ns.fs)
if err != nil {
return nil, wrap(err)
}
nb, err = ns.notebookFactory(path, config)
if err != nil {
return nil, wrap(err)
}
ns.notebooks[path] = nb
return nb, nil
}
// cachedNotebookAt returns any cached notebook containing the given path.
func (ns *NotebookStore) cachedNotebookAt(path string) *Notebook {
path, err := ns.fs.Abs(path)
if err != nil {
return nil
}
for root, nb := range ns.notebooks {
if isDesc, err := ns.fs.IsDescendantOf(root, path); isDesc && err == nil {
return nb
}
}
return nil
}
// InitOpts holds the user preferences when creating a new notebook.
type InitOpts struct {
WikiLinks bool
Hashtags bool
ColonTags bool
MultiwordTags bool
}
// Init creates a new notebook at the given file path.
func (ns *NotebookStore) Init(path string, options InitOpts) (*Notebook, error) {
wrap := errors.Wrapper("init")
path, err := ns.fs.Abs(path)
if err != nil {
return nil, wrap(err)
}
if existingPath, err := ns.locateNotebook(path); err == nil {
return nil, wrap(fmt.Errorf("a notebook already exists in %v", existingPath))
}
// Create the default configuration file.
config, err := ns.generateConfig(options)
if err != nil {
return nil, wrap(err)
}
err = ns.fs.Write(filepath.Join(path, ".zk/config.toml"), []byte(config))
if err != nil {
return nil, wrap(err)
}
// Create the default template.
err = ns.fs.Write(filepath.Join(path, ".zk/templates/default.md"), []byte(defaultTemplate))
if err != nil {
return nil, wrap(err)
}
return ns.Open(path)
}
// locateNotebook finds the root of the notebook containing the given path.
func (ns *NotebookStore) locateNotebook(path string) (string, error) {
if !filepath.IsAbs(path) {
panic("absolute path expected")
}
var locate func(string) (string, error)
locate = func(currentPath string) (string, error) {
if currentPath == "/" || currentPath == "." {
return "", ErrNotebookNotFound(path)
}
exists, err := ns.fs.DirExists(filepath.Join(currentPath, ".zk"))
switch {
case err != nil:
return "", err
case exists:
return currentPath, nil
default:
return locate(filepath.Dir(currentPath))
}
}
return locate(path)
}
func (ns *NotebookStore) generateConfig(options InitOpts) (string, error) {
loader, err := ns.templateLoaderFactory("en")
if err != nil {
return "", err
}
template, err := loader.LoadTemplate(defaultConfig)
if err != nil {
return "", err
}
return template.Render(options)
}
const defaultConfig = `# zk configuration file
#
# Uncomment the properties you want to customize.
# NOTE SETTINGS
#
# Defines the default options used when generating new notes.
[note]
# Language used when writing notes.
# This is used to generate slugs or with date formats.
#language = "en"
# The default title used for new note, if no ` + "`" + `--title` + "`" + ` flag is provided.
#default-title = "Untitled"
# Template used to generate a note's filename, without extension.
#filename = "\{{id}}"
# The file extension used for the notes.
#extension = "md"
# Template used to generate a note's content.
# If not an absolute path, it is relative to .zk/templates/
template = "default.md"
# Configure random ID generation.
# The charset used for random IDs. You can use:
# * letters: only letters from a to z.
# * numbers: 0 to 9
# * alphanum: letters + numbers
# * hex: hexadecimal, from a to f and 0 to 9
# * custom string: will use any character from the provided value
#id-charset = "alphanum"
# Length of the generated IDs.
#id-length = 4
# Letter case for the random IDs, among lower, upper or mixed.
#id-case = "lower"
# EXTRA VARIABLES
#
# A dictionary of variables you can use for any custom values when generating
# new notes. They are accessible in templates with \{{extra.<key>}}
[extra]
#key = "value"
# GROUP OVERRIDES
#
# You can override global settings from [note] and [extra] for a particular
# group of notes by declaring a [group."<name>"] section.
#
# Specify the list of directories which will automatically belong to the group
# with the optional ` + "`" + `paths` + "`" + ` property.
#
# Omitting ` + "`" + `paths` + "`" + ` is equivalent to providing a single path equal to the name of
# the group. This can be useful to quickly declare a group by the name of the
# directory it applies to.
#[group."<NAME>"]
#paths = ["<DIR1>", "<DIR2>"]
#[group."<NAME>".note]
#filename = "\{{date now}}"
#[group."<NAME>".extra]
#key = "value"
# MARKDOWN SETTINGS
[format.markdown]
# Format used to generate links between notes.
# Either "wiki", "markdown" or a custom template. Default is "markdown".
{{#if WikiLinks}}
link-format = "wiki"
{{else}}
#link-format = "wiki"
{{/if}}
# Indicates whether a link's path will be percent-encoded.
# Defaults to true for "markdown" format and false for "wiki" format.
#link-encode-path = true
# Indicates whether a link's path file extension will be removed.
# Defaults to true.
#link-drop-extension = true
# Enable support for #hashtags.
{{#if Hashtags}}
hashtags = true
{{else}}
hashtags = false
{{/if}}
# Enable support for :colon:separated:tags:.
{{#if ColonTags}}
colon-tags = true
{{else}}
colon-tags = false
{{/if}}
# Enable support for Bear's #multi-word tags#
# Hashtags must be enabled for multi-word tags to work.
{{#if MultiwordTags}}
multiword-tags = true
{{else}}
multiword-tags = false
{{/if}}
# EXTERNAL TOOLS
[tool]
# Default editor used to open notes. When not set, the EDITOR or VISUAL
# environment variables are used.
#editor = "vim"
# Pager used to scroll through long output. If you want to disable paging
# altogether, set it to an empty string "".
#pager = "less -FIRX"
# Command used to preview a note during interactive fzf mode.
# Set it to an empty string "" to disable preview.
# bat is a great tool to render Markdown document with syntax highlighting.
#https://github.com/sharkdp/bat
#fzf-preview = "bat -p --color always {-1}"
# NAMED FILTERS
#
# A named filter is a set of note filtering options used frequently together.
#
[filter]
# Matches the notes created the last two weeks. For example:
# $ zk list recents --limit 15
# $ zk edit recents --interactive
#recents = "--sort created- --created-after 'last two weeks'"
# COMMAND ALIASES
#
# Aliases are user commands called with ` + "`" + `zk <alias> [<flags>] [<args>]` + "`" + `.
#
# The alias will be executed with ` + "`" + `$SHELL -c` + "`" + `, please refer to your shell's
# man page to see the available syntax. In most shells:
# * $@ can be used to expand all the provided flags and arguments
# * you can pipe commands together with the usual | character
#
[alias]
# Here are a few aliases to get you started.
# Shortcut to a command.
#ls = "zk list $@"
# Default flags for an existing command.
#list = "zk list --quiet $@"
# Edit the last modified note.
#editlast = "zk edit --limit 1 --sort modified- $@"
# Edit the notes selected interactively among the notes created the last two weeks.
# This alias doesn't take any argument, so we don't use $@.
#recent = "zk edit --sort created- --created-after 'last two weeks' --interactive"
# Print paths separated with colons for the notes found with the given
# arguments. This can be useful to expand a complex search query into a flag
# taking only paths. For example:
# zk list --link-to "` + "`" + `zk path -m potatoe` + "`" + `"
#path = "zk list --quiet --format \{{path}} --delimiter , $@"
# Show a random note.
#lucky = "zk list --quiet --format full --sort random --limit 1"
# Returns the Git history for the notes found with the given arguments.
# Note the use of a pipe and the location of $@.
#hist = "zk list --format path --delimiter0 --quiet $@ | xargs -t -0 git log --patch --"
# Edit this configuration file.
#conf = '$EDITOR "$ZK_NOTEBOOK_DIR/.zk/config.toml"'
`
const defaultTemplate = `# {{title}}
{{content}}
`