zk/internal/core/config.go

527 lines
13 KiB
Go
Raw Normal View History

package core
2020-12-25 19:25:52 +00:00
import (
"fmt"
2020-12-31 12:44:17 +00:00
"path/filepath"
"strings"
2020-12-31 12:44:17 +00:00
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
toml "github.com/pelletier/go-toml"
2020-12-25 19:25:52 +00:00
)
// Config holds the user configuration.
2020-12-31 12:44:17 +00:00
type Config struct {
2021-02-13 21:16:11 +00:00
Note NoteConfig
Groups map[string]GroupConfig
Format FormatConfig
2021-02-13 21:16:11 +00:00
Tool ToolConfig
LSP LSPConfig
2021-03-24 20:06:32 +00:00
Filters map[string]string
2021-01-24 11:10:13 +00:00
Aliases map[string]string
2021-02-13 21:16:11 +00:00
Extra map[string]string
2021-03-17 17:04:27 +00:00
}
// NewDefaultConfig creates a new Config with the default settings.
func NewDefaultConfig() Config {
return Config{
Note: NoteConfig{
FilenameTemplate: "{{id}}",
Extension: "md",
BodyTemplatePath: opt.NullString,
Lang: "en",
DefaultTitle: "Untitled",
IDOptions: IDOptions{
Charset: CharsetAlphanum,
Length: 4,
Case: CaseLower,
},
Ignore: []string{},
2021-03-17 17:04:27 +00:00
},
Groups: map[string]GroupConfig{},
Format: FormatConfig{
Markdown: MarkdownConfig{
2021-04-18 14:37:54 +00:00
Hashtags: true,
ColonTags: false,
MultiwordTags: false,
LinkFormat: "markdown",
LinkEncodePath: true,
LinkDropExtension: true,
2021-03-17 17:04:27 +00:00
},
},
LSP: LSPConfig{
Diagnostics: LSPDiagnosticConfig{
WikiTitle: LSPDiagnosticNone,
DeadLink: LSPDiagnosticError,
},
},
Filters: map[string]string{},
Aliases: map[string]string{},
Extra: map[string]string{},
2021-03-17 17:04:27 +00:00
}
2020-12-31 12:44:17 +00:00
}
// RootGroupConfig returns the default GroupConfig for the root directory and its descendants.
func (c Config) RootGroupConfig() GroupConfig {
return GroupConfig{
Paths: []string{},
2021-02-13 21:16:11 +00:00
Note: c.Note,
Extra: c.Extra,
}
}
// GroupConfigForPath returns the GroupConfig for the group matching the given
// path relative to the notebook. Fallback on the root GroupConfig.
func (c Config) GroupConfigForPath(path string) (GroupConfig, error) {
name, err := c.GroupNameForPath(path)
if err != nil {
return GroupConfig{}, err
}
return c.GroupConfigNamed(name)
}
// GroupConfigNamed returns the GroupConfig for the group with the given name.
// An empty name matches the root GroupConfig.
func (c Config) GroupConfigNamed(name string) (GroupConfig, error) {
if name == "" {
return c.RootGroupConfig(), nil
} else {
group, ok := c.Groups[name]
if !ok {
return GroupConfig{}, fmt.Errorf("no group named `%s` found in the config", name)
}
return group, nil
2021-03-17 17:04:27 +00:00
}
}
2021-03-17 17:04:27 +00:00
// GroupNameForPath returns the name of the GroupConfig matching the given
// path, relative to the notebook.
func (c Config) GroupNameForPath(path string) (string, error) {
for name, config := range c.Groups {
for _, groupPath := range config.Paths {
matches, err := filepath.Match(groupPath, path)
if err != nil {
return "", errors.Wrapf(err, "failed to match group %s to %s", name, path)
} else if matches {
return name, nil
}
if strings.HasPrefix(path, groupPath+"/") {
return name, nil
}
2021-03-17 17:04:27 +00:00
}
}
return "", nil
2021-03-17 17:04:27 +00:00
}
// FormatConfig holds the configuration for document formats, such as Markdown.
type FormatConfig struct {
Markdown MarkdownConfig
}
// MarkdownConfig holds the configuration for Markdown documents.
type MarkdownConfig struct {
// Hashtags indicates whether #hashtags are supported.
2021-03-17 17:04:27 +00:00
Hashtags bool
// ColonTags indicates whether :colon:tags: are supported.
2021-03-17 17:04:27 +00:00
ColonTags bool
// MultiwordTags indicates whether #multi-word tags# are supported.
2021-03-17 17:04:27 +00:00
MultiwordTags bool
2021-04-18 14:37:54 +00:00
// Format used to generate links between notes.
// Either "wiki", "markdown" or a custom template. Default is "markdown".
LinkFormat string
// Indicates whether a link's path will be percent-encoded.
// Defaults to true for "markdown" format only, false otherwise.
LinkEncodePath bool
// Indicates whether a link's path file extension will be removed.
LinkDropExtension bool
}
2021-02-13 21:16:11 +00:00
// ToolConfig holds the external tooling configuration.
type ToolConfig struct {
Editor opt.String
Pager opt.String
FzfPreview opt.String
FzfLine opt.String
2021-02-13 21:16:11 +00:00
}
// LSPConfig holds the Language Server Protocol configuration.
type LSPConfig struct {
Diagnostics LSPDiagnosticConfig
}
// LSPDiagnosticConfig holds the LSP diagnostics configuration.
type LSPDiagnosticConfig struct {
WikiTitle LSPDiagnosticSeverity
DeadLink LSPDiagnosticSeverity
}
type LSPDiagnosticSeverity int
const (
LSPDiagnosticNone LSPDiagnosticSeverity = 0
LSPDiagnosticError LSPDiagnosticSeverity = 1
LSPDiagnosticWarning LSPDiagnosticSeverity = 2
LSPDiagnosticInfo LSPDiagnosticSeverity = 3
LSPDiagnosticHint LSPDiagnosticSeverity = 4
)
2021-02-13 21:16:11 +00:00
// NoteConfig holds the user configuration used when generating new notes.
type NoteConfig struct {
// Handlebars template used when generating a new filename.
2020-12-31 12:44:17 +00:00
FilenameTemplate string
2021-02-13 21:16:11 +00:00
// Extension appended to the filename.
Extension string
// Path to the handlebars template used when generating the note content.
2020-12-31 12:44:17 +00:00
BodyTemplatePath opt.String
2021-02-13 21:16:11 +00:00
// Language of the note content.
Lang string
// Default title to use when none is provided.
DefaultTitle string
// Settings used when generating a random ID.
IDOptions IDOptions
// Path globs to ignore when indexing notes.
Ignore []string
2020-12-31 12:44:17 +00:00
}
// GroupConfig holds the user configuration for a given group of notes.
type GroupConfig struct {
Paths []string
2021-02-13 21:16:11 +00:00
Note NoteConfig
Extra map[string]string
}
// IgnoreGlobs returns all the Note.Ignore path globs for the group paths,
// relative to the root of the notebook.
func (c GroupConfig) IgnoreGlobs() []string {
if len(c.Paths) == 0 {
return c.Note.Ignore
}
globs := []string{}
for _, p := range c.Paths {
for _, g := range c.Note.Ignore {
globs = append(globs, filepath.Join(p, g))
}
}
return globs
}
// Clone creates a copy of the GroupConfig receiver.
func (c GroupConfig) Clone() GroupConfig {
2020-12-31 14:54:39 +00:00
clone := c
clone.Paths = make([]string, len(c.Paths))
copy(clone.Paths, c.Paths)
2020-12-31 14:54:39 +00:00
clone.Extra = make(map[string]string)
for k, v := range c.Extra {
clone.Extra[k] = v
}
return clone
}
2021-03-17 17:04:27 +00:00
// OpenConfig creates a new Config instance from its TOML representation stored
// in the given file.
func OpenConfig(path string, parentConfig Config, fs FileStorage) (Config, error) {
// The local config is optional.
exists, err := fs.FileExists(path)
if err == nil && !exists {
return parentConfig, nil
}
content, err := fs.Read(path)
2021-03-17 17:04:27 +00:00
if err != nil {
return parentConfig, errors.Wrapf(err, "failed to open config file at %s", path)
}
return ParseConfig(content, path, parentConfig)
}
// ParseConfig creates a new Config instance from its TOML representation.
2021-03-17 17:04:27 +00:00
// path is the config absolute path, from which will be derived the base path
// for templates.
//
// The parentConfig will be used to inherit default config settings.
func ParseConfig(content []byte, path string, parentConfig Config) (Config, error) {
wrap := errors.Wrapperf("failed to read config")
2021-03-17 17:04:27 +00:00
config := parentConfig
var tomlConf tomlConfig
err := toml.Unmarshal(content, &tomlConf)
2020-12-31 12:44:17 +00:00
if err != nil {
return config, wrap(err)
2020-12-31 12:44:17 +00:00
}
2021-03-17 17:04:27 +00:00
// Note
2021-02-13 21:16:11 +00:00
note := tomlConf.Note
if note.Filename != "" {
2021-03-17 17:04:27 +00:00
config.Note.FilenameTemplate = note.Filename
2020-12-31 12:44:17 +00:00
}
2021-02-13 21:16:11 +00:00
if note.Extension != "" {
2021-03-17 17:04:27 +00:00
config.Note.Extension = note.Extension
}
2021-02-13 21:16:11 +00:00
if note.Template != "" {
2021-03-17 17:04:27 +00:00
config.Note.BodyTemplatePath = opt.NewNotEmptyString(note.Template)
2020-12-31 12:44:17 +00:00
}
2021-02-13 21:16:11 +00:00
if note.IDLength != 0 {
2021-03-17 17:04:27 +00:00
config.Note.IDOptions.Length = note.IDLength
}
2021-02-13 21:16:11 +00:00
if note.IDCharset != "" {
2021-03-17 17:04:27 +00:00
config.Note.IDOptions.Charset = charsetFromString(note.IDCharset)
}
2021-02-13 21:16:11 +00:00
if note.IDCase != "" {
2021-03-17 17:04:27 +00:00
config.Note.IDOptions.Case = caseFromString(note.IDCase)
2020-12-31 12:44:17 +00:00
}
2021-02-13 21:16:11 +00:00
if note.Lang != "" {
2021-03-17 17:04:27 +00:00
config.Note.Lang = note.Lang
}
2021-02-13 21:16:11 +00:00
if note.DefaultTitle != "" {
2021-03-17 17:04:27 +00:00
config.Note.DefaultTitle = note.DefaultTitle
}
for _, v := range note.Ignore {
config.Note.Ignore = append(config.Note.Ignore, v)
}
if tomlConf.Extra != nil {
for k, v := range tomlConf.Extra {
2021-03-17 17:04:27 +00:00
config.Extra[k] = v
2020-12-31 12:44:17 +00:00
}
}
2021-03-17 17:04:27 +00:00
// Groups
for name, dirTOML := range tomlConf.Groups {
2021-03-17 17:04:27 +00:00
parent, ok := config.Groups[name]
if !ok {
parent = config.RootGroupConfig()
}
config.Groups[name] = parent.merge(dirTOML, name)
2020-12-31 12:44:17 +00:00
}
2021-03-17 17:04:27 +00:00
// Format
markdown := tomlConf.Format.Markdown
if markdown.Hashtags != nil {
config.Format.Markdown.Hashtags = *markdown.Hashtags
}
if markdown.ColonTags != nil {
config.Format.Markdown.ColonTags = *markdown.ColonTags
}
if markdown.MultiwordTags != nil {
config.Format.Markdown.MultiwordTags = *markdown.MultiwordTags
}
2021-04-18 14:37:54 +00:00
if markdown.LinkFormat != nil && *markdown.LinkFormat == "" {
*markdown.LinkFormat = "markdown"
}
if markdown.LinkFormat != nil {
config.Format.Markdown.LinkFormat = *markdown.LinkFormat
}
if markdown.LinkEncodePath != nil {
config.Format.Markdown.LinkEncodePath = *markdown.LinkEncodePath
} else if markdown.LinkFormat != nil {
config.Format.Markdown.LinkEncodePath = (*markdown.LinkFormat == "markdown")
}
if markdown.LinkDropExtension != nil {
config.Format.Markdown.LinkDropExtension = *markdown.LinkDropExtension
}
2021-03-17 17:04:27 +00:00
// Tool
tool := tomlConf.Tool
if tool.Editor != nil {
config.Tool.Editor = opt.NewNotEmptyString(*tool.Editor)
}
if tool.Pager != nil {
config.Tool.Pager = opt.NewStringWithPtr(tool.Pager)
}
if tool.FzfPreview != nil {
config.Tool.FzfPreview = opt.NewStringWithPtr(tool.FzfPreview)
}
if tool.FzfLine != nil {
config.Tool.FzfLine = opt.NewNotEmptyString(*tool.FzfLine)
}
2021-03-17 17:04:27 +00:00
// LSP
lspDiags := tomlConf.LSP.Diagnostics
if lspDiags.WikiTitle != nil {
config.LSP.Diagnostics.WikiTitle, err = lspDiagnosticSeverityFromString(*lspDiags.WikiTitle)
if err != nil {
return config, wrap(err)
}
}
if lspDiags.DeadLink != nil {
config.LSP.Diagnostics.DeadLink, err = lspDiagnosticSeverityFromString(*lspDiags.DeadLink)
if err != nil {
return config, wrap(err)
}
}
2021-03-24 20:06:32 +00:00
// Filters
if tomlConf.Filters != nil {
for k, v := range tomlConf.Filters {
config.Filters[k] = v
}
}
2021-03-17 17:04:27 +00:00
// Aliases
if tomlConf.Aliases != nil {
for k, v := range tomlConf.Aliases {
2021-03-17 17:04:27 +00:00
config.Aliases[k] = v
2021-01-24 11:10:13 +00:00
}
2020-12-31 12:44:17 +00:00
}
2021-03-17 17:04:27 +00:00
return config, nil
2020-12-31 12:44:17 +00:00
}
2021-03-17 17:04:27 +00:00
func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string) GroupConfig {
2021-02-13 21:16:11 +00:00
res := c.Clone()
2020-12-31 12:44:17 +00:00
if tomlConf.Paths != nil {
for _, p := range tomlConf.Paths {
res.Paths = append(res.Paths, p)
}
} else {
// If no `paths` config property was given for this group, we assume
// that its name will be used as the path.
res.Paths = append(res.Paths, name)
}
2021-02-13 21:16:11 +00:00
note := tomlConf.Note
if note.Filename != "" {
res.Note.FilenameTemplate = note.Filename
2020-12-31 12:44:17 +00:00
}
2021-02-13 21:16:11 +00:00
if note.Extension != "" {
res.Note.Extension = note.Extension
}
2021-02-13 21:16:11 +00:00
if note.Template != "" {
2021-03-17 17:04:27 +00:00
res.Note.BodyTemplatePath = opt.NewNotEmptyString(note.Template)
2020-12-31 12:44:17 +00:00
}
2021-02-13 21:16:11 +00:00
if note.IDLength != 0 {
res.Note.IDOptions.Length = note.IDLength
}
2021-02-13 21:16:11 +00:00
if note.IDCharset != "" {
res.Note.IDOptions.Charset = charsetFromString(note.IDCharset)
}
2021-02-13 21:16:11 +00:00
if note.IDCase != "" {
res.Note.IDOptions.Case = caseFromString(note.IDCase)
2020-12-31 12:44:17 +00:00
}
2021-02-13 21:16:11 +00:00
if note.Lang != "" {
res.Note.Lang = note.Lang
}
2021-02-13 21:16:11 +00:00
if note.DefaultTitle != "" {
res.Note.DefaultTitle = note.DefaultTitle
}
for _, v := range note.Ignore {
res.Note.Ignore = append(res.Note.Ignore, v)
}
if tomlConf.Extra != nil {
for k, v := range tomlConf.Extra {
2020-12-31 12:44:17 +00:00
res.Extra[k] = v
}
}
2021-02-13 21:16:11 +00:00
2020-12-31 12:44:17 +00:00
return res
}
// tomlConfig holds the TOML representation of Config
type tomlConfig struct {
2021-02-13 21:16:11 +00:00
Note tomlNoteConfig
Groups map[string]tomlGroupConfig `toml:"group"`
2021-03-17 17:04:27 +00:00
Format tomlFormatConfig
2021-02-13 21:16:11 +00:00
Tool tomlToolConfig
LSP tomlLSPConfig
2021-02-13 21:16:11 +00:00
Extra map[string]string
2021-03-24 20:06:32 +00:00
Filters map[string]string `toml:"filter"`
2021-02-13 21:16:11 +00:00
Aliases map[string]string `toml:"alias"`
2020-12-25 19:25:52 +00:00
}
2021-02-13 21:16:11 +00:00
type tomlNoteConfig struct {
Filename string
Extension string
Template string
Lang string `toml:"language"`
DefaultTitle string `toml:"default-title"`
IDCharset string `toml:"id-charset"`
IDLength int `toml:"id-length"`
IDCase string `toml:"id-case"`
Ignore []string `toml:"ignore"`
2020-12-25 19:25:52 +00:00
}
type tomlGroupConfig struct {
Paths []string
2021-02-13 21:16:11 +00:00
Note tomlNoteConfig
Extra map[string]string
2020-12-26 13:49:20 +00:00
}
2021-03-17 17:04:27 +00:00
type tomlFormatConfig struct {
Markdown tomlMarkdownConfig
}
type tomlMarkdownConfig struct {
2021-04-18 14:37:54 +00:00
Hashtags *bool `toml:"hashtags"`
ColonTags *bool `toml:"colon-tags"`
MultiwordTags *bool `toml:"multiword-tags"`
LinkFormat *string `toml:"link-format"`
LinkEncodePath *bool `toml:"link-encode-path"`
LinkDropExtension *bool `toml:"link-drop-extension"`
2021-03-17 17:04:27 +00:00
}
2021-02-13 21:16:11 +00:00
type tomlToolConfig struct {
2021-03-17 17:04:27 +00:00
Editor *string
2021-02-13 21:16:11 +00:00
Pager *string
FzfPreview *string `toml:"fzf-preview"`
FzfLine *string `toml:"fzf-line"`
}
type tomlLSPConfig struct {
Diagnostics struct {
WikiTitle *string `toml:"wiki-title"`
DeadLink *string `toml:"dead-link"`
}
}
2020-12-31 12:44:17 +00:00
func charsetFromString(charset string) Charset {
switch charset {
case "alphanum":
return CharsetAlphanum
case "hex":
return CharsetHex
case "letters":
return CharsetLetters
case "numbers":
return CharsetNumbers
default:
return Charset(charset)
2020-12-25 19:25:52 +00:00
}
2020-12-31 12:44:17 +00:00
}
func caseFromString(c string) Case {
switch c {
case "lower":
return CaseLower
case "upper":
return CaseUpper
case "mixed":
return CaseMixed
default:
return CaseLower
}
}
func lspDiagnosticSeverityFromString(s string) (LSPDiagnosticSeverity, error) {
switch s {
case "", "none":
return LSPDiagnosticNone, nil
case "error":
return LSPDiagnosticError, nil
case "warning":
return LSPDiagnosticWarning, nil
case "info":
return LSPDiagnosticInfo, nil
case "hint":
return LSPDiagnosticHint, nil
default:
return LSPDiagnosticNone, fmt.Errorf("%s: unknown LSP diagnostic severity - may be none, hint, info, warning or error", s)
}
}