Filter notes by tags (#6)

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"`.
pull/7/head
Mickaël Menu 3 years ago committed by GitHub
parent 7dc3120d3a
commit 08cc4a3c3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 `;`.

@ -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

@ -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)

@ -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")

@ -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)
})
}

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

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

@ -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
---

@ -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")
})

@ -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`,
})

@ -10,3 +10,6 @@
- id: 4
kind: "tag"
name: "fantasy"
- id: 5
kind: "tag"
name: "history"

@ -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

@ -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, '<zk:match>' || l.title || '</zk:match>'), '\x01') AS snippet"
snippetCol = "GROUP_CONCAT(REPLACE(l.snippet, l.title, '<zk:match>' || l.title || '</zk:match>'), '\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, '<zk:match>', '</zk:match>', '…', 20) as snippet`
snippetCol = `snippet(notes_fts, 2, '<zk:match>', '</zk:match>', '…', 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"

@ -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",

@ -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))
}

@ -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() {}

@ -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

@ -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}}
`)

@ -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

@ -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",
},

@ -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 {

@ -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",
},

@ -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 |

@ -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 := ""

@ -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(),

Loading…
Cancel
Save