-
Notifications
You must be signed in to change notification settings - Fork 114
/
filtering.go
282 lines (246 loc) · 9.53 KB
/
filtering.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
package cli
import (
"fmt"
"time"
"github.com/alecthomas/kong"
"github.com/kballard/go-shellquote"
"github.com/mickael-menu/zk/internal/core"
dateutil "github.com/mickael-menu/zk/internal/util/date"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/strings"
)
// Filtering holds filtering options to select notes.
type Filtering struct {
Path []string `kong:"group='filter',arg,optional,placeholder='PATH',help='Find notes matching the given path, including its descendants.'" json:"hrefs"`
Interactive bool `kong:"group='filter',short='i',help='Select notes interactively with fzf.'" json:"-"`
Limit int `kong:"group='filter',short='n',placeholder='COUNT',help='Limit the number of notes found.'" json:"limit"`
Match string `kong:"group='filter',short='m',placeholder='QUERY',help='Terms to search for in the notes.'" json:"match"`
ExactMatch bool `kong:"group='filter',short='e',help='Search for exact occurrences of the --match argument (case insensitive).'" json:"exactMatch"`
Exclude []string `kong:"group='filter',short='x',placeholder='PATH',help='Ignore notes matching the given path, including its descendants.'" json:"excludeHrefs"`
Tag []string `kong:"group='filter',short='t',help='Find notes tagged with the given tags.'" json:"tags"`
Mention []string `kong:"group='filter',placeholder='PATH',help='Find notes mentioning the title of the given ones.'" json:"mention"`
MentionedBy []string `kong:"group='filter',placeholder='PATH',help='Find notes whose title is mentioned in the given ones.'" json:"mentionedBy"`
LinkTo []string `kong:"group='filter',short='l',placeholder='PATH',help='Find notes which are linking to the given ones.'" json:"linkTo"`
NoLinkTo []string `kong:"group='filter',placeholder='PATH',help='Find notes which are not linking to the given notes.'" json:"-"`
LinkedBy []string `kong:"group='filter',short='L',placeholder='PATH',help='Find notes which are linked by the given ones.'" json:"linkedBy"`
NoLinkedBy []string `kong:"group='filter',placeholder='PATH',help='Find notes which are not linked by the given ones.'" json:"-"`
Orphan bool `kong:"group='filter',help='Find notes which are not linked by any other note.'" json:"orphan"`
Related []string `kong:"group='filter',placeholder='PATH',help='Find notes which might be related to the given ones.'" json:"related"`
MaxDistance int `kong:"group='filter',placeholder='COUNT',help='Maximum distance between two linked notes.'" json:"maxDistance"`
Recursive bool `kong:"group='filter',short='r',help='Follow links recursively.'" json:"recursive"`
Created string `kong:"group='filter',placeholder='DATE',help:'Find notes created on the given date.'" json:"created"`
CreatedBefore string `kong:"group='filter',placeholder='DATE',help='Find notes created before the given date.'" json:"createdBefore"`
CreatedAfter string `kong:"group='filter',placeholder='DATE',help='Find notes created after the given date.'" json:"createdAfter"`
Modified string `kong:"group='filter',placeholder='DATE',help='Find notes modified on the given date.'" json:"modified"`
ModifiedBefore string `kong:"group='filter',placeholder='DATE',help='Find notes modified before the given date.'" json:"modifiedBefore"`
ModifiedAfter string `kong:"group='filter',placeholder='DATE',help='Find notes modified after the given date.'" json:"modifiedAfter"`
Sort []string `kong:"group='sort',short='s',placeholder='TERM',help='Order the notes by the given criterion.'" json:"sort"`
}
// ExpandNamedFilters expands recursively any named filter found in the Path field.
func (f Filtering) ExpandNamedFilters(filters map[string]string, expandedFilters []string) (Filtering, error) {
actualPaths := []string{}
for _, path := range f.Path {
if filter, ok := filters[path]; ok && !strings.InList(expandedFilters, path) {
wrap := errors.Wrapperf("failed to expand named filter `%v`", path)
var parsedFilter Filtering
parser, err := kong.New(&parsedFilter)
if err != nil {
return f, wrap(err)
}
args, err := shellquote.Split(filter)
if err != nil {
return f, wrap(err)
}
_, err = parser.Parse(args)
if err != nil {
return f, wrap(err)
}
// Expand recursively, but prevent infinite loops by registering
// the current filter in the list of expanded filters.
parsedFilter, err = parsedFilter.ExpandNamedFilters(filters, append(expandedFilters, path))
if err != nil {
return f, err
}
actualPaths = append(actualPaths, parsedFilter.Path...)
f.Exclude = append(f.Exclude, parsedFilter.Exclude...)
f.Tag = append(f.Tag, parsedFilter.Tag...)
f.Mention = append(f.Mention, parsedFilter.Mention...)
f.MentionedBy = append(f.MentionedBy, parsedFilter.MentionedBy...)
f.LinkTo = append(f.LinkTo, parsedFilter.LinkTo...)
f.NoLinkTo = append(f.NoLinkTo, parsedFilter.NoLinkTo...)
f.LinkedBy = append(f.LinkedBy, parsedFilter.LinkedBy...)
f.NoLinkedBy = append(f.NoLinkedBy, parsedFilter.NoLinkedBy...)
f.Related = append(f.Related, parsedFilter.Related...)
f.Sort = append(f.Sort, parsedFilter.Sort...)
f.ExactMatch = f.ExactMatch || parsedFilter.ExactMatch
f.Interactive = f.Interactive || parsedFilter.Interactive
f.Orphan = f.Orphan || parsedFilter.Orphan
f.Recursive = f.Recursive || parsedFilter.Recursive
if f.Limit == 0 {
f.Limit = parsedFilter.Limit
}
if f.MaxDistance == 0 {
f.MaxDistance = parsedFilter.MaxDistance
}
if f.Created == "" {
f.Created = parsedFilter.Created
}
if f.CreatedBefore == "" {
f.CreatedBefore = parsedFilter.CreatedBefore
}
if f.CreatedAfter == "" {
f.CreatedAfter = parsedFilter.CreatedAfter
}
if f.Modified == "" {
f.Modified = parsedFilter.Modified
}
if f.ModifiedBefore == "" {
f.ModifiedBefore = parsedFilter.ModifiedBefore
}
if f.ModifiedAfter == "" {
f.ModifiedAfter = parsedFilter.ModifiedAfter
}
if f.Match == "" {
f.Match = parsedFilter.Match
} else if parsedFilter.Match != "" {
f.Match = fmt.Sprintf("(%s) AND (%s)", f.Match, parsedFilter.Match)
}
} else {
actualPaths = append(actualPaths, path)
}
}
f.Path = actualPaths
return f, nil
}
// NewNoteFindOpts creates an instance of core.NoteFindOpts from a set of user flags.
func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts, error) {
opts := core.NoteFindOpts{}
f, err := f.ExpandNamedFilters(notebook.Config.Filters, []string{})
if err != nil {
return opts, err
}
opts.Match = opt.NewNotEmptyString(f.Match)
opts.ExactMatch = f.ExactMatch
if paths, ok := relPaths(notebook, f.Path); ok {
opts.IncludeHrefs = paths
}
if paths, ok := relPaths(notebook, f.Exclude); ok {
opts.ExcludeHrefs = paths
}
if len(f.Tag) > 0 {
opts.Tags = f.Tag
}
if len(f.Mention) > 0 {
opts.Mention = f.Mention
}
if len(f.MentionedBy) > 0 {
opts.MentionedBy = f.MentionedBy
}
if paths, ok := relPaths(notebook, f.LinkedBy); ok {
opts.LinkedBy = &core.LinkFilter{
Hrefs: paths,
Negate: false,
Recursive: f.Recursive,
MaxDistance: f.MaxDistance,
}
} else if paths, ok := relPaths(notebook, f.NoLinkedBy); ok {
opts.LinkedBy = &core.LinkFilter{
Hrefs: paths,
Negate: true,
}
}
if paths, ok := relPaths(notebook, f.LinkTo); ok {
opts.LinkTo = &core.LinkFilter{
Hrefs: paths,
Negate: false,
Recursive: f.Recursive,
MaxDistance: f.MaxDistance,
}
} else if paths, ok := relPaths(notebook, f.NoLinkTo); ok {
opts.LinkTo = &core.LinkFilter{
Hrefs: paths,
Negate: true,
}
}
if paths, ok := relPaths(notebook, f.Related); ok {
opts.Related = paths
}
opts.Orphan = f.Orphan
if f.Created != "" {
start, end, err := parseDayRange(f.Created)
if err != nil {
return opts, err
}
opts.CreatedStart = &start
opts.CreatedEnd = &end
} else {
if f.CreatedBefore != "" {
date, err := dateutil.TimeFromNatural(f.CreatedBefore)
if err != nil {
return opts, err
}
opts.CreatedEnd = &date
}
if f.CreatedAfter != "" {
date, err := dateutil.TimeFromNatural(f.CreatedAfter)
if err != nil {
return opts, err
}
opts.CreatedStart = &date
}
}
if f.Modified != "" {
start, end, err := parseDayRange(f.Modified)
if err != nil {
return opts, err
}
opts.ModifiedStart = &start
opts.ModifiedEnd = &end
} else {
if f.ModifiedBefore != "" {
date, err := dateutil.TimeFromNatural(f.ModifiedBefore)
if err != nil {
return opts, err
}
opts.ModifiedEnd = &date
}
if f.ModifiedAfter != "" {
date, err := dateutil.TimeFromNatural(f.ModifiedAfter)
if err != nil {
return opts, err
}
opts.ModifiedStart = &date
}
}
sorters, err := core.NoteSortersFromStrings(f.Sort)
if err != nil {
return opts, err
}
opts.Sorters = sorters
opts.Limit = f.Limit
return opts, nil
}
func relPaths(notebook *core.Notebook, paths []string) ([]string, bool) {
relPaths := make([]string, 0)
for _, p := range paths {
path, err := notebook.RelPath(p)
if err == nil {
relPaths = append(relPaths, path)
}
}
return relPaths, len(relPaths) > 0
}
func parseDayRange(date string) (start time.Time, end time.Time, err error) {
day, err := dateutil.TimeFromNatural(date)
if err != nil {
return
}
start = startOfDay(day)
end = start.AddDate(0, 0, 1)
return start, end, nil
}
func startOfDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}