diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6cc0ab0..10fed8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,9 @@ All notable changes to this project will be documented in this file.
### Added
* Support for tags.
- * Many tag flavors are supported: `#hashtags`, `:colon:separated:tags:` and even Bear's [`#multi-word tags#`](https://blog.bear.app/2017/11/bear-tips-how-to-create-multi-word-tags/). If you prefer to use a YAML frontmatter, list your tags with the keys `tags` or `keywords`.
+ * Filter notes by their tags using `--tag "history, europe"`. To match notes associated with either tags, use a pipe `|` or `OR` (all caps), e.g. `--tag "inbox OR todo"`.
+ * Many tag flavors are supported: `#hashtags`, `:colon:separated:tags:` and even Bear's [`#multi-word tags#`](https://blog.bear.app/2017/11/bear-tips-how-to-create-multi-word-tags/). If you prefer to use a YAML frontmatter, list your tags with the key `tags` or `keywords`.
### Changed
* Multiple `--extra` variables are now separated by `,` instead of `;`.
-
diff --git a/README.md b/README.md
index cd5cf1e..08af72e 100644
--- a/README.md
+++ b/README.md
@@ -22,11 +22,7 @@ What `zk` is not:
* a note editor
* a tool to serve your notes on the web – for this, you may be interested in [Neuron](docs/neuron.md) or [Gollum](https://github.com/gollum/gollum).
-## Roadmap
-
-* [ ] Tags
-* [ ] Link relations
-* [ ] Extended YAML front matter support
+[See the changelog](CHANGELOG.md) for the list of upcoming features waiting to be released.
## Install
diff --git a/adapter/handlebars/handlebars.go b/adapter/handlebars/handlebars.go
index e565041..80863fc 100644
--- a/adapter/handlebars/handlebars.go
+++ b/adapter/handlebars/handlebars.go
@@ -15,6 +15,7 @@ import (
func Init(lang string, supportsUTF8 bool, logger util.Logger, styler style.Styler) {
helpers.RegisterConcat()
helpers.RegisterDate(logger)
+ helpers.RegisterJoin()
helpers.RegisterList(supportsUTF8)
helpers.RegisterPrepend(logger)
helpers.RegisterShell(logger)
diff --git a/adapter/handlebars/handlebars_test.go b/adapter/handlebars/handlebars_test.go
index 57e3393..6300f44 100644
--- a/adapter/handlebars/handlebars_test.go
+++ b/adapter/handlebars/handlebars_test.go
@@ -94,6 +94,18 @@ func TestConcatHelper(t *testing.T) {
testString(t, "{{concat '> ' 'A quote'}}", nil, "> A quote")
}
+func TestJoinHelper(t *testing.T) {
+ test := func(items []string, expected string) {
+ context := map[string]interface{}{"items": items}
+ testString(t, "{{join items '-'}}", context, expected)
+ }
+
+ test([]string{}, "")
+ test([]string{"Item 1"}, "Item 1")
+ test([]string{"Item 1", "Item 2"}, "Item 1-Item 2")
+ test([]string{"Item 1", "Item 2", "Item 3"}, "Item 1-Item 2-Item 3")
+}
+
func TestPrependHelper(t *testing.T) {
// inline
testString(t, "{{prepend '> ' 'A quote'}}", nil, "> A quote")
diff --git a/adapter/handlebars/helpers/join.go b/adapter/handlebars/helpers/join.go
new file mode 100644
index 0000000..2427591
--- /dev/null
+++ b/adapter/handlebars/helpers/join.go
@@ -0,0 +1,17 @@
+package helpers
+
+import (
+ "strings"
+
+ "github.com/aymerick/raymond"
+)
+
+// RegisterJoin registers a {{join}} template helper which concatenates list
+// items with the given separator.
+//
+// {{join list ', '}} -> item1, item2, item3
+func RegisterJoin() {
+ raymond.RegisterHelper("join", func(list []string, delimiter string) string {
+ return strings.Join(list, delimiter)
+ })
+}
diff --git a/adapter/markdown/extensions/tag.go b/adapter/markdown/extensions/tag.go
index 366eb09..d33ecb7 100644
--- a/adapter/markdown/extensions/tag.go
+++ b/adapter/markdown/extensions/tag.go
@@ -156,6 +156,7 @@ func (p *hashtagParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
}
}
+ tag = strings.TrimSpace(tag)
if len(tag) == 0 || !isValidHashTag(tag) {
return nil
}
@@ -220,6 +221,7 @@ func (p *colontagParser) Parse(parent ast.Node, block text.Reader, pc parser.Con
escaping = true
} else if char == ':' {
+ tag = strings.TrimSpace(tag)
if len(tag) == 0 {
break
}
diff --git a/adapter/markdown/markdown.go b/adapter/markdown/markdown.go
index 0369dc7..c5917ab 100644
--- a/adapter/markdown/markdown.go
+++ b/adapter/markdown/markdown.go
@@ -166,8 +166,18 @@ func parseTags(frontmatter frontmatter, root ast.Node, source []byte) ([]string,
findFMTags := func(key string) []string {
if tags, ok := frontmatter.getStrings(key); ok {
return tags
+
} else if tags := frontmatter.getString(key); !tags.IsNull() {
- return strings.Fields(tags.Unwrap())
+ // Parse a space-separated string list
+ res := []string{}
+ for _, s := range strings.Fields(tags.Unwrap()) {
+ s = strings.TrimSpace(s)
+ if len(s) > 0 {
+ res = append(res, s)
+ }
+ }
+ return res
+
} else {
return []string{}
}
@@ -304,11 +314,14 @@ func (m frontmatter) getStrings(keys ...string) ([]string, bool) {
key = strings.ToLower(key)
if val, ok := m.values[key]; ok {
if val, ok := val.([]interface{}); ok {
- strings := []string{}
+ strs := []string{}
for _, v := range val {
- strings = append(strings, fmt.Sprint(v))
+ s := strings.TrimSpace(fmt.Sprint(v))
+ if len(s) > 0 {
+ strs = append(strs, s)
+ }
}
- return strings, true
+ return strs, true
}
}
}
diff --git a/adapter/markdown/markdown_test.go b/adapter/markdown/markdown_test.go
index 5d022cc..ca876ae 100644
--- a/adapter/markdown/markdown_test.go
+++ b/adapter/markdown/markdown_test.go
@@ -217,6 +217,8 @@ func TestParseWordtags(t *testing.T) {
test("#a/@'~-_$%&+=: end", []string{"a/@'~-_$%&+=:"})
// Escape punctuation and space
test(`#an\ \\espaced\ tag\!`, []string{`an \espaced tag!`})
+ // Leading and trailing spaces are trimmed
+ test(`#\ \ tag\ \ end`, []string{`tag`})
// Hashtags containing only numbers and dots are invalid
test("#123, #1.2.3", []string{})
// Must not be preceded by a hash or any other valid hashtag character
@@ -265,6 +267,8 @@ func TestParseColontags(t *testing.T) {
test(":#a/@'~-_$%&+=: end", []string{"#a/@'~-_$%&+="})
// Escape punctuation and space
test(`:an\ \\espaced\ tag\!:`, []string{`an \espaced tag!`})
+ // Leading and trailing spaces are trimmed
+ test(`:\ \ tag\ \ :`, []string{`tag`})
// A colontag containing only numbers is valid
test(":123:1.2.3:", []string{"123"})
// Must not be preceded by a : or any other valid colontag character
@@ -308,9 +312,9 @@ Body
`, []string{"keyword1", "keyword 2"})
test(`---
-tags: [tag1, tag 2]
+tags: [tag1, " tag 2 "]
keywords:
- - keyword1
+ - keyword1
- keyword 2
---
@@ -319,7 +323,7 @@ Body
// When a string, parse space-separated tags.
test(`---
-Tags: "tag1 #tag-2"
+Tags: "tag1 #tag-2"
Keywords: kw1 kw2 kw3
---
diff --git a/adapter/sqlite/collection_dao_test.go b/adapter/sqlite/collection_dao_test.go
index 3108559..22e3a32 100644
--- a/adapter/sqlite/collection_dao_test.go
+++ b/adapter/sqlite/collection_dao_test.go
@@ -26,7 +26,7 @@ func TestCollectionDAOFindOrCreate(t *testing.T) {
// Creates when not found
sql := "SELECT id FROM collections WHERE kind = ? AND name = ?"
assertNotExist(t, tx, sql, "unknown", "created")
- id, err = dao.FindOrCreate("unknown", "created")
+ _, err = dao.FindOrCreate("unknown", "created")
assert.Nil(t, err)
assertExist(t, tx, sql, "unknown", "created")
})
diff --git a/adapter/sqlite/db.go b/adapter/sqlite/db.go
index 0a111dc..cf947eb 100644
--- a/adapter/sqlite/db.go
+++ b/adapter/sqlite/db.go
@@ -4,6 +4,7 @@ import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
+ "github.com/mickael-menu/zk/core/note"
"github.com/mickael-menu/zk/util/errors"
)
@@ -137,6 +138,14 @@ func (db *DB) Migrate() error {
)`,
`CREATE INDEX IF NOT EXISTS index_notes_collections ON notes_collections (note_id, collection_id)`,
+ // View of notes with their associated metadata (e.g. tags), for simpler queries.
+ `CREATE VIEW notes_with_metadata AS
+ SELECT n.*, GROUP_CONCAT(c.name, '` + "\x01" + `') AS tags
+ FROM notes n
+ LEFT JOIN notes_collections nc ON nc.note_id = n.id
+ LEFT JOIN collections c ON nc.collection_id = c.id AND c.kind = '` + string(note.CollectionKindTag) + `'
+ GROUP BY n.id`,
+
`PRAGMA user_version = 2`,
})
diff --git a/adapter/sqlite/fixtures/default/collections.yml b/adapter/sqlite/fixtures/default/collections.yml
index 5fdc128..9051d1f 100644
--- a/adapter/sqlite/fixtures/default/collections.yml
+++ b/adapter/sqlite/fixtures/default/collections.yml
@@ -10,3 +10,6 @@
- id: 4
kind: "tag"
name: "fantasy"
+- id: 5
+ kind: "tag"
+ name: "history"
diff --git a/adapter/sqlite/fixtures/default/notes_collections.yml b/adapter/sqlite/fixtures/default/notes_collections.yml
index 34315b7..15fd887 100644
--- a/adapter/sqlite/fixtures/default/notes_collections.yml
+++ b/adapter/sqlite/fixtures/default/notes_collections.yml
@@ -1,9 +1,18 @@
- id: 1
- note_id: 1
- collection_id: 1
+ note_id: 1 # log/2021-01-03.md
+ collection_id: 1 # tag:fiction
- id: 2
- note_id: 1
- collection_id: 2
+ note_id: 1 # log/2021-01-03.md
+ collection_id: 2 # tag:adventure
- id: 3
- note_id: 2
- collection_id: 3
+ note_id: 2 # log/2021-01-04.md
+ collection_id: 3 # genre:fiction
+- id: 4
+ note_id: 5 # ref/test/b.md
+ collection_id: 2 # tag:adventure
+- id: 5
+ note_id: 4 # f39c8.md
+ collection_id: 4 # tag:fantasy
+- id: 6
+ note_id: 5 # ref/test/b.md
+ collection_id: 5 # tag:adventure
diff --git a/adapter/sqlite/note_dao.go b/adapter/sqlite/note_dao.go
index fa0592f..0304397 100644
--- a/adapter/sqlite/note_dao.go
+++ b/adapter/sqlite/note_dao.go
@@ -3,6 +3,7 @@ package sqlite
import (
"database/sql"
"fmt"
+ "regexp"
"strconv"
"strings"
"time"
@@ -293,25 +294,19 @@ func (d *NoteDAO) Find(opts note.FinderOpts) ([]note.Match, error) {
var (
id, wordCount int
title, lead, body, rawContent string
- nullableSnippets sql.NullString
+ snippets, tags sql.NullString
path, checksum string
created, modified time.Time
)
- err := rows.Scan(&id, &path, &title, &lead, &body, &rawContent, &wordCount, &created, &modified, &checksum, &nullableSnippets)
+ err := rows.Scan(&id, &path, &title, &lead, &body, &rawContent, &wordCount, &created, &modified, &checksum, &tags, &snippets)
if err != nil {
d.logger.Err(err)
continue
}
- snippets := make([]string, 0)
- if nullableSnippets.Valid && nullableSnippets.String != "" {
- snippets = strings.Split(nullableSnippets.String, "\x01")
- snippets = strutil.RemoveDuplicates(snippets)
- }
-
matches = append(matches, note.Match{
- Snippets: snippets,
+ Snippets: parseListFromNullString(snippets),
Metadata: note.Metadata{
Path: path,
Title: title,
@@ -319,6 +314,8 @@ func (d *NoteDAO) Find(opts note.FinderOpts) ([]note.Match, error) {
Body: body,
RawContent: rawContent,
WordCount: wordCount,
+ Links: []note.Link{},
+ Tags: parseListFromNullString(tags),
Created: created,
Modified: modified,
Checksum: checksum,
@@ -329,12 +326,22 @@ func (d *NoteDAO) Find(opts note.FinderOpts) ([]note.Match, error) {
return matches, nil
}
+// parseListFromNullString splits a 0-separated string.
+func parseListFromNullString(str sql.NullString) []string {
+ list := []string{}
+ if str.Valid && str.String != "" {
+ list = strings.Split(str.String, "\x01")
+ list = strutil.RemoveDuplicates(list)
+ }
+ return list
+}
+
func (d *NoteDAO) findRows(opts note.FinderOpts) (*sql.Rows, error) {
snippetCol := `n.lead`
- joinClauses := make([]string, 0)
- whereExprs := make([]string, 0)
- additionalOrderTerms := make([]string, 0)
- args := make([]interface{}, 0)
+ joinClauses := []string{}
+ whereExprs := []string{}
+ additionalOrderTerms := []string{}
+ args := []interface{}{}
groupBy := ""
transitiveClosure := false
@@ -359,7 +366,7 @@ func (d *NoteDAO) findRows(opts note.FinderOpts) (*sql.Rows, error) {
if !negate {
if direction != 0 {
- snippetCol = "GROUP_CONCAT(REPLACE(l.snippet, l.title, '' || l.title || ''), '\x01') AS snippet"
+ snippetCol = "GROUP_CONCAT(REPLACE(l.snippet, l.title, '' || l.title || ''), '\x01')"
}
joinOns := make([]string, 0)
@@ -413,7 +420,7 @@ func (d *NoteDAO) findRows(opts note.FinderOpts) (*sql.Rows, error) {
switch filter := filter.(type) {
case note.MatchFilter:
- snippetCol = `snippet(notes_fts, 2, '', '', '…', 20) as snippet`
+ snippetCol = `snippet(notes_fts, 2, '', '', '…', 20)`
joinClauses = append(joinClauses, "JOIN notes_fts ON n.id = notes_fts.rowid")
additionalOrderTerms = append(additionalOrderTerms, `bm25(notes_fts, 1000.0, 500.0, 1.0)`)
whereExprs = append(whereExprs, "notes_fts MATCH ?")
@@ -430,6 +437,33 @@ func (d *NoteDAO) findRows(opts note.FinderOpts) (*sql.Rows, error) {
}
whereExprs = append(whereExprs, strings.Join(regexes, " OR "))
+ case note.TagFilter:
+ if len(filter) == 0 {
+ break
+ }
+ separatorRegex := regexp.MustCompile(`(\ OR\ )|\|`)
+ for _, tags := range filter {
+ tags := separatorRegex.Split(tags, -1)
+
+ globs := make([]string, 0)
+ for _, tag := range tags {
+ tag = strings.TrimSpace(tag)
+ if len(tag) == 0 {
+ continue
+ }
+ globs = append(globs, "t.name GLOB ?")
+ args = append(args, tag)
+ }
+
+ whereExprs = append(whereExprs, fmt.Sprintf(`n.id IN (
+SELECT note_id FROM notes_collections
+ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
+ )`,
+ note.CollectionKindTag,
+ strings.Join(globs, " OR "),
+ ))
+ }
+
case note.ExcludePathFilter:
if len(filter) == 0 {
break
@@ -504,14 +538,14 @@ func (d *NoteDAO) findRows(opts note.FinderOpts) (*sql.Rows, error) {
query += `WITH RECURSIVE transitive_closure(source_id, target_id, title, snippet, distance, path) AS (
SELECT source_id, target_id, title, snippet,
- 1 AS distance,
+ 1 AS distance,
'.' || source_id || '.' || target_id || '.' AS path
FROM links
UNION ALL
SELECT tc.source_id, l.target_id, l.title, l.snippet,
- tc.distance + 1,
+ tc.distance + 1,
tc.path || l.target_id || '.' AS path
FROM links AS l
JOIN transitive_closure AS tc
@@ -528,9 +562,9 @@ func (d *NoteDAO) findRows(opts note.FinderOpts) (*sql.Rows, error) {
query += "\n)\n"
}
- query += "SELECT n.id, n.path, n.title, n.lead, n.body, n.raw_content, n.word_count, n.created, n.modified, n.checksum, " + snippetCol + "\n"
+ query += fmt.Sprintf("SELECT n.id, n.path, n.title, n.lead, n.body, n.raw_content, n.word_count, n.created, n.modified, n.checksum, n.tags, %s AS snippet\n", snippetCol)
- query += "FROM notes n\n"
+ query += "FROM notes_with_metadata n\n"
for _, clause := range joinClauses {
query += clause + "\n"
diff --git a/adapter/sqlite/note_dao_test.go b/adapter/sqlite/note_dao_test.go
index 48938df..a2b9508 100644
--- a/adapter/sqlite/note_dao_test.go
+++ b/adapter/sqlite/note_dao_test.go
@@ -402,6 +402,24 @@ func TestNoteDAOFindLimit(t *testing.T) {
})
}
+func TestNoteDAOFindTag(t *testing.T) {
+ test := func(tags []string, expectedPaths []string) {
+ testNoteDAOFindPaths(t, note.FinderOpts{
+ Filters: []note.Filter{note.TagFilter(tags)},
+ }, expectedPaths)
+ }
+
+ test([]string{"fiction"}, []string{"log/2021-01-03.md"})
+ test([]string{" adventure "}, []string{"ref/test/b.md", "log/2021-01-03.md"})
+ test([]string{"fiction", "adventure"}, []string{"log/2021-01-03.md"})
+ test([]string{"fiction|fantasy"}, []string{"f39c8.md", "log/2021-01-03.md"})
+ test([]string{"fiction | fantasy"}, []string{"f39c8.md", "log/2021-01-03.md"})
+ test([]string{"fiction OR fantasy"}, []string{"f39c8.md", "log/2021-01-03.md"})
+ test([]string{"fiction | adventure | fantasy"}, []string{"ref/test/b.md", "f39c8.md", "log/2021-01-03.md"})
+ test([]string{"fiction | history", "adventure"}, []string{"ref/test/b.md", "log/2021-01-03.md"})
+ test([]string{"fiction", "unknown"}, []string{})
+}
+
func TestNoteDAOFindMatch(t *testing.T) {
testNoteDAOFind(t,
note.FinderOpts{
@@ -416,6 +434,8 @@ func TestNoteDAOFindMatch(t *testing.T) {
Body: "Index of the Zettelkasten",
RawContent: "# Index\nIndex of the Zettelkasten",
WordCount: 4,
+ Links: []note.Link{},
+ Tags: []string{},
Created: time.Date(2019, 12, 4, 11, 59, 11, 0, time.UTC),
Modified: time.Date(2019, 12, 4, 12, 17, 21, 0, time.UTC),
Checksum: "iaefhv",
@@ -430,6 +450,8 @@ func TestNoteDAOFindMatch(t *testing.T) {
Body: "A third daily note",
RawContent: "# A third daily note",
WordCount: 4,
+ Links: []note.Link{},
+ Tags: []string{},
Created: time.Date(2020, 11, 29, 8, 20, 18, 0, time.UTC),
Modified: time.Date(2020, 11, 10, 8, 20, 18, 0, time.UTC),
Checksum: "earkte",
@@ -444,6 +466,8 @@ func TestNoteDAOFindMatch(t *testing.T) {
Body: "A second daily note",
RawContent: "# A second daily note",
WordCount: 4,
+ Links: []note.Link{},
+ Tags: []string{},
Created: time.Date(2020, 11, 29, 8, 20, 18, 0, time.UTC),
Modified: time.Date(2020, 11, 29, 8, 20, 18, 0, time.UTC),
Checksum: "arstde",
@@ -458,6 +482,8 @@ func TestNoteDAOFindMatch(t *testing.T) {
Body: "A daily note\n\nWith lot of content",
RawContent: "# A daily note\nA daily note\n\nWith lot of content",
WordCount: 3,
+ Links: []note.Link{},
+ Tags: []string{"fiction", "adventure"},
Created: time.Date(2020, 11, 22, 16, 27, 45, 0, time.UTC),
Modified: time.Date(2020, 11, 22, 16, 27, 45, 0, time.UTC),
Checksum: "qwfpgj",
@@ -602,7 +628,8 @@ func TestNoteDAOFindLinkedByWithSnippets(t *testing.T) {
Body: "It shall appear before b.md",
RawContent: "#Another nested note\nIt shall appear before b.md",
WordCount: 5,
- Links: nil,
+ Links: []note.Link{},
+ Tags: []string{},
Created: time.Date(2019, 11, 20, 20, 32, 56, 0, time.UTC),
Modified: time.Date(2019, 11, 20, 20, 34, 6, 0, time.UTC),
Checksum: "iecywst",
@@ -620,7 +647,8 @@ func TestNoteDAOFindLinkedByWithSnippets(t *testing.T) {
Body: "A daily note\n\nWith lot of content",
RawContent: "# A daily note\nA daily note\n\nWith lot of content",
WordCount: 3,
- Links: nil,
+ Links: []note.Link{},
+ Tags: []string{"fiction", "adventure"},
Created: time.Date(2020, 11, 22, 16, 27, 45, 0, time.UTC),
Modified: time.Date(2020, 11, 22, 16, 27, 45, 0, time.UTC),
Checksum: "qwfpgj",
diff --git a/cmd/finder_opts.go b/cmd/finder_opts.go
index 9c168dd..eb44aad 100644
--- a/cmd/finder_opts.go
+++ b/cmd/finder_opts.go
@@ -17,6 +17,7 @@ type Filtering struct {
Limit int `group:filter short:n placeholder:COUNT help:"Limit the number of notes found."`
Match string `group:filter short:m placeholder:QUERY help:"Terms to search for in the notes."`
Exclude []string `group:filter short:x placeholder:PATH help:"Ignore notes matching the given path, including its descendants."`
+ Tag []string `group:filter short:t help:"Find notes tagged with the given tags."`
Orphan bool `group:filter help:"Find notes which are not linked by any other note." xor:link`
LinkedBy []string `group:filter short:l placeholder:PATH help:"Find notes which are linked by the given ones." xor:link`
LinkingTo []string `group:filter short:L placeholder:PATH help:"Find notes which are linking to the given ones." xor:link`
@@ -53,6 +54,10 @@ func NewFinderOpts(zk *zk.Zk, filtering Filtering, sorting Sorting) (*note.Finde
filters = append(filters, note.ExcludePathFilter(excludePaths))
}
+ if len(filtering.Tag) > 0 {
+ filters = append(filters, note.TagFilter(filtering.Tag))
+ }
+
if filtering.Match != "" {
filters = append(filters, note.MatchFilter(filtering.Match))
}
diff --git a/core/note/find.go b/core/note/find.go
index c00c73d..e6b5271 100644
--- a/core/note/find.go
+++ b/core/note/find.go
@@ -44,6 +44,9 @@ type PathFilter []string
// ExcludePathFilter is a note filter using path globs to exclude notes from the list.
type ExcludePathFilter []string
+// TagFilter is a note filter using tag globs found in the notes.
+type TagFilter []string
+
// LinkedByFilter is a note filter used to select notes being linked by another one.
type LinkedByFilter struct {
Paths []string
@@ -80,6 +83,7 @@ type InteractiveFilter bool
func (f MatchFilter) sealed() {}
func (f PathFilter) sealed() {}
func (f ExcludePathFilter) sealed() {}
+func (f TagFilter) sealed() {}
func (f LinkedByFilter) sealed() {}
func (f LinkingToFilter) sealed() {}
func (f RelatedFilter) sealed() {}
diff --git a/core/note/format.go b/core/note/format.go
index a6ea64a..723a56f 100644
--- a/core/note/format.go
+++ b/core/note/format.go
@@ -79,6 +79,7 @@ Modified: {{date created "short"}}
"full": `{{style "title" title}} {{style "path" path}}
Created: {{date created "short"}}
Modified: {{date created "short"}}
+Tags: {{join tags ", "}}
{{prepend " " body}}
`,
@@ -104,6 +105,7 @@ func (f *Formatter) Format(match Match) (string, error) {
Lead: match.Lead,
Body: match.Body,
Snippets: snippets,
+ Tags: match.Tags,
RawContent: match.RawContent,
WordCount: match.WordCount,
Created: match.Created,
@@ -120,6 +122,7 @@ type formatRenderContext struct {
Snippets []string
RawContent string `handlebars:"raw-content"`
WordCount int `handlebars:"word-count"`
+ Tags []string
Created time.Time
Modified time.Time
Checksum string
diff --git a/core/note/format_test.go b/core/note/format_test.go
index f2eec7f..ce69fbf 100644
--- a/core/note/format_test.go
+++ b/core/note/format_test.go
@@ -55,6 +55,7 @@ Modified: {{date created "short"}}
test("full", `{{style "title" title}} {{style "path" path}}
Created: {{date created "short"}}
Modified: {{date created "short"}}
+Tags: {{join tags ", "}}
{{prepend " " body}}
`)
diff --git a/core/zk/config.go b/core/zk/config.go
index d3a4c93..257e54f 100644
--- a/core/zk/config.go
+++ b/core/zk/config.go
@@ -56,7 +56,7 @@ type GroupConfig struct {
Extra map[string]string
}
-// ConfigOverrides holds user configuration overriden values, for example fed
+// ConfigOverrides holds user configuration overridden values, for example fed
// from CLI flags.
type ConfigOverrides struct {
Group opt.String
@@ -79,7 +79,7 @@ func (c GroupConfig) Clone() GroupConfig {
}
// Override modifies the GroupConfig receiver by updating the properties
-// overriden in ConfigOverrides.
+// overridden in ConfigOverrides.
func (c *GroupConfig) Override(overrides ConfigOverrides) {
if !overrides.BodyTemplatePath.IsNull() {
c.Note.BodyTemplatePath = overrides.BodyTemplatePath
diff --git a/core/zk/config_test.go b/core/zk/config_test.go
index 3415f47..d54160c 100644
--- a/core/zk/config_test.go
+++ b/core/zk/config_test.go
@@ -445,9 +445,9 @@ func TestGroupConfigOverride(t *testing.T) {
// Some overrides
sut.Override(ConfigOverrides{
- BodyTemplatePath: opt.NewString("overriden-template"),
+ BodyTemplatePath: opt.NewString("overridden-template"),
Extra: map[string]string{
- "hello": "overriden",
+ "hello": "overridden",
"additional": "value",
},
})
@@ -455,7 +455,7 @@ func TestGroupConfigOverride(t *testing.T) {
Paths: []string{"path"},
Note: NoteConfig{
FilenameTemplate: "filename",
- BodyTemplatePath: opt.NewString("overriden-template"),
+ BodyTemplatePath: opt.NewString("overridden-template"),
IDOptions: IDOptions{
Length: 4,
Charset: CharsetLetters,
@@ -463,7 +463,7 @@ func TestGroupConfigOverride(t *testing.T) {
},
},
Extra: map[string]string{
- "hello": "overriden",
+ "hello": "overridden",
"salut": "le monde",
"additional": "value",
},
diff --git a/core/zk/zk.go b/core/zk/zk.go
index 5efe7c1..c5c040f 100644
--- a/core/zk/zk.go
+++ b/core/zk/zk.go
@@ -70,7 +70,7 @@ template = "default.md"
# Specify the list of directories which will automatically belong to the group
# with the optional ` + "`" + `paths` + "`" + ` property.
#
-# Omiting ` + "`" + `paths` + "`" + ` is equivalent to providing a single path equal to the name of
+# 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.
@@ -315,18 +315,18 @@ func (zk *Zk) DirAt(path string, overrides ...ConfigOverrides) (*Dir, error) {
func (zk *Zk) findConfigForDirNamed(name string, overrides []ConfigOverrides) (GroupConfig, error) {
// If there's a Group overrides, attempt to find a matching group.
- overridenGroup := ""
+ overriddenGroup := ""
for _, o := range overrides {
if !o.Group.IsNull() {
- overridenGroup = o.Group.Unwrap()
- if group, ok := zk.Config.Groups[overridenGroup]; ok {
+ overriddenGroup = o.Group.Unwrap()
+ if group, ok := zk.Config.Groups[overriddenGroup]; ok {
return group, nil
}
}
}
- if overridenGroup != "" {
- return GroupConfig{}, fmt.Errorf("%s: group not find in the config file", overridenGroup)
+ if overriddenGroup != "" {
+ return GroupConfig{}, fmt.Errorf("%s: group not find in the config file", overriddenGroup)
}
for groupName, group := range zk.Config.Groups {
diff --git a/core/zk/zk_test.go b/core/zk/zk_test.go
index da5654e..d0b8898 100644
--- a/core/zk/zk_test.go
+++ b/core/zk/zk_test.go
@@ -253,9 +253,9 @@ func TestDirAtWithOverrides(t *testing.T) {
dir, err := zk.DirAt(".",
ConfigOverrides{
- BodyTemplatePath: opt.NewString("overriden-template"),
+ BodyTemplatePath: opt.NewString("overridden-template"),
Extra: map[string]string{
- "hello": "overriden",
+ "hello": "overridden",
"additional": "value",
},
},
@@ -272,7 +272,7 @@ func TestDirAtWithOverrides(t *testing.T) {
Paths: []string{},
Note: NoteConfig{
FilenameTemplate: "{{id}}.note",
- BodyTemplatePath: opt.NewString("overriden-template"),
+ BodyTemplatePath: opt.NewString("overridden-template"),
IDOptions: IDOptions{
Length: 4,
Charset: CharsetLetters,
@@ -280,7 +280,7 @@ func TestDirAtWithOverrides(t *testing.T) {
},
},
Extra: map[string]string{
- "hello": "overriden",
+ "hello": "overridden",
"additional": "value2",
"additional2": "value3",
},
diff --git a/docs/template-format.md b/docs/template-format.md
index 2313d27..763c0a5 100644
--- a/docs/template-format.md
+++ b/docs/template-format.md
@@ -11,6 +11,7 @@ The following variables are available in the templates used when formatting note
| `snippets` | [string] | List of context-sensitive relevant excerpts from the note |
| `raw-content` | string | The full raw content of the note file |
| `word-count` | int | Number of words in the note |
+| `tags` | [string] | List of tags found in the note |
| `created` | date | Date of creation of the note |
| `modified` | date | Last date of modification of the note |
| `checksum` | string | SHA-256 checksum of the note file |
diff --git a/util/fts5/fts5.go b/util/fts5/fts5.go
index 56f6918..515b6dc 100644
--- a/util/fts5/fts5.go
+++ b/util/fts5/fts5.go
@@ -22,7 +22,7 @@ func ConvertQuery(query string) string {
')': true,
}
- // Indicates whether the current term was explicitely quoted in the query.
+ // Indicates whether the current term was explicitly quoted in the query.
inQuote := false
// Current term being read.
term := ""
diff --git a/util/paths/walk.go b/util/paths/walk.go
index e01c201..51462c8 100644
--- a/util/paths/walk.go
+++ b/util/paths/walk.go
@@ -41,11 +41,6 @@ func Walk(basePath string, extension string, logger util.Logger) <-chan Metadata
return nil
}
- curDir := filepath.Dir(path)
- if curDir == "." {
- curDir = ""
- }
-
c <- Metadata{
Path: path,
Modified: info.ModTime().UTC(),