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.
<!--## 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

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

@ -24,12 +24,13 @@ type NoteDAO struct {
logger util.Logger
// Prepared SQL statements
indexedStmt *LazyStmt
addStmt *LazyStmt
updateStmt *LazyStmt
removeStmt *LazyStmt
findIdByPathStmt *LazyStmt
findByIdStmt *LazyStmt
indexedStmt *LazyStmt
addStmt *LazyStmt
updateStmt *LazyStmt
removeStmt *LazyStmt
findIdByPathStmt *LazyStmt
findIdsByPathRegexStmt *LazyStmt
findByIdStmt *LazyStmt
}
// 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 = ?
`),
// 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.
findByIdStmt: tx.PrepareLazy(`
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)
}
func (d *NoteDAO) FindIdsByHrefs(hrefs []string, allowPartialMatch bool) ([]core.NoteID, error) {
ids := make([]core.NoteID, 0)
for _, href := range hrefs {
id, err := d.FindIdByHref(href, allowPartialMatch)
if err != nil {
return ids, err
}
if id.IsValid() {
ids = append(ids, id)
}
}
func idForRow(row *sql.Row) (core.NoteID, error) {
var id sql.NullInt64
err := row.Scan(&id)
if len(ids) == 0 {
return ids, fmt.Errorf("could not find notes at: " + strings.Join(hrefs, ", "))
switch {
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) {
if allowPartialMatch {
id, err := d.FindIdByHref(href, false)
if id.IsValid() || err != nil {
return id, err
}
}
opts := core.NewNoteFindOptsByHref(href, allowPartialMatch)
rows, err := d.findRows(opts, noteSelectionID)
func (d *NoteDAO) findIdsByPathRegex(regex string) ([]core.NoteID, error) {
ids := []core.NoteID{}
rows, err := d.findIdsByPathRegexStmt.Query(regex)
if err != nil {
return 0, err
return ids, err
}
defer rows.Close()
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
err := row.Scan(&id)
err = row.Scan(&id)
switch {
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) {
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.
ids, err := d.FindIdsByHrefs(opts.Mention, true /* allowPartialMatch */)
ids, err := d.findIdsByHrefs(opts.Mention, true /* allowPartialHrefs */)
if err != nil {
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.
for _, id := range ids {
opts = opts.ExcludingID(id)
}
opts = opts.ExcludingIDs(ids)
// Find their titles.
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
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 {
return err
}
@ -472,28 +530,20 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq
}
}
if opts.IncludePaths != nil {
regexes := make([]string, 0)
for _, path := range opts.IncludePaths {
regexes = append(regexes, "n.path REGEXP ?")
if !opts.EnablePathRegexes {
path = pathRegex(path)
}
args = append(args, path)
if opts.IncludeHrefs != nil {
ids, err := d.findIdsByHrefs(opts.IncludeHrefs, opts.AllowPartialHrefs)
if err != nil {
return nil, err
}
whereExprs = append(whereExprs, strings.Join(regexes, " OR "))
opts = opts.IncludingIDs(ids)
}
if opts.ExcludePaths != nil {
regexes := make([]string, 0)
for _, path := range opts.ExcludePaths {
regexes = append(regexes, "n.path NOT REGEXP ?")
if !opts.EnablePathRegexes {
path = pathRegex(path)
}
args = append(args, path)
if opts.ExcludeHrefs != nil {
ids, err := d.findIdsByHrefs(opts.ExcludeHrefs, opts.AllowPartialHrefs)
if err != nil {
return nil, err
}
whereExprs = append(whereExprs, strings.Join(regexes, " AND "))
opts = opts.ExcludingIDs(ids)
}
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 {
ids, err := d.FindIdsByHrefs(opts.MentionedBy, true /* allowPartialMatch */)
ids, err := d.findIdsByHrefs(opts.MentionedBy, true /* allowPartialHrefs */)
if err != nil {
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.
for _, id := range ids {
opts = opts.ExcludingID(id)
}
opts = opts.ExcludingIDs(ids)
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)")
@ -562,7 +613,7 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
if opts.LinkedBy != nil {
filter := opts.LinkedBy
maxDistance = filter.MaxDistance
err := setupLinkFilter(filter.Paths, -1, filter.Negate, filter.Recursive)
err := setupLinkFilter(filter.Hrefs, -1, filter.Negate, filter.Recursive)
if err != nil {
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 {
filter := opts.LinkTo
maxDistance = filter.MaxDistance
err := setupLinkFilter(filter.Paths, 1, filter.Negate, filter.Recursive)
err := setupLinkFilter(filter.Hrefs, 1, filter.Negate, filter.Recursive)
if err != nil {
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)
}
if opts.IncludeIDs != nil {
whereExprs = append(whereExprs, "n.id IN ("+joinNoteIDs(opts.IncludeIDs, ",")+")")
}
if opts.ExcludeIDs != nil {
whereExprs = append(whereExprs, "n.id NOT IN ("+joinNoteIDs(opts.ExcludeIDs, ",")+")")
}
@ -793,20 +848,11 @@ func orderTerm(sorter core.NoteSorter) string {
return "n.title" + order
case core.NoteSortWordCount:
return "n.word_count" + order
case core.NoteSortPathLength:
return "LENGTH(path)" + order
default:
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
// (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) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
notes, err := dao.FindMinimal(core.NoteFindOpts{})
assert.Nil(t, err)
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: 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{}{
@ -272,13 +332,14 @@ func TestNoteDAOFindMinimalWithFilter(t *testing.T) {
func TestNoteDAOFindAll(t *testing.T) {
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",
})
}
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",
"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 | history", "adventure"}, []string{"ref/test/b.md", "log/2021-01-03.md"})
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{"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{"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{"-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/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/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) {
@ -434,7 +495,7 @@ func TestNoteDAOFindExactMatchCannotBeUsedWithMention(t *testing.T) {
func TestNoteDAOFindInPathAbsoluteFile(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludePaths: []string{"log/2021-01-03.md"},
IncludeHrefs: []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) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludePaths: []string{"log/2021-01"},
IncludeHrefs: []string{"log/2021-01"},
},
[]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) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludePaths: []string{"lo"},
IncludeHrefs: []string{"lo"},
AllowPartialHrefs: false,
},
[]string{},
)
testNoteDAOFindPaths(t,
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"},
)
@ -470,25 +533,25 @@ func TestNoteDAOFindInPathRequiresCompleteDirName(t *testing.T) {
func TestNoteDAOFindInMultiplePaths(t *testing.T) {
testNoteDAOFindPaths(t,
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) {
testNoteDAOFindPaths(t,
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) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
ExcludePaths: []string{"ref", "log/2021-01"},
ExcludeHrefs: []string{"ref", "log/2021-01"},
},
[]string{"f39c8.md", "log/2021-02-04.md", "index.md"},
)
@ -562,7 +625,7 @@ func TestNoteDAOFindUnlinkedMentions(t *testing.T) {
core.NoteFindOpts{
Mention: []string{"log/2021-01-03.md", "index.md"},
LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-03.md", "index.md"},
Hrefs: []string{"log/2021-01-03.md", "index.md"},
Negate: true,
},
},
@ -626,7 +689,7 @@ func TestNoteDAOFindUnlinkedMentionedBy(t *testing.T) {
core.NoteFindOpts{
MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"},
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,
},
},
@ -638,7 +701,7 @@ func TestNoteDAOFindLinkedBy(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{
Paths: []string{"f39c8.md", "log/2021-01-03"},
Hrefs: []string{"f39c8.md", "log/2021-01-03"},
Negate: false,
Recursive: false,
},
@ -651,7 +714,7 @@ func TestNoteDAOFindLinkedByRecursive(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"},
Hrefs: []string{"log/2021-01-04.md"},
Negate: false,
Recursive: true,
},
@ -664,7 +727,7 @@ func TestNoteDAOFindLinkedByRecursiveWithMaxDistance(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"},
Hrefs: []string{"log/2021-01-04.md"},
Negate: false,
Recursive: true,
MaxDistance: 2,
@ -677,7 +740,7 @@ func TestNoteDAOFindLinkedByRecursiveWithMaxDistance(t *testing.T) {
func TestNoteDAOFindLinkedByWithSnippets(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{Paths: []string{"f39c8.md"}},
LinkedBy: &core.LinkFilter{Hrefs: []string{"f39c8.md"}},
},
[]core.ContextualNote{
{
@ -733,12 +796,12 @@ func TestNoteDAOFindNotLinkedBy(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{
Paths: []string{"f39c8.md", "log/2021-01-03"},
Hrefs: []string{"f39c8.md", "log/2021-01-03"},
Negate: true,
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,
core.NoteFindOpts{
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,
Recursive: false,
},
@ -759,7 +822,7 @@ func TestNoteDAOFindLinkToRecursive(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"},
Hrefs: []string{"log/2021-01-04.md"},
Negate: false,
Recursive: true,
},
@ -772,7 +835,7 @@ func TestNoteDAOFindLinkToRecursiveWithMaxDistance(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"},
Hrefs: []string{"log/2021-01-04.md"},
Negate: false,
Recursive: true,
MaxDistance: 2,
@ -785,9 +848,9 @@ func TestNoteDAOFindLinkToRecursiveWithMaxDistance(t *testing.T) {
func TestNoteDAOFindNotLinkTo(t *testing.T) {
testNoteDAOFindPaths(t,
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) {
testNoteDAOFindPaths(t,
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{
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{
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) {
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",
})
testNoteDAOFindSort(t, core.NoteSortCreated, false, []string{
"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) {
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",
})
testNoteDAOFindSort(t, core.NoteSortModified, false, []string{
"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) {
testNoteDAOFindSort(t, core.NoteSortPath, true, []string{
"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{
"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",
})
}
func TestNoteDAOFindSortTitle(t *testing.T) {
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",
})
testNoteDAOFindSort(t, core.NoteSortTitle, false, []string{
"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) {
testNoteDAOFindSort(t, core.NoteSortWordCount, true, []string{
"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{
"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",
})
}

@ -88,3 +88,16 @@
created: "2020-11-29T08:20:18Z"
modified: "2020-11-10T08:20:18Z"
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
if paths, ok := relPaths(notebook, f.Path); ok {
opts.IncludePaths = paths
opts.IncludeHrefs = paths
}
if paths, ok := relPaths(notebook, f.Exclude); ok {
opts.ExcludePaths = paths
opts.ExcludeHrefs = paths
}
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 {
opts.LinkedBy = &core.LinkFilter{
Paths: paths,
Hrefs: paths,
Negate: false,
Recursive: f.Recursive,
MaxDistance: f.MaxDistance,
}
} else if paths, ok := relPaths(notebook, f.NoLinkedBy); ok {
opts.LinkedBy = &core.LinkFilter{
Paths: paths,
Hrefs: paths,
Negate: true,
}
}
if paths, ok := relPaths(notebook, f.LinkTo); ok {
opts.LinkTo = &core.LinkFilter{
Paths: paths,
Hrefs: paths,
Negate: false,
Recursive: f.Recursive,
MaxDistance: f.MaxDistance,
}
} else if paths, ok := relPaths(notebook, f.NoLinkTo); ok {
opts.LinkTo = &core.LinkFilter{
Paths: paths,
Hrefs: paths,
Negate: true,
}
}

@ -6,7 +6,6 @@ import (
"time"
"unicode/utf8"
"github.com/mickael-menu/zk/internal/util/icu"
"github.com/mickael-menu/zk/internal/util/opt"
)
@ -16,12 +15,15 @@ type NoteFindOpts struct {
Match opt.String
// Search for exact occurrences of the Match string.
ExactMatch bool
// Filter by note paths.
IncludePaths []string
// Filter excluding notes at the given paths.
ExcludePaths []string
// Indicates whether IncludePaths and ExcludePaths are using regexes.
EnablePathRegexes bool
// Filter by note hrefs.
IncludeHrefs []string
// Filter excluding notes at the given hrefs.
ExcludeHrefs []string
// Indicates whether href options can match any portion of a path.
// This is used for wiki links.
AllowPartialHrefs bool
// Filter including notes with the given IDs.
IncludeIDs []NoteID
// Filter excluding notes with the given IDs.
ExcludeIDs []NoteID
// Filter by tags found in the notes.
@ -34,7 +36,7 @@ type NoteFindOpts struct {
LinkedBy *LinkFilter
// Filter to select notes linking to another one.
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
// Filter to select notes having no other notes linking to them.
Orphan bool
@ -52,42 +54,31 @@ type NoteFindOpts struct {
Sorters []NoteSorter
}
// NewNoteFindOptsByHref creates a new set of filtering options to find a note
// from a link href.
// If allowPartialMatch is true, the href can match any unique sub portion of a note path.
func NewNoteFindOptsByHref(href string, allowPartialMatch bool) NoteFindOpts {
// 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]
if allowPartialMatch {
href = "(.*)" + icu.EscapePattern(href) + "(.*)"
// IncludingIDs creates a new FinderOpts after adding the given IDs to the list
// of excluded note IDs.
func (o NoteFindOpts) IncludingIDs(ids []NoteID) NoteFindOpts {
if o.IncludeIDs == nil {
o.IncludeIDs = []NoteID{}
}
return NoteFindOpts{
IncludePaths: []string{href},
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,
}
o.IncludeIDs = append(o.IncludeIDs, ids...)
return o
}
// 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.
func (o NoteFindOpts) ExcludingID(id NoteID) NoteFindOpts {
func (o NoteFindOpts) ExcludingIDs(ids []NoteID) NoteFindOpts {
if o.ExcludeIDs == nil {
o.ExcludeIDs = []NoteID{}
}
o.ExcludeIDs = append(o.ExcludeIDs, id)
o.ExcludeIDs = append(o.ExcludeIDs, ids...)
return o
}
// LinkFilter is a note filter used to select notes linking to other ones.
type LinkFilter struct {
Paths []string
Hrefs []string
Negate bool
Recursive bool
MaxDistance int
@ -115,10 +106,6 @@ const (
NoteSortTitle
// Sort by the number of words in the note bodies.
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

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

Loading…
Cancel
Save