Make filename take precedence over folders when matching sub-paths (#112)

pull/115/head
Mickaël Menu 3 years ago committed by GitHub
parent 9ae8e5b041
commit 0e88685140
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,7 +2,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
<!--## Unreleased--> ## Unreleased
### Fixed
* [#111](https://github.com/mickael-menu/zk/issues/111) Filenames take precedence over folders when matching a sub-path with wiki links.
## 0.8.0 ## 0.8.0

@ -428,7 +428,7 @@ func NewServer(opts ServerOpts) *Server {
} }
opts := core.NoteFindOpts{ opts := core.NoteFindOpts{
LinkTo: &core.LinkFilter{Paths: []string{p}}, LinkTo: &core.LinkFilter{Hrefs: []string{p}},
} }
notes, err := notebook.FindNotes(opts) notes, err := notebook.FindNotes(opts)
@ -567,7 +567,7 @@ func (s *Server) executeCommandNew(context *glsp.Context, args []interface{}) (i
return nil, err return nil, err
} }
note, err = notebook.FindNote(core.NoteFindOpts{ note, err = notebook.FindNote(core.NoteFindOpts{
IncludePaths: []string{noteExists.Name}, IncludeHrefs: []string{noteExists.Name},
}) })
if err != nil { if err != nil {
return nil, err return nil, err

@ -24,12 +24,13 @@ type NoteDAO struct {
logger util.Logger logger util.Logger
// Prepared SQL statements // Prepared SQL statements
indexedStmt *LazyStmt indexedStmt *LazyStmt
addStmt *LazyStmt addStmt *LazyStmt
updateStmt *LazyStmt updateStmt *LazyStmt
removeStmt *LazyStmt removeStmt *LazyStmt
findIdByPathStmt *LazyStmt findIdByPathStmt *LazyStmt
findByIdStmt *LazyStmt findIdsByPathRegexStmt *LazyStmt
findByIdStmt *LazyStmt
} }
// NewNoteDAO creates a new instance of a DAO working on the given database // NewNoteDAO creates a new instance of a DAO working on the given database
@ -70,6 +71,15 @@ func NewNoteDAO(tx Transaction, logger util.Logger) *NoteDAO {
WHERE path = ? WHERE path = ?
`), `),
// Find note IDs from a regex matching their path.
findIdsByPathRegexStmt: tx.PrepareLazy(`
SELECT id FROM notes
WHERE path REGEXP ?
-- To find the best match possible, we sort by path length.
-- See https://github.com/mickael-menu/zk/issues/23
ORDER BY LENGTH(path) ASC
`),
// Find a note from its ID. // Find a note from its ID.
findByIdStmt: tx.PrepareLazy(` findByIdStmt: tx.PrepareLazy(`
SELECT id, path, title, lead, body, raw_content, word_count, created, modified, metadata, checksum, tags, lead AS snippet SELECT id, path, title, lead, body, raw_content, word_count, created, modified, metadata, checksum, tags, lead AS snippet
@ -195,49 +205,49 @@ func (d *NoteDAO) FindIdByPath(path string) (core.NoteID, error) {
return idForRow(row) return idForRow(row)
} }
func (d *NoteDAO) FindIdsByHrefs(hrefs []string, allowPartialMatch bool) ([]core.NoteID, error) { func idForRow(row *sql.Row) (core.NoteID, error) {
ids := make([]core.NoteID, 0) var id sql.NullInt64
for _, href := range hrefs { err := row.Scan(&id)
id, err := d.FindIdByHref(href, allowPartialMatch)
if err != nil {
return ids, err
}
if id.IsValid() {
ids = append(ids, id)
}
}
if len(ids) == 0 { switch {
return ids, fmt.Errorf("could not find notes at: " + strings.Join(hrefs, ", ")) case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return core.NoteID(id.Int64), nil
} }
return ids, nil
} }
func (d *NoteDAO) FindIdByHref(href string, allowPartialMatch bool) (core.NoteID, error) { func (d *NoteDAO) findIdsByPathRegex(regex string) ([]core.NoteID, error) {
if allowPartialMatch { ids := []core.NoteID{}
id, err := d.FindIdByHref(href, false) rows, err := d.findIdsByPathRegexStmt.Query(regex)
if id.IsValid() || err != nil {
return id, err
}
}
opts := core.NewNoteFindOptsByHref(href, allowPartialMatch)
rows, err := d.findRows(opts, noteSelectionID)
if err != nil { if err != nil {
return 0, err return ids, err
} }
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
return d.scanNoteID(rows) var id sql.NullInt64
err := rows.Scan(&id)
if err != nil {
return ids, err
}
ids = append(ids, core.NoteID(id.Int64))
} }
return 0, nil
return ids, nil
} }
func idForRow(row *sql.Row) (core.NoteID, error) { func (d *NoteDAO) findIdWithStmt(stmt *LazyStmt, args ...interface{}) (core.NoteID, error) {
row, err := stmt.QueryRow(args...)
if err != nil {
return core.NoteID(0), err
}
var id sql.NullInt64 var id sql.NullInt64
err := row.Scan(&id) err = row.Scan(&id)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
@ -249,6 +259,53 @@ func idForRow(row *sql.Row) (core.NoteID, error) {
} }
} }
func (d *NoteDAO) FindIdByHref(href string, allowPartialHref bool) (core.NoteID, error) {
ids, err := d.FindIdsByHref(href, allowPartialHref)
if len(ids) == 0 || err != nil {
return 0, err
}
return ids[0], nil
}
func (d *NoteDAO) findIdsByHrefs(hrefs []string, allowPartialHrefs bool) ([]core.NoteID, error) {
ids := make([]core.NoteID, 0)
for _, href := range hrefs {
cids, err := d.FindIdsByHref(href, allowPartialHrefs)
if err != nil {
return ids, err
}
ids = append(ids, cids...)
}
return ids, nil
}
func (d *NoteDAO) FindIdsByHref(href string, allowPartialHref bool) ([]core.NoteID, error) {
// Remove any anchor at the end of the HREF, since it's most likely
// matching a sub-section in the note.
href = strings.SplitN(href, "#", 2)[0]
href = icu.EscapePattern(href)
if allowPartialHref {
ids, err := d.findIdsByPathRegex("^(.*/)?[^/]*" + href + "[^/]*$")
if len(ids) > 0 || err != nil {
return ids, err
}
ids, err = d.findIdsByPathRegex(".*" + href + ".*")
if len(ids) > 0 || err != nil {
return ids, err
}
}
ids, err := d.findIdsByPathRegex(href + "[^/]*|" + href + "/.+")
if len(ids) > 0 || err != nil {
return ids, err
}
return []core.NoteID{}, nil
}
func (d *NoteDAO) FindMinimal(opts core.NoteFindOpts) ([]core.MinimalNote, error) { func (d *NoteDAO) FindMinimal(opts core.NoteFindOpts) ([]core.MinimalNote, error) {
notes := make([]core.MinimalNote, 0) notes := make([]core.MinimalNote, 0)
@ -328,15 +385,16 @@ func (d *NoteDAO) expandMentionsIntoMatch(opts core.NoteFindOpts) (core.NoteFind
} }
// Find the IDs for the mentioned paths. // Find the IDs for the mentioned paths.
ids, err := d.FindIdsByHrefs(opts.Mention, true /* allowPartialMatch */) ids, err := d.findIdsByHrefs(opts.Mention, true /* allowPartialHrefs */)
if err != nil { if err != nil {
return opts, err return opts, err
} }
if len(ids) == 0 {
return opts, fmt.Errorf("could not find notes at: " + strings.Join(opts.Mention, ", "))
}
// Exclude the mentioned notes from the results. // Exclude the mentioned notes from the results.
for _, id := range ids { opts = opts.ExcludingIDs(ids)
opts = opts.ExcludingID(id)
}
// Find their titles. // Find their titles.
titlesQuery := "SELECT title, metadata FROM notes WHERE id IN (" + joinNoteIDs(ids, ",") + ")" titlesQuery := "SELECT title, metadata FROM notes WHERE id IN (" + joinNoteIDs(ids, ",") + ")"
@ -391,7 +449,7 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq
maxDistance := 0 maxDistance := 0
setupLinkFilter := func(hrefs []string, direction int, negate, recursive bool) error { setupLinkFilter := func(hrefs []string, direction int, negate, recursive bool) error {
ids, err := d.FindIdsByHrefs(hrefs, true /* allowPartialMatch */) ids, err := d.findIdsByHrefs(hrefs, true /* allowPartialHrefs */)
if err != nil { if err != nil {
return err return err
} }
@ -472,28 +530,20 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq
} }
} }
if opts.IncludePaths != nil { if opts.IncludeHrefs != nil {
regexes := make([]string, 0) ids, err := d.findIdsByHrefs(opts.IncludeHrefs, opts.AllowPartialHrefs)
for _, path := range opts.IncludePaths { if err != nil {
regexes = append(regexes, "n.path REGEXP ?") return nil, err
if !opts.EnablePathRegexes {
path = pathRegex(path)
}
args = append(args, path)
} }
whereExprs = append(whereExprs, strings.Join(regexes, " OR ")) opts = opts.IncludingIDs(ids)
} }
if opts.ExcludePaths != nil { if opts.ExcludeHrefs != nil {
regexes := make([]string, 0) ids, err := d.findIdsByHrefs(opts.ExcludeHrefs, opts.AllowPartialHrefs)
for _, path := range opts.ExcludePaths { if err != nil {
regexes = append(regexes, "n.path NOT REGEXP ?") return nil, err
if !opts.EnablePathRegexes {
path = pathRegex(path)
}
args = append(args, path)
} }
whereExprs = append(whereExprs, strings.Join(regexes, " AND ")) opts = opts.ExcludingIDs(ids)
} }
if opts.Tags != nil { if opts.Tags != nil {
@ -545,15 +595,16 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
} }
if opts.MentionedBy != nil { if opts.MentionedBy != nil {
ids, err := d.FindIdsByHrefs(opts.MentionedBy, true /* allowPartialMatch */) ids, err := d.findIdsByHrefs(opts.MentionedBy, true /* allowPartialHrefs */)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(ids) == 0 {
return nil, fmt.Errorf("could not find notes at: " + strings.Join(opts.MentionedBy, ", "))
}
// Exclude the mentioning notes from the results. // Exclude the mentioning notes from the results.
for _, id := range ids { opts = opts.ExcludingIDs(ids)
opts = opts.ExcludingID(id)
}
snippetCol = `snippet(nsrc.notes_fts, 2, '<zk:match>', '</zk:match>', '…', 20)` snippetCol = `snippet(nsrc.notes_fts, 2, '<zk:match>', '</zk:match>', '…', 20)`
joinClauses = append(joinClauses, "JOIN notes_fts nsrc ON nsrc.rowid IN ("+joinNoteIDs(ids, ",")+") AND nsrc.notes_fts MATCH mention_query(n.title, n.metadata)") joinClauses = append(joinClauses, "JOIN notes_fts nsrc ON nsrc.rowid IN ("+joinNoteIDs(ids, ",")+") AND nsrc.notes_fts MATCH mention_query(n.title, n.metadata)")
@ -562,7 +613,7 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
if opts.LinkedBy != nil { if opts.LinkedBy != nil {
filter := opts.LinkedBy filter := opts.LinkedBy
maxDistance = filter.MaxDistance maxDistance = filter.MaxDistance
err := setupLinkFilter(filter.Paths, -1, filter.Negate, filter.Recursive) err := setupLinkFilter(filter.Hrefs, -1, filter.Negate, filter.Recursive)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -571,7 +622,7 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
if opts.LinkTo != nil { if opts.LinkTo != nil {
filter := opts.LinkTo filter := opts.LinkTo
maxDistance = filter.MaxDistance maxDistance = filter.MaxDistance
err := setupLinkFilter(filter.Paths, 1, filter.Negate, filter.Recursive) err := setupLinkFilter(filter.Hrefs, 1, filter.Negate, filter.Recursive)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -612,6 +663,10 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
args = append(args, opts.ModifiedEnd) args = append(args, opts.ModifiedEnd)
} }
if opts.IncludeIDs != nil {
whereExprs = append(whereExprs, "n.id IN ("+joinNoteIDs(opts.IncludeIDs, ",")+")")
}
if opts.ExcludeIDs != nil { if opts.ExcludeIDs != nil {
whereExprs = append(whereExprs, "n.id NOT IN ("+joinNoteIDs(opts.ExcludeIDs, ",")+")") whereExprs = append(whereExprs, "n.id NOT IN ("+joinNoteIDs(opts.ExcludeIDs, ",")+")")
} }
@ -793,20 +848,11 @@ func orderTerm(sorter core.NoteSorter) string {
return "n.title" + order return "n.title" + order
case core.NoteSortWordCount: case core.NoteSortWordCount:
return "n.word_count" + order return "n.word_count" + order
case core.NoteSortPathLength:
return "LENGTH(path)" + order
default: default:
panic(fmt.Sprintf("%v: unknown core.NoteSortField", sorter.Field)) panic(fmt.Sprintf("%v: unknown core.NoteSortField", sorter.Field))
} }
} }
// pathRegex returns an ICU regex to match the files in the folder at given
// `path`, or any file having `path` for prefix.
func pathRegex(path string) string {
path = icu.EscapePattern(path)
return path + "[^/]*|" + path + "/.+"
}
// buildMentionQuery creates an FTS5 predicate to match the given note's title // buildMentionQuery creates an FTS5 predicate to match the given note's title
// (or aliases from the metadata) in the content of another note. // (or aliases from the metadata) in the content of another note.
// //

@ -226,12 +226,72 @@ func TestNoteDAORemoveCascadeLinks(t *testing.T) {
}) })
} }
func TestNoteDAOFindIdsByHref(t *testing.T) {
test := func(href string, allowPartialHref bool, expected []core.NoteID) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
actual, err := dao.FindIdsByHref(href, allowPartialHref)
assert.Nil(t, err)
assert.Equal(t, actual, expected)
})
}
test("test", false, []core.NoteID{})
test("test", true, []core.NoteID{6, 5, 8})
// Filename takes precedence over the rest of the path.
// See https://github.com/mickael-menu/zk/issues/111
test("ref", true, []core.NoteID{8})
}
func TestNoteDAOFindIncludingHrefs(t *testing.T) {
test := func(href string, allowPartialHref bool, expected []string) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludeHrefs: []string{href},
AllowPartialHrefs: allowPartialHref,
},
expected,
)
}
test("test", false, []string{})
test("test", true, []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md"})
// Filename takes precedence over the rest of the path.
// See https://github.com/mickael-menu/zk/issues/111
test("ref", true, []string{"ref/test/ref.md"})
}
func TestNoteDAOFindExcludingHrefs(t *testing.T) {
test := func(href string, allowPartialHref bool, expected []string) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
ExcludeHrefs: []string{href},
AllowPartialHrefs: allowPartialHref,
},
expected,
)
}
test("test", false, []string{"ref/test/ref.md", "ref/test/b.md",
"f39c8.md", "ref/test/a.md", "log/2021-01-03.md", "log/2021-02-04.md",
"index.md", "log/2021-01-04.md"})
test("test", true, []string{"f39c8.md", "log/2021-01-03.md",
"log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
// Filename takes precedence over the rest of the path.
// See https://github.com/mickael-menu/zk/issues/111
test("ref", true, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md",
"log/2021-01-03.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
}
func TestNoteDAOFindMinimalAll(t *testing.T) { func TestNoteDAOFindMinimalAll(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) { testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
notes, err := dao.FindMinimal(core.NoteFindOpts{}) notes, err := dao.FindMinimal(core.NoteFindOpts{})
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, notes, []core.MinimalNote{ assert.Equal(t, notes, []core.MinimalNote{
{ID: 8, Path: "ref/test/ref.md", Title: "", Metadata: map[string]interface{}{}},
{ID: 5, Path: "ref/test/b.md", Title: "A nested note", Metadata: map[string]interface{}{}}, {ID: 5, Path: "ref/test/b.md", Title: "A nested note", Metadata: map[string]interface{}{}},
{ID: 4, Path: "f39c8.md", Title: "An interesting note", Metadata: map[string]interface{}{}}, {ID: 4, Path: "f39c8.md", Title: "An interesting note", Metadata: map[string]interface{}{}},
{ID: 6, Path: "ref/test/a.md", Title: "Another nested note", Metadata: map[string]interface{}{ {ID: 6, Path: "ref/test/a.md", Title: "Another nested note", Metadata: map[string]interface{}{
@ -272,13 +332,14 @@ func TestNoteDAOFindMinimalWithFilter(t *testing.T) {
func TestNoteDAOFindAll(t *testing.T) { func TestNoteDAOFindAll(t *testing.T) {
testNoteDAOFindPaths(t, core.NoteFindOpts{}, []string{ testNoteDAOFindPaths(t, core.NoteFindOpts{}, []string{
"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md", "ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md",
"log/2021-02-04.md", "index.md", "log/2021-01-04.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md",
}) })
} }
func TestNoteDAOFindLimit(t *testing.T) { func TestNoteDAOFindLimit(t *testing.T) {
testNoteDAOFindPaths(t, core.NoteFindOpts{Limit: 2}, []string{ testNoteDAOFindPaths(t, core.NoteFindOpts{Limit: 3}, []string{
"ref/test/ref.md",
"ref/test/b.md", "ref/test/b.md",
"f39c8.md", "f39c8.md",
}) })
@ -298,9 +359,9 @@ func TestNoteDAOFindTag(t *testing.T) {
test([]string{"fiction | adventure | fantasy"}, []string{"ref/test/b.md", "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 | history", "adventure"}, []string{"ref/test/b.md", "log/2021-01-03.md"})
test([]string{"fiction", "unknown"}, []string{}) test([]string{"fiction", "unknown"}, []string{})
test([]string{"-fiction"}, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) test([]string{"-fiction"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
test([]string{"NOT fiction"}, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) test([]string{"NOT fiction"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
test([]string{"NOTfiction"}, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) test([]string{"NOTfiction"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
} }
func TestNoteDAOFindMatch(t *testing.T) { func TestNoteDAOFindMatch(t *testing.T) {
@ -434,7 +495,7 @@ func TestNoteDAOFindExactMatchCannotBeUsedWithMention(t *testing.T) {
func TestNoteDAOFindInPathAbsoluteFile(t *testing.T) { func TestNoteDAOFindInPathAbsoluteFile(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
IncludePaths: []string{"log/2021-01-03.md"}, IncludeHrefs: []string{"log/2021-01-03.md"},
}, },
[]string{"log/2021-01-03.md"}, []string{"log/2021-01-03.md"},
) )
@ -444,7 +505,7 @@ func TestNoteDAOFindInPathAbsoluteFile(t *testing.T) {
func TestNoteDAOFindInPathWithFilePrefix(t *testing.T) { func TestNoteDAOFindInPathWithFilePrefix(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
IncludePaths: []string{"log/2021-01"}, IncludeHrefs: []string{"log/2021-01"},
}, },
[]string{"log/2021-01-03.md", "log/2021-01-04.md"}, []string{"log/2021-01-03.md", "log/2021-01-04.md"},
) )
@ -454,13 +515,15 @@ func TestNoteDAOFindInPathWithFilePrefix(t *testing.T) {
func TestNoteDAOFindInPathRequiresCompleteDirName(t *testing.T) { func TestNoteDAOFindInPathRequiresCompleteDirName(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
IncludePaths: []string{"lo"}, IncludeHrefs: []string{"lo"},
AllowPartialHrefs: false,
}, },
[]string{}, []string{},
) )
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
IncludePaths: []string{"log"}, IncludeHrefs: []string{"log"},
AllowPartialHrefs: false,
}, },
[]string{"log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md"}, []string{"log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md"},
) )
@ -470,25 +533,25 @@ func TestNoteDAOFindInPathRequiresCompleteDirName(t *testing.T) {
func TestNoteDAOFindInMultiplePaths(t *testing.T) { func TestNoteDAOFindInMultiplePaths(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
IncludePaths: []string{"ref", "index.md"}, IncludeHrefs: []string{"ref", "index.md"},
}, },
[]string{"ref/test/b.md", "ref/test/a.md", "index.md"}, []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md"},
) )
} }
func TestNoteDAOFindExcludingPath(t *testing.T) { func TestNoteDAOFindExcludingPath(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
ExcludePaths: []string{"log"}, ExcludeHrefs: []string{"log"},
}, },
[]string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "index.md"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "index.md"},
) )
} }
func TestNoteDAOFindExcludingMultiplePaths(t *testing.T) { func TestNoteDAOFindExcludingMultiplePaths(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
ExcludePaths: []string{"ref", "log/2021-01"}, ExcludeHrefs: []string{"ref", "log/2021-01"},
}, },
[]string{"f39c8.md", "log/2021-02-04.md", "index.md"}, []string{"f39c8.md", "log/2021-02-04.md", "index.md"},
) )
@ -562,7 +625,7 @@ func TestNoteDAOFindUnlinkedMentions(t *testing.T) {
core.NoteFindOpts{ core.NoteFindOpts{
Mention: []string{"log/2021-01-03.md", "index.md"}, Mention: []string{"log/2021-01-03.md", "index.md"},
LinkTo: &core.LinkFilter{ LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-03.md", "index.md"}, Hrefs: []string{"log/2021-01-03.md", "index.md"},
Negate: true, Negate: true,
}, },
}, },
@ -626,7 +689,7 @@ func TestNoteDAOFindUnlinkedMentionedBy(t *testing.T) {
core.NoteFindOpts{ core.NoteFindOpts{
MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"}, MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"},
LinkedBy: &core.LinkFilter{ LinkedBy: &core.LinkFilter{
Paths: []string{"ref/test/b.md", "log/2021-01-04.md"}, Hrefs: []string{"ref/test/b.md", "log/2021-01-04.md"},
Negate: true, Negate: true,
}, },
}, },
@ -638,7 +701,7 @@ func TestNoteDAOFindLinkedBy(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
LinkedBy: &core.LinkFilter{ LinkedBy: &core.LinkFilter{
Paths: []string{"f39c8.md", "log/2021-01-03"}, Hrefs: []string{"f39c8.md", "log/2021-01-03"},
Negate: false, Negate: false,
Recursive: false, Recursive: false,
}, },
@ -651,7 +714,7 @@ func TestNoteDAOFindLinkedByRecursive(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
LinkedBy: &core.LinkFilter{ LinkedBy: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"}, Hrefs: []string{"log/2021-01-04.md"},
Negate: false, Negate: false,
Recursive: true, Recursive: true,
}, },
@ -664,7 +727,7 @@ func TestNoteDAOFindLinkedByRecursiveWithMaxDistance(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
LinkedBy: &core.LinkFilter{ LinkedBy: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"}, Hrefs: []string{"log/2021-01-04.md"},
Negate: false, Negate: false,
Recursive: true, Recursive: true,
MaxDistance: 2, MaxDistance: 2,
@ -677,7 +740,7 @@ func TestNoteDAOFindLinkedByRecursiveWithMaxDistance(t *testing.T) {
func TestNoteDAOFindLinkedByWithSnippets(t *testing.T) { func TestNoteDAOFindLinkedByWithSnippets(t *testing.T) {
testNoteDAOFind(t, testNoteDAOFind(t,
core.NoteFindOpts{ core.NoteFindOpts{
LinkedBy: &core.LinkFilter{Paths: []string{"f39c8.md"}}, LinkedBy: &core.LinkFilter{Hrefs: []string{"f39c8.md"}},
}, },
[]core.ContextualNote{ []core.ContextualNote{
{ {
@ -733,12 +796,12 @@ func TestNoteDAOFindNotLinkedBy(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
LinkedBy: &core.LinkFilter{ LinkedBy: &core.LinkFilter{
Paths: []string{"f39c8.md", "log/2021-01-03"}, Hrefs: []string{"f39c8.md", "log/2021-01-03"},
Negate: true, Negate: true,
Recursive: false, Recursive: false,
}, },
}, },
[]string{"ref/test/b.md", "f39c8.md", "log/2021-02-04.md", "index.md"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "log/2021-02-04.md", "index.md"},
) )
} }
@ -746,7 +809,7 @@ func TestNoteDAOFindLinkTo(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
LinkTo: &core.LinkFilter{ LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-04", "ref/test/a.md"}, Hrefs: []string{"log/2021-01-04", "ref/test/a.md"},
Negate: false, Negate: false,
Recursive: false, Recursive: false,
}, },
@ -759,7 +822,7 @@ func TestNoteDAOFindLinkToRecursive(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
LinkTo: &core.LinkFilter{ LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"}, Hrefs: []string{"log/2021-01-04.md"},
Negate: false, Negate: false,
Recursive: true, Recursive: true,
}, },
@ -772,7 +835,7 @@ func TestNoteDAOFindLinkToRecursiveWithMaxDistance(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
LinkTo: &core.LinkFilter{ LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"}, Hrefs: []string{"log/2021-01-04.md"},
Negate: false, Negate: false,
Recursive: true, Recursive: true,
MaxDistance: 2, MaxDistance: 2,
@ -785,9 +848,9 @@ func TestNoteDAOFindLinkToRecursiveWithMaxDistance(t *testing.T) {
func TestNoteDAOFindNotLinkTo(t *testing.T) { func TestNoteDAOFindNotLinkTo(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{ core.NoteFindOpts{
LinkTo: &core.LinkFilter{Paths: []string{"log/2021-01-04", "ref/test/a.md"}, Negate: true}, LinkTo: &core.LinkFilter{Hrefs: []string{"log/2021-01-04", "ref/test/a.md"}, Negate: true},
}, },
[]string{"ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}, []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"},
) )
} }
@ -810,7 +873,7 @@ func TestNoteDAOFindRelated(t *testing.T) {
func TestNoteDAOFindOrphan(t *testing.T) { func TestNoteDAOFindOrphan(t *testing.T) {
testNoteDAOFindPaths(t, testNoteDAOFindPaths(t,
core.NoteFindOpts{Orphan: true}, core.NoteFindOpts{Orphan: true},
[]string{"ref/test/b.md", "log/2021-02-04.md"}, []string{"ref/test/ref.md", "ref/test/b.md", "log/2021-02-04.md"},
) )
} }
@ -832,7 +895,7 @@ func TestNoteDAOFindCreatedBefore(t *testing.T) {
core.NoteFindOpts{ core.NoteFindOpts{
CreatedEnd: &end, CreatedEnd: &end,
}, },
[]string{"ref/test/b.md", "ref/test/a.md"}, []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md"},
) )
} }
@ -864,7 +927,7 @@ func TestNoteDAOFindModifiedBefore(t *testing.T) {
core.NoteFindOpts{ core.NoteFindOpts{
ModifiedEnd: &end, ModifiedEnd: &end,
}, },
[]string{"ref/test/b.md", "ref/test/a.md", "index.md"}, []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md"},
) )
} }
@ -880,55 +943,55 @@ func TestNoteDAOFindModifiedAfter(t *testing.T) {
func TestNoteDAOFindSortCreated(t *testing.T) { func TestNoteDAOFindSortCreated(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortCreated, true, []string{ testNoteDAOFindSort(t, core.NoteSortCreated, true, []string{
"ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md", "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md",
"log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md", "log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md",
}) })
testNoteDAOFindSort(t, core.NoteSortCreated, false, []string{ testNoteDAOFindSort(t, core.NoteSortCreated, false, []string{
"log/2021-02-04.md", "log/2021-01-04.md", "log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md", "log/2021-01-03.md",
"f39c8.md", "index.md", "ref/test/b.md", "ref/test/a.md", "f39c8.md", "index.md", "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md",
}) })
} }
func TestNoteDAOFindSortModified(t *testing.T) { func TestNoteDAOFindSortModified(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortModified, true, []string{ testNoteDAOFindSort(t, core.NoteSortModified, true, []string{
"ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md", "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md",
"log/2021-02-04.md", "log/2021-01-03.md", "log/2021-01-04.md", "log/2021-02-04.md", "log/2021-01-03.md", "log/2021-01-04.md",
}) })
testNoteDAOFindSort(t, core.NoteSortModified, false, []string{ testNoteDAOFindSort(t, core.NoteSortModified, false, []string{
"log/2021-01-04.md", "log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md", "log/2021-01-03.md", "log/2021-02-04.md",
"f39c8.md", "index.md", "ref/test/b.md", "ref/test/a.md", "f39c8.md", "index.md", "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md",
}) })
} }
func TestNoteDAOFindSortPath(t *testing.T) { func TestNoteDAOFindSortPath(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortPath, true, []string{ testNoteDAOFindSort(t, core.NoteSortPath, true, []string{
"f39c8.md", "index.md", "log/2021-01-03.md", "log/2021-01-04.md", "f39c8.md", "index.md", "log/2021-01-03.md", "log/2021-01-04.md",
"log/2021-02-04.md", "ref/test/a.md", "ref/test/b.md", "log/2021-02-04.md", "ref/test/a.md", "ref/test/b.md", "ref/test/ref.md",
}) })
testNoteDAOFindSort(t, core.NoteSortPath, false, []string{ testNoteDAOFindSort(t, core.NoteSortPath, false, []string{
"ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md", "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md",
"log/2021-01-04.md", "log/2021-01-03.md", "index.md", "f39c8.md", "log/2021-01-04.md", "log/2021-01-03.md", "index.md", "f39c8.md",
}) })
} }
func TestNoteDAOFindSortTitle(t *testing.T) { func TestNoteDAOFindSortTitle(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortTitle, true, []string{ testNoteDAOFindSort(t, core.NoteSortTitle, true, []string{
"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md", "ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md",
"log/2021-02-04.md", "index.md", "log/2021-01-04.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md",
}) })
testNoteDAOFindSort(t, core.NoteSortTitle, false, []string{ testNoteDAOFindSort(t, core.NoteSortTitle, false, []string{
"log/2021-01-04.md", "index.md", "log/2021-02-04.md", "log/2021-01-04.md", "index.md", "log/2021-02-04.md",
"log/2021-01-03.md", "ref/test/a.md", "f39c8.md", "ref/test/b.md", "log/2021-01-03.md", "ref/test/a.md", "f39c8.md", "ref/test/b.md", "ref/test/ref.md",
}) })
} }
func TestNoteDAOFindSortWordCount(t *testing.T) { func TestNoteDAOFindSortWordCount(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortWordCount, true, []string{ testNoteDAOFindSort(t, core.NoteSortWordCount, true, []string{
"log/2021-01-03.md", "log/2021-02-04.md", "index.md", "log/2021-01-03.md", "log/2021-02-04.md", "index.md",
"log/2021-01-04.md", "f39c8.md", "ref/test/a.md", "ref/test/b.md", "log/2021-01-04.md", "ref/test/ref.md", "f39c8.md", "ref/test/a.md", "ref/test/b.md",
}) })
testNoteDAOFindSort(t, core.NoteSortWordCount, false, []string{ testNoteDAOFindSort(t, core.NoteSortWordCount, false, []string{
"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "ref/test/b.md", "ref/test/ref.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md",
"index.md", "log/2021-01-04.md", "log/2021-01-03.md", "index.md", "log/2021-01-04.md", "log/2021-01-03.md",
}) })
} }

@ -88,3 +88,16 @@
created: "2020-11-29T08:20:18Z" created: "2020-11-29T08:20:18Z"
modified: "2020-11-10T08:20:18Z" modified: "2020-11-10T08:20:18Z"
metadata: "{}" metadata: "{}"
- id: 8
path: "ref/test/ref.md"
sortable_path: "ref/ref.md"
title: ""
lead: ""
body: ""
raw_content: ""
word_count: 5
checksum: "ientrs"
created: "2019-11-20T20:32:56Z"
modified: "2019-11-20T20:34:06Z"
metadata: '{}'

@ -142,11 +142,11 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts,
opts.ExactMatch = f.ExactMatch opts.ExactMatch = f.ExactMatch
if paths, ok := relPaths(notebook, f.Path); ok { if paths, ok := relPaths(notebook, f.Path); ok {
opts.IncludePaths = paths opts.IncludeHrefs = paths
} }
if paths, ok := relPaths(notebook, f.Exclude); ok { if paths, ok := relPaths(notebook, f.Exclude); ok {
opts.ExcludePaths = paths opts.ExcludeHrefs = paths
} }
if len(f.Tag) > 0 { if len(f.Tag) > 0 {
@ -163,28 +163,28 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts,
if paths, ok := relPaths(notebook, f.LinkedBy); ok { if paths, ok := relPaths(notebook, f.LinkedBy); ok {
opts.LinkedBy = &core.LinkFilter{ opts.LinkedBy = &core.LinkFilter{
Paths: paths, Hrefs: paths,
Negate: false, Negate: false,
Recursive: f.Recursive, Recursive: f.Recursive,
MaxDistance: f.MaxDistance, MaxDistance: f.MaxDistance,
} }
} else if paths, ok := relPaths(notebook, f.NoLinkedBy); ok { } else if paths, ok := relPaths(notebook, f.NoLinkedBy); ok {
opts.LinkedBy = &core.LinkFilter{ opts.LinkedBy = &core.LinkFilter{
Paths: paths, Hrefs: paths,
Negate: true, Negate: true,
} }
} }
if paths, ok := relPaths(notebook, f.LinkTo); ok { if paths, ok := relPaths(notebook, f.LinkTo); ok {
opts.LinkTo = &core.LinkFilter{ opts.LinkTo = &core.LinkFilter{
Paths: paths, Hrefs: paths,
Negate: false, Negate: false,
Recursive: f.Recursive, Recursive: f.Recursive,
MaxDistance: f.MaxDistance, MaxDistance: f.MaxDistance,
} }
} else if paths, ok := relPaths(notebook, f.NoLinkTo); ok { } else if paths, ok := relPaths(notebook, f.NoLinkTo); ok {
opts.LinkTo = &core.LinkFilter{ opts.LinkTo = &core.LinkFilter{
Paths: paths, Hrefs: paths,
Negate: true, Negate: true,
} }
} }

@ -6,7 +6,6 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/mickael-menu/zk/internal/util/icu"
"github.com/mickael-menu/zk/internal/util/opt" "github.com/mickael-menu/zk/internal/util/opt"
) )
@ -16,12 +15,15 @@ type NoteFindOpts struct {
Match opt.String Match opt.String
// Search for exact occurrences of the Match string. // Search for exact occurrences of the Match string.
ExactMatch bool ExactMatch bool
// Filter by note paths. // Filter by note hrefs.
IncludePaths []string IncludeHrefs []string
// Filter excluding notes at the given paths. // Filter excluding notes at the given hrefs.
ExcludePaths []string ExcludeHrefs []string
// Indicates whether IncludePaths and ExcludePaths are using regexes. // Indicates whether href options can match any portion of a path.
EnablePathRegexes bool // This is used for wiki links.
AllowPartialHrefs bool
// Filter including notes with the given IDs.
IncludeIDs []NoteID
// Filter excluding notes with the given IDs. // Filter excluding notes with the given IDs.
ExcludeIDs []NoteID ExcludeIDs []NoteID
// Filter by tags found in the notes. // Filter by tags found in the notes.
@ -34,7 +36,7 @@ type NoteFindOpts struct {
LinkedBy *LinkFilter LinkedBy *LinkFilter
// Filter to select notes linking to another one. // Filter to select notes linking to another one.
LinkTo *LinkFilter LinkTo *LinkFilter
// Filter to select notes which could might be related to the given notes paths. // Filter to select notes which could might be related to the given notes hrefs.
Related []string Related []string
// Filter to select notes having no other notes linking to them. // Filter to select notes having no other notes linking to them.
Orphan bool Orphan bool
@ -52,42 +54,31 @@ type NoteFindOpts struct {
Sorters []NoteSorter Sorters []NoteSorter
} }
// NewNoteFindOptsByHref creates a new set of filtering options to find a note // IncludingIDs creates a new FinderOpts after adding the given IDs to the list
// from a link href. // of excluded note IDs.
// If allowPartialMatch is true, the href can match any unique sub portion of a note path. func (o NoteFindOpts) IncludingIDs(ids []NoteID) NoteFindOpts {
func NewNoteFindOptsByHref(href string, allowPartialMatch bool) NoteFindOpts { if o.IncludeIDs == nil {
// Remove any anchor at the end of the HREF, since it's most likely o.IncludeIDs = []NoteID{}
// matching a sub-section in the note.
href = strings.SplitN(href, "#", 2)[0]
if allowPartialMatch {
href = "(.*)" + icu.EscapePattern(href) + "(.*)"
} }
return NoteFindOpts{ o.IncludeIDs = append(o.IncludeIDs, ids...)
IncludePaths: []string{href}, return o
EnablePathRegexes: allowPartialMatch,
// To find the best match possible, we sort by path length.
// See https://github.com/mickael-menu/zk/issues/23
Sorters: []NoteSorter{{Field: NoteSortPathLength, Ascending: true}},
Limit: 1,
}
} }
// ExcludingID creates a new FinderOpts after adding the given ID to the list // ExcludingIDs creates a new FinderOpts after adding the given IDs to the list
// of excluded note IDs. // of excluded note IDs.
func (o NoteFindOpts) ExcludingID(id NoteID) NoteFindOpts { func (o NoteFindOpts) ExcludingIDs(ids []NoteID) NoteFindOpts {
if o.ExcludeIDs == nil { if o.ExcludeIDs == nil {
o.ExcludeIDs = []NoteID{} o.ExcludeIDs = []NoteID{}
} }
o.ExcludeIDs = append(o.ExcludeIDs, id) o.ExcludeIDs = append(o.ExcludeIDs, ids...)
return o return o
} }
// LinkFilter is a note filter used to select notes linking to other ones. // LinkFilter is a note filter used to select notes linking to other ones.
type LinkFilter struct { type LinkFilter struct {
Paths []string Hrefs []string
Negate bool Negate bool
Recursive bool Recursive bool
MaxDistance int MaxDistance int
@ -115,10 +106,6 @@ const (
NoteSortTitle NoteSortTitle
// Sort by the number of words in the note bodies. // Sort by the number of words in the note bodies.
NoteSortWordCount NoteSortWordCount
// Sort by the length of the note path.
// This is not accessible to the user but used for technical reasons, to
// find the best match when searching a path prefix.
NoteSortPathLength
) )
// NoteSortersFromStrings returns a list of NoteSorter from their string // NoteSortersFromStrings returns a list of NoteSorter from their string

@ -224,9 +224,12 @@ func (n *Notebook) FindMinimalNote(opts NoteFindOpts) (*MinimalNote, error) {
} }
// FindByHref retrieves the first note matching the given link href. // FindByHref retrieves the first note matching the given link href.
// If allowPartialMatch is true, the href can match any unique sub portion of a note path. // If allowPartialHref is true, the href can match any unique sub portion of a note path.
func (n *Notebook) FindByHref(href string, allowPartialMatch bool) (*MinimalNote, error) { func (n *Notebook) FindByHref(href string, allowPartialHref bool) (*MinimalNote, error) {
return n.FindMinimalNote(NewNoteFindOptsByHref(href, allowPartialMatch)) return n.FindMinimalNote(NoteFindOpts{
IncludeHrefs: []string{href},
AllowPartialHrefs: allowPartialHref,
})
} }
// FindMatching retrieves the first note matching the given search terms. // FindMatching retrieves the first note matching the given search terms.

Loading…
Cancel
Save