Commit
+1015 -709 +/-14 browse
1 | diff --git a/bin/dune b/bin/dune |
2 | index ce01fde..43a0b41 100644 |
3 | --- a/bin/dune |
4 | +++ b/bin/dune |
5 | @@ -1,6 +1,8 @@ |
6 | (executable |
7 | (public_name note) |
8 | (name note) |
9 | - (libraries note_lib) |
10 | + (preprocess |
11 | + (pps ppx_jane)) |
12 | + (libraries note_lib base core dune-build-info) |
13 | (modes exe) |
14 | (ocamlopt_flags)) |
15 | diff --git a/bin/note.ml b/bin/note.ml |
16 | index 8170d1d..9c3dc9b 100644 |
17 | --- a/bin/note.ml |
18 | +++ b/bin/note.ml |
19 | @@ -1,3 +1,268 @@ |
20 | - let () = |
21 | - Note_lib.Cmd.run ; |
22 | + open Core |
23 | + open Note_lib |
24 | |
25 | + let cfg = Config.load |
26 | + |
27 | + let note_of_title title = |
28 | + sprintf {| |
29 | + --- |
30 | + title: "%s" |
31 | + --- |
32 | + |
33 | + # %s |
34 | + |} title title |> Note.of_string |
35 | + |
36 | + let rec convert_tree tree = |
37 | + let (Note.Tree (note, others)) = tree in |
38 | + let title = note.frontmatter.title in |
39 | + let title = "[" ^ title ^ "]" in |
40 | + Display.Tree.Tree (title, List.map ~f:convert_tree others) |
41 | + |
42 | + let get_notes = |
43 | + let notes = cfg.state_dir |> Note.load |> Note.flatten ~accm:[] in |
44 | + notes |
45 | + |
46 | + let get_title (note : Note.note) = note.frontmatter.title |
47 | + |
48 | + let get_tags (note : Note.note) = note.frontmatter.tags |
49 | + |
50 | + let to_keys ~kind notes = |
51 | + match kind with |
52 | + | `Title -> List.map ~f:get_title notes |
53 | + | `Tags -> List.concat (List.map ~f:get_tags notes) |
54 | + |
55 | + let search_arg kind = |
56 | + Command.Arg_type.create |
57 | + ~complete:(fun _ ~part -> |
58 | + let notes = get_notes in |
59 | + List.filter_map |
60 | + ~f:(fun key -> |
61 | + if String.is_substring ~substring:part key then Some key else None) |
62 | + (to_keys ~kind notes)) |
63 | + (fun filter -> filter) |
64 | + |
65 | + let key_arg = |
66 | + Command.Arg_type.create |
67 | + ~complete:(fun _ ~part -> |
68 | + let string_keys = List.map ~f:Config.Key.to_string Config.Key.all in |
69 | + List.filter |
70 | + ~f:(fun key -> String.is_substring ~substring:part key) |
71 | + string_keys) |
72 | + Config.Key.of_string |
73 | + |
74 | + let flag_to_op state = |
75 | + match state with true -> Note.Operator.And | false -> Note.Operator.Or |
76 | + |
77 | + let column_list_arg = |
78 | + Command.Arg_type.create (fun value -> |
79 | + List.map ~f:Config.Column.of_string (String.split ~on:',' value)) |
80 | + |
81 | + let encoding_arg = |
82 | + Command.Arg_type.create |
83 | + ~complete:(fun _ ~part -> |
84 | + let string_keys = |
85 | + List.map ~f:Config.Encoding.to_string Config.Encoding.all |
86 | + in |
87 | + List.filter |
88 | + ~f:(fun key -> String.is_substring ~substring:part key) |
89 | + string_keys) |
90 | + Config.Encoding.of_string |
91 | + |
92 | + let list_style_arg = |
93 | + Command.Arg_type.create |
94 | + ~complete:(fun _ ~part -> |
95 | + let string_keys = |
96 | + List.map ~f:Config.ListStyle.to_string Config.ListStyle.all |
97 | + in |
98 | + List.filter |
99 | + ~f:(fun key -> String.is_substring ~substring:part key) |
100 | + string_keys) |
101 | + Config.ListStyle.of_string |
102 | + |
103 | + let term_args = |
104 | + let open Command.Let_syntax in |
105 | + [%map_open |
106 | + let title = |
107 | + flag "title" |
108 | + (listed (search_arg `Title)) |
109 | + ~doc:"regular expression matching the note title" |
110 | + and tags = |
111 | + flag "tag" |
112 | + (listed (search_arg `Tags)) |
113 | + ~doc:"sequence of regular expressions matching note tags" |
114 | + in |
115 | + let term : Note.Term.t = { title; description = []; tags } in |
116 | + term] |
117 | + |
118 | + (* |
119 | + * commands |
120 | + *) |
121 | + |
122 | + let cat_note = |
123 | + let open Command.Let_syntax in |
124 | + Command.basic ~summary:"write notes to stdout" |
125 | + ~readme:(fun () -> |
126 | + {| |
127 | + Write one or more notes to stdout. By default the cat command will write every |
128 | + note to stdout as plain text however the encoding can be adjusted to yaml or |
129 | + json for consumption by other tools. |
130 | + |}) |
131 | + [%map_open |
132 | + let term = term_args |
133 | + and encoding = |
134 | + flag "encoding" |
135 | + (optional_with_default cfg.encoding encoding_arg) |
136 | + ~doc:"format [json | yaml | raw] (default: raw)" |
137 | + in |
138 | + fun () -> |
139 | + let notes = |
140 | + cfg.state_dir |> Note.load |> Note.find_many ~term ~notes:[] |
141 | + in |
142 | + List.iter |
143 | + ~f:(fun note -> |
144 | + print_endline (Note.Encoding.to_string ~style:encoding note)) |
145 | + notes] |
146 | + |
147 | + let config_show = |
148 | + Command.basic ~summary:"show the current configuration" |
149 | + (Command.Param.return (fun () -> print_string (Config.to_string cfg))) |
150 | + |
151 | + let config_get = |
152 | + let open Command.Let_syntax in |
153 | + Command.basic ~summary:"get a config value" |
154 | + [%map_open |
155 | + let key = anon ("key" %: key_arg) in |
156 | + fun () -> print_endline (Config.get cfg key)] |
157 | + |
158 | + let config_set = |
159 | + let open Command.Let_syntax in |
160 | + Command.basic ~summary:"set a config value" |
161 | + [%map_open |
162 | + let key = anon ("key" %: key_arg) and value = anon ("value" %: string) in |
163 | + fun () -> |
164 | + let cfg = Config.set cfg key value in |
165 | + Config.save cfg] |
166 | + |
167 | + let create_note = |
168 | + let open Command.Let_syntax in |
169 | + Command.basic ~summary:"create a new note" |
170 | + ~readme:(fun () -> |
171 | + {| |
172 | + Create a new note and save it to disk in your configured state_dir. The |
173 | + on_modification callback will be invoked if the file is committed to disk. |
174 | + |}) |
175 | + [%map_open |
176 | + let open_stdin = |
177 | + flag "stdin" (optional bool) |
178 | + ~doc:"read content from stdin and copy it into the note body" |
179 | + and title = anon ("title" %: string) in |
180 | + fun () -> |
181 | + let slug = Slug.next cfg.state_dir in |
182 | + match open_stdin with |
183 | + | Some _ -> |
184 | + (* reading from stdin so write directly to note *) |
185 | + let note = |
186 | + In_channel.stdin |> In_channel.input_all |> Note.of_string |
187 | + in |
188 | + slug.path |
189 | + |> Io.create ~callback:cfg.on_modification |
190 | + ~content:(Note.to_string note) |
191 | + | None -> |
192 | + let content = title |> note_of_title |> Note.to_string in |
193 | + Io.create_on_change ~callback:cfg.on_modification |
194 | + ~editor:cfg.editor ~content slug.path] |
195 | + |
196 | + let delete_note = |
197 | + let open Command.Let_syntax in |
198 | + Command.basic ~summary:"delete an existing note" |
199 | + ~readme:(fun () -> |
200 | + {| |
201 | + Delete the first note that matches the filter criteria. |
202 | + |}) |
203 | + [%map_open |
204 | + let term = term_args in |
205 | + fun () -> |
206 | + let note = cfg.state_dir |> Note.load |> Note.find_one ~term in |
207 | + match note with |
208 | + | Some note -> |
209 | + (Option.value_exn note.slug).path |
210 | + |> Io.delete ~callback:cfg.on_modification |
211 | + ~title:note.frontmatter.title |
212 | + | None -> failwith "not found"] |
213 | + |
214 | + let edit_note = |
215 | + let open Command.Let_syntax in |
216 | + Command.basic ~summary:"edit an existing note" |
217 | + ~readme:(fun () -> |
218 | + {| |
219 | + Select a note that matches the filter criteria and open it in your text editor. |
220 | + |}) |
221 | + [%map_open |
222 | + let term = term_args in |
223 | + fun () -> |
224 | + let note = cfg.state_dir |> Note.load |> Note.find_one ~term in |
225 | + match note with |
226 | + | Some note -> |
227 | + (Option.value_exn note.slug).path |
228 | + |> Io.edit ~callback:cfg.on_modification ~editor:cfg.editor |
229 | + | None -> failwith "not found"] |
230 | + |
231 | + let list_notes = |
232 | + let open Command.Let_syntax in |
233 | + Command.basic ~summary:"list existing notes" |
234 | + ~readme:(fun () -> |
235 | + {| |
236 | + List one or more notes that match the filter criteria, if no filter criteria |
237 | + is provided then all notes will be listed. |
238 | + |}) |
239 | + [%map_open |
240 | + let term = term_args |
241 | + and style = |
242 | + flag "style" |
243 | + (optional_with_default cfg.list_style list_style_arg) |
244 | + ~doc:"list style [fixed | wide | simple]" |
245 | + and columns = |
246 | + flag "columns" |
247 | + (optional_with_default cfg.column_list column_list_arg) |
248 | + ~doc:"columns to include in output" |
249 | + in |
250 | + fun () -> |
251 | + let notes = |
252 | + Note.find_many |
253 | + ~term |
254 | + ~notes:[] (Note.load cfg.state_dir) |
255 | + in |
256 | + let styles = cfg.styles in |
257 | + let cells = Note.Util.to_cells ~columns ~styles notes in |
258 | + Display.Cell.to_stdout ~style cells] |
259 | + |
260 | + let sync = |
261 | + Command.basic ~summary:"sync notes to a remote server" |
262 | + (Command.Param.return (fun () -> Sync.sync cfg.on_sync)) |
263 | + |
264 | + let tree = |
265 | + Command.basic ~summary:"tree debug command" |
266 | + (Command.Param.return (fun () -> |
267 | + cfg.state_dir |> Note.load |> convert_tree |> Display.Tree.to_string |> print_endline)) |
268 | + |
269 | + let version = |
270 | + match Build_info.V1.version () with |
271 | + | None -> "n/a" |
272 | + | Some v -> Build_info.V1.Version.to_string v |
273 | + |
274 | + let run = |
275 | + Command.run ~version ~build_info:"" |
276 | + (Command.group ~summary:"Note is a simple CLI based note taking application" |
277 | + [ |
278 | + ("cat", cat_note); |
279 | + ("create", create_note); |
280 | + ( "config", |
281 | + Command.group ~summary:"config management" |
282 | + [ ("show", config_show); ("get", config_get); ("set", config_set) ] |
283 | + ); |
284 | + ("delete", delete_note); |
285 | + ("edit", edit_note); |
286 | + ("ls", list_notes); |
287 | + ("sync", sync); |
288 | + ("tree", tree); |
289 | + ]) |
290 | diff --git a/lib/cmd.ml b/lib/cmd.ml |
291 | deleted file mode 100644 |
292 | index 2b5aae3..0000000 |
293 | --- a/lib/cmd.ml |
294 | +++ /dev/null |
295 | @@ -1,279 +0,0 @@ |
296 | - open Core |
297 | - |
298 | - let cfg = Config.load |
299 | - |
300 | - let get_notes = |
301 | - List.map |
302 | - ~f:(fun slug -> |
303 | - let content = In_channel.read_all (Slug.get_path slug) in |
304 | - Note.of_string ~content slug) |
305 | - (Slug.load cfg.state_dir) |
306 | - |
307 | - let to_keys ~kind notes = |
308 | - match kind with |
309 | - | `Title -> List.map ~f:Note.get_title notes |
310 | - | `Tags -> List.concat (List.map ~f:Note.get_tags notes) |
311 | - |
312 | - let search_arg kind = |
313 | - Command.Arg_type.create |
314 | - ~complete:(fun _ ~part -> |
315 | - let notes = get_notes in |
316 | - List.filter_map |
317 | - ~f:(fun key -> |
318 | - if String.is_substring ~substring:part key then Some key else None) |
319 | - (to_keys ~kind notes)) |
320 | - (fun filter -> Re.Str.regexp filter) |
321 | - |
322 | - let key_arg = |
323 | - Command.Arg_type.create |
324 | - ~complete:(fun _ ~part -> |
325 | - let string_keys = List.map ~f:Config.Key.to_string Config.Key.all in |
326 | - List.filter |
327 | - ~f:(fun key -> String.is_substring ~substring:part key) |
328 | - string_keys) |
329 | - Config.Key.of_string |
330 | - |
331 | - let flag_to_op state = match state with true -> Note.And | false -> Note.Or |
332 | - |
333 | - let column_list_arg = |
334 | - Command.Arg_type.create (fun value -> |
335 | - List.map ~f:Config.Column.of_string (String.split ~on:',' value)) |
336 | - |
337 | - let encoding_arg = |
338 | - Command.Arg_type.create |
339 | - ~complete:(fun _ ~part -> |
340 | - let string_keys = |
341 | - List.map ~f:Config.Encoding.to_string Config.Encoding.all |
342 | - in |
343 | - List.filter |
344 | - ~f:(fun key -> String.is_substring ~substring:part key) |
345 | - string_keys) |
346 | - Config.Encoding.of_string |
347 | - |
348 | - let list_style_arg = |
349 | - Command.Arg_type.create |
350 | - ~complete:(fun _ ~part -> |
351 | - let string_keys = |
352 | - List.map ~f:Config.ListStyle.to_string Config.ListStyle.all |
353 | - in |
354 | - List.filter |
355 | - ~f:(fun key -> String.is_substring ~substring:part key) |
356 | - string_keys) |
357 | - Config.ListStyle.of_string |
358 | - |
359 | - let filter_args = |
360 | - let open Command.Let_syntax in |
361 | - [%map_open |
362 | - let titles = |
363 | - flag "title" |
364 | - (listed (search_arg `Title)) |
365 | - ~doc:"regular expression matching the note title" |
366 | - and tags = |
367 | - flag "tag" |
368 | - (listed (search_arg `Tags)) |
369 | - ~doc:"sequence of regular expressions matching note tags" |
370 | - and operator = flag "and" no_arg ~doc:"logical AND instead of default OR" in |
371 | - (titles, tags, operator)] |
372 | - |
373 | - (* |
374 | - * commands |
375 | - *) |
376 | - |
377 | - let cat_note = |
378 | - let open Command.Let_syntax in |
379 | - Command.basic ~summary:"write notes to stdout" |
380 | - ~readme:(fun () -> |
381 | - {| |
382 | - Write one or more notes to stdout. By default the cat command will write every |
383 | - note to stdout as plain text however the encoding can be adjusted to yaml or |
384 | - json for consumption by other tools. |
385 | - |}) |
386 | - [%map_open |
387 | - let titles, tags, operator = filter_args |
388 | - and title = anon (sequence ("title" %: search_arg `Title)) |
389 | - and encoding = |
390 | - flag "encoding" |
391 | - (optional_with_default cfg.encoding encoding_arg) |
392 | - ~doc:"format [json | yaml | raw] (default: raw)" |
393 | - in |
394 | - fun () -> |
395 | - let notes = |
396 | - Note.find_many |
397 | - ~term: |
398 | - { |
399 | - titles = List.append title titles; |
400 | - tags; |
401 | - operator = flag_to_op operator; |
402 | - } |
403 | - get_notes |
404 | - in |
405 | - List.iter |
406 | - ~f:(fun note -> |
407 | - print_endline (Note.Encoding.to_string ~style:encoding note)) |
408 | - notes] |
409 | - |
410 | - let config_show = |
411 | - Command.basic ~summary:"show the current configuration" |
412 | - (Command.Param.return (fun () -> print_string (Config.to_string cfg))) |
413 | - |
414 | - let config_get = |
415 | - let open Command.Let_syntax in |
416 | - Command.basic ~summary:"get a config value" |
417 | - [%map_open |
418 | - let key = anon ("key" %: key_arg) in |
419 | - fun () -> print_endline (Config.get cfg key)] |
420 | - |
421 | - let config_set = |
422 | - let open Command.Let_syntax in |
423 | - Command.basic ~summary:"set a config value" |
424 | - [%map_open |
425 | - let key = anon ("key" %: key_arg) and value = anon ("value" %: string) in |
426 | - fun () -> |
427 | - let cfg = Config.set cfg key value in |
428 | - Config.save cfg] |
429 | - |
430 | - let create_note = |
431 | - let open Command.Let_syntax in |
432 | - Command.basic ~summary:"create a new note" |
433 | - ~readme:(fun () -> |
434 | - {| |
435 | - Create a new note and save it to disk in your configured state_dir. The |
436 | - on_modification callback will be invoked if the file is committed to disk. |
437 | - |}) |
438 | - [%map_open |
439 | - let open_stdin = |
440 | - flag "stdin" (optional bool) |
441 | - ~doc:"read content from stdin and copy it into the note body" |
442 | - and title = anon ("title" %: string) |
443 | - and tags = anon (sequence ("tag" %: string)) in |
444 | - fun () -> |
445 | - let slug = Slug.next cfg.state_dir in |
446 | - match open_stdin with |
447 | - | Some _ -> |
448 | - (* reading from stdin so write directly to note *) |
449 | - let content = In_channel.input_all In_channel.stdin in |
450 | - let note = Note.build ~tags ~content ~title slug in |
451 | - Io.create ~callback:cfg.on_modification |
452 | - ~content:(Note.to_string note) (Slug.get_path slug) |
453 | - | None -> |
454 | - let note = Note.build ~tags ~content:"" ~title slug in |
455 | - let init_content = Note.to_string note in |
456 | - Io.create_on_change ~callback:cfg.on_modification ~editor:cfg.editor |
457 | - init_content (Slug.get_path slug)] |
458 | - |
459 | - let delete_note = |
460 | - let open Command.Let_syntax in |
461 | - Command.basic ~summary:"delete an existing note" |
462 | - ~readme:(fun () -> |
463 | - {| |
464 | - Delete the first note that matches the filter criteria. |
465 | - |}) |
466 | - [%map_open |
467 | - let titles, tags, operator = filter_args |
468 | - and title = anon (sequence ("title" %: search_arg `Title)) in |
469 | - fun () -> |
470 | - let notes = get_notes in |
471 | - let note = |
472 | - Note.find_one |
473 | - ~term: |
474 | - { |
475 | - titles = List.append title titles; |
476 | - tags; |
477 | - operator = flag_to_op operator; |
478 | - } |
479 | - notes |
480 | - in |
481 | - match note with |
482 | - | Some note -> |
483 | - Io.delete ~callback:cfg.on_modification ~title:(Note.get_title note) |
484 | - (Note.get_path note) |
485 | - | None -> failwith "not found"] |
486 | - |
487 | - let edit_note = |
488 | - let open Command.Let_syntax in |
489 | - Command.basic ~summary:"edit an existing note" |
490 | - ~readme:(fun () -> |
491 | - {| |
492 | - Select a note that matches the filter criteria and open it in your text editor. |
493 | - |}) |
494 | - [%map_open |
495 | - let titles, tags, operator = filter_args |
496 | - and title = anon (sequence ("title" %: search_arg `Title)) in |
497 | - fun () -> |
498 | - let note = |
499 | - Note.find_one |
500 | - ~term: |
501 | - { |
502 | - titles = List.append title titles; |
503 | - tags; |
504 | - operator = flag_to_op operator; |
505 | - } |
506 | - get_notes |
507 | - in |
508 | - match note with |
509 | - | Some note -> |
510 | - Io.edit ~callback:cfg.on_modification ~editor:cfg.editor |
511 | - (Note.get_path note) |
512 | - | None -> failwith "not found"] |
513 | - |
514 | - let list_notes = |
515 | - let open Command.Let_syntax in |
516 | - Command.basic ~summary:"list existing notes" |
517 | - ~readme:(fun () -> |
518 | - {| |
519 | - List one or more notes that match the filter criteria, if no filter criteria |
520 | - is provided then all notes will be listed. |
521 | - |}) |
522 | - [%map_open |
523 | - let titles, tags, operator = filter_args |
524 | - and style = |
525 | - flag "style" |
526 | - (optional_with_default cfg.list_style list_style_arg) |
527 | - ~doc:"list style [fixed | wide | simple]" |
528 | - and columns = |
529 | - flag "columns" |
530 | - (optional_with_default cfg.column_list column_list_arg) |
531 | - ~doc:"columns to include in output" |
532 | - in |
533 | - fun () -> |
534 | - let notes = |
535 | - Note.find_many |
536 | - ~term:{ titles; tags; operator = flag_to_op operator } |
537 | - get_notes |
538 | - in |
539 | - let styles = cfg.styles in |
540 | - let cells = Note.to_cells ~columns ~styles notes in |
541 | - Display.to_stdout ~style cells] |
542 | - |
543 | - let sync = |
544 | - Command.basic ~summary:"sync notes to a remote server" |
545 | - (Command.Param.return (fun () -> Sync.sync cfg.on_sync)) |
546 | - |
547 | - let tree = |
548 | - Command.basic ~summary:"tree debug command" |
549 | - (Command.Param.return (fun () -> |
550 | - let notes = get_notes in |
551 | - print_endline (sprintf "%d" (List.length notes)); |
552 | - Note.dump_tree notes)) |
553 | - |
554 | - let version = |
555 | - match Build_info.V1.version () with |
556 | - | None -> "n/a" |
557 | - | Some v -> Build_info.V1.Version.to_string v |
558 | - |
559 | - let run = |
560 | - Command.run ~version ~build_info:"" |
561 | - (Command.group ~summary:"Note is a simple CLI based note taking application" |
562 | - [ |
563 | - ("cat", cat_note); |
564 | - ("create", create_note); |
565 | - ( "config", |
566 | - Command.group ~summary:"config management" |
567 | - [ ("show", config_show); ("get", config_get); ("set", config_set) ] |
568 | - ); |
569 | - ("delete", delete_note); |
570 | - ("edit", edit_note); |
571 | - ("ls", list_notes); |
572 | - ("sync", sync); |
573 | - ("tree", tree); |
574 | - ]) |
575 | diff --git a/lib/display.ml b/lib/display.ml |
576 | index 4036af7..99d4821 100644 |
577 | --- a/lib/display.ml |
578 | +++ b/lib/display.ml |
579 | @@ -5,66 +5,106 @@ type cell = string * int * int |
580 | |
581 | type row = cell list |
582 | |
583 | - let fixed_spacing cells = |
584 | - (* find the maximum cell length per column *) |
585 | - let maximum_values = |
586 | - List.fold ~init:[] |
587 | - ~f:(fun accm row -> |
588 | - List.mapi |
589 | - ~f:(fun i col -> |
590 | - let col_length = snd3 col in |
591 | - let current_max = |
592 | - match List.nth accm i with Some len -> len | None -> 0 |
593 | - in |
594 | - if col_length > current_max then col_length + 2 else current_max) |
595 | - row) |
596 | - cells |
597 | - in |
598 | - maximum_values |
599 | + module Tree = struct |
600 | + type t = Tree of (string * t list) |
601 | |
602 | - let fix_right cells = |
603 | - let widths = fixed_spacing cells in |
604 | - let term_width, _ = size () in |
605 | - let _, right = List.split_n widths 1 in |
606 | - let col_one = List.nth_exn widths 0 in |
607 | - [ col_one + (term_width - List.fold ~init:5 ~f:( + ) widths) ] @ right |
608 | + let fill ~last ~state = |
609 | + let get_padding = function true -> "│ " | false -> " " in |
610 | + let get_edge = function true -> "└──" | false -> "├──" in |
611 | + match List.length state with |
612 | + | 0 -> [] |
613 | + | 1 -> [ last |> get_edge ] |
614 | + | len -> |
615 | + let state = List.slice state 0 (len - 1) in |
616 | + let padding = List.map ~f:get_padding state in |
617 | + List.append padding [ last |> get_edge ] |
618 | |
619 | - let apply cells widths = |
620 | - (* let maximums = fixed_spacing cells in *) |
621 | - let cells = |
622 | - List.map |
623 | - ~f:(fun row -> |
624 | - List.mapi |
625 | - ~f:(fun i entry -> |
626 | - let max = List.nth_exn widths i in |
627 | - let text, length, padding = entry in |
628 | - let padding = padding + (max - length) in |
629 | - let padding = if padding > 0 then padding else 0 in |
630 | - (text, length, padding)) |
631 | - row) |
632 | - cells |
633 | - in |
634 | - List.fold ~init:[] |
635 | - ~f:(fun accm row -> |
636 | - accm |
637 | - @ [ |
638 | - List.fold ~init:"" |
639 | - ~f:(fun accm cell -> |
640 | - let text, _, padding = cell in |
641 | - String.concat [ accm; text; String.make padding ' ' ]) |
642 | - row; |
643 | - ]) |
644 | - cells |
645 | + let rec to_lines ?(state = []) ?(last = false) next = |
646 | + let (Tree next) = next in |
647 | + let title, children = next in |
648 | + match List.length children with |
649 | + | 0 -> |
650 | + (* leaf *) |
651 | + List.append (fill ~last ~state) [ title; "\n" ] |
652 | + | n_children -> |
653 | + (* node *) |
654 | + List.foldi |
655 | + ~init: |
656 | + [ List.append (fill ~last ~state) [ title; "\n" ] |> String.concat ] |
657 | + ~f:(fun i accm node -> |
658 | + let is_last = Int.equal i (n_children - 1) in |
659 | + let state = List.append state [ phys_equal is_last false ] in |
660 | + let lines = to_lines ~state ~last:is_last node in |
661 | + List.append accm lines) |
662 | + children |
663 | + |
664 | + let to_string t = |
665 | + let result = t |> to_lines |> String.concat in |
666 | + "\n" ^ result |
667 | + end |
668 | + |
669 | + module Cell = struct |
670 | + let fixed_spacing cells = |
671 | + (* find the maximum cell length per column *) |
672 | + let maximum_values = |
673 | + List.fold ~init:[] |
674 | + ~f:(fun accm row -> |
675 | + List.mapi |
676 | + ~f:(fun i col -> |
677 | + let col_length = snd3 col in |
678 | + let current_max = |
679 | + match List.nth accm i with Some len -> len | None -> 0 |
680 | + in |
681 | + if col_length > current_max then col_length + 2 else current_max) |
682 | + row) |
683 | + cells |
684 | + in |
685 | + maximum_values |
686 | + |
687 | + let fix_right cells = |
688 | + let widths = fixed_spacing cells in |
689 | + let term_width, _ = size () in |
690 | + let _, right = List.split_n widths 1 in |
691 | + let col_one = List.nth_exn widths 0 in |
692 | + [ col_one + (term_width - List.fold ~init:5 ~f:( + ) widths) ] @ right |
693 | |
694 | - let to_stdout ~style cells = |
695 | - match style with |
696 | - | `Simple -> |
697 | - List.iter |
698 | - ~f:(fun cell -> |
699 | - print_endline |
700 | - (let value = List.nth_exn cell 0 in |
701 | - let text = fst3 value in |
702 | - text)) |
703 | + let apply cells widths = |
704 | + (* let maximums = fixed_spacing cells in *) |
705 | + let cells = |
706 | + List.map |
707 | + ~f:(fun row -> |
708 | + List.mapi |
709 | + ~f:(fun i entry -> |
710 | + let max = List.nth_exn widths i in |
711 | + let text, length, padding = entry in |
712 | + let padding = padding + (max - length) in |
713 | + let padding = if padding > 0 then padding else 0 in |
714 | + (text, length, padding)) |
715 | + row) |
716 | cells |
717 | - | `Fixed -> List.iter ~f:print_endline (apply cells (fixed_spacing cells)) |
718 | - | `Wide -> List.iter ~f:print_endline (apply cells (fix_right cells)) |
719 | + in |
720 | + List.fold ~init:[] |
721 | + ~f:(fun accm row -> |
722 | + accm |
723 | + @ [ |
724 | + List.fold ~init:"" |
725 | + ~f:(fun accm cell -> |
726 | + let text, _, padding = cell in |
727 | + String.concat [ accm; text; String.make padding ' ' ]) |
728 | + row; |
729 | + ]) |
730 | + cells |
731 | + |
732 | + let to_stdout ~style cells = |
733 | + match style with |
734 | + | `Simple -> |
735 | + List.iter |
736 | + ~f:(fun cell -> |
737 | + print_endline |
738 | + (let value = List.nth_exn cell 0 in |
739 | + let text = fst3 value in |
740 | + text)) |
741 | + cells |
742 | + | `Fixed -> List.iter ~f:print_endline (apply cells (fixed_spacing cells)) |
743 | + | `Wide -> List.iter ~f:print_endline (apply cells (fix_right cells)) |
744 | + end |
745 | diff --git a/lib/io.ml b/lib/io.ml |
746 | index 13d07e6..f950e67 100644 |
747 | --- a/lib/io.ml |
748 | +++ b/lib/io.ml |
749 | @@ -4,7 +4,7 @@ let create ~callback ~content dest = |
750 | Out_channel.write_all ~data:content dest; |
751 | match callback with Some cmd -> Sys.command_exn cmd | None -> () |
752 | |
753 | - let create_on_change ~callback ~editor content dest = |
754 | + let create_on_change ~callback ~editor ~content dest = |
755 | let tmp_file = Filename.temp_file "note" ".md" in |
756 | Out_channel.write_all ~data:content tmp_file; |
757 | let command = sprintf "%s %s" editor tmp_file in |
758 | @@ -37,3 +37,7 @@ let delete ~callback ~title path = |
759 | else |
760 | print_endline "No changes made" |
761 | | None -> () |
762 | + |
763 | + |
764 | + let read path = |
765 | + In_channel.read_all path |
766 | diff --git a/lib/note.ml b/lib/note.ml |
767 | index f90f9cc..912e77c 100644 |
768 | --- a/lib/note.ml |
769 | +++ b/lib/note.ml |
770 | @@ -1,71 +1,114 @@ |
771 | open Core |
772 | |
773 | - type operator = And | Or |
774 | + module Operator = struct |
775 | + type t = And | Or |
776 | |
777 | - and term = { |
778 | - titles : Re.Str.regexp list; |
779 | - tags : Re.Str.regexp list; |
780 | - operator : operator; |
781 | - } |
782 | + let of_string = function |
783 | + | "Or" -> Or |
784 | + | "And" -> And |
785 | + | _ -> failwith "invalid operator" |
786 | |
787 | - and note = { |
788 | - frontmatter : Ezjsonm.t; |
789 | - content : string; |
790 | - markdown : Omd.doc; |
791 | - slug : Slug.t; |
792 | - parent : term option; |
793 | - } |
794 | + let to_string = function Or -> "Or" | And -> "And" |
795 | + end |
796 | |
797 | - let operator_of_string = function |
798 | - | "Or" -> Or |
799 | - | "And" -> And |
800 | - | _ -> failwith "invalid operator" |
801 | + module Term = struct |
802 | + (* TODO: almost identical to frontmatter structure *) |
803 | + type t = { |
804 | + title : string list; |
805 | + description : string list; |
806 | + tags : string list; |
807 | + } |
808 | |
809 | - let term_of_json json = |
810 | - let titles = |
811 | - match Ezjsonm.find_opt json [ "titles" ] with |
812 | - | Some titles -> List.map ~f:Re.Str.regexp (Ezjsonm.get_strings titles) |
813 | - | None -> [] |
814 | - and tags = |
815 | - match Ezjsonm.find_opt json [ "tags" ] with |
816 | - | Some tags -> List.map ~f:Re.Str.regexp (Ezjsonm.get_strings tags) |
817 | - | None -> [] |
818 | - and operator = |
819 | - match Ezjsonm.find_opt json [ "operator" ] with |
820 | - | Some operator -> operator_of_string (Ezjsonm.get_string operator) |
821 | - | None -> Or |
822 | - in |
823 | - { titles; tags; operator } |
824 | + let empty = { title = []; description = []; tags = [] } |
825 | + |
826 | + let of_json json = |
827 | + let title = |
828 | + match Ezjsonm.find_opt json [ "title" ] with |
829 | + | Some title -> Ezjsonm.get_strings title |
830 | + | None -> [] |
831 | + in |
832 | + let description = |
833 | + match Ezjsonm.find_opt json [ "description" ] with |
834 | + | Some description -> Ezjsonm.get_strings description |
835 | + | None -> [] |
836 | + in |
837 | + let tags = |
838 | + match Ezjsonm.find_opt json [ "tags" ] with |
839 | + | Some tags -> Ezjsonm.get_strings tags |
840 | + | None -> [] |
841 | + in |
842 | + { title; description; tags } |
843 | |
844 | - let build ?(description = "") ?(tags = []) ?(content = "") ~title slug = |
845 | - let frontmatter = |
846 | + let to_json term = |
847 | Ezjsonm.dict |
848 | [ |
849 | - ("title", Ezjsonm.string title); |
850 | - ("description", Ezjsonm.string description); |
851 | - ("tags", Ezjsonm.strings tags); |
852 | + ("title", Ezjsonm.strings term.title); |
853 | + ("description", Ezjsonm.strings term.description); |
854 | + ("tags", Ezjsonm.strings term.tags); |
855 | ] |
856 | - in |
857 | - let markdown = Omd.of_string content in |
858 | - { frontmatter; content; markdown; slug; parent = None } |
859 | + end |
860 | + |
861 | + module Frontmatter = struct |
862 | + type t = { |
863 | + title : string; |
864 | + description : string; |
865 | + tags : string list; |
866 | + parent : Term.t option; |
867 | + } |
868 | + |
869 | + let empty = { title = ""; description = ""; tags = []; parent = None } |
870 | + |
871 | + let of_json json = |
872 | + let title = |
873 | + match Ezjsonm.find_opt json [ "title" ] with |
874 | + | Some title -> Ezjsonm.get_string title |
875 | + | None -> "" |
876 | + in |
877 | + let description = |
878 | + match Ezjsonm.find_opt json [ "description" ] with |
879 | + | Some description -> Ezjsonm.get_string description |
880 | + | None -> "" |
881 | + in |
882 | + let tags = |
883 | + match Ezjsonm.find_opt json [ "tags" ] with |
884 | + | Some tags -> Ezjsonm.get_strings tags |
885 | + | None -> [] |
886 | + in |
887 | + let parent = |
888 | + match Ezjsonm.find_opt json [ "parent" ] with |
889 | + | Some parent -> Some (Term.of_json parent) |
890 | + | None -> None |
891 | + in |
892 | + { title; description; tags; parent } |
893 | + |
894 | + let to_json frontmatter = |
895 | + Ezjsonm.dict |
896 | + [ |
897 | + ("title", Ezjsonm.string frontmatter.title); |
898 | + ("description", Ezjsonm.string frontmatter.description); |
899 | + ("tags", Ezjsonm.strings frontmatter.tags); |
900 | + ] |
901 | + end |
902 | |
903 | - let get_title t = |
904 | - (* if title is specified use that, otherwise fall back to slug *) |
905 | - match Ezjsonm.find_opt (Ezjsonm.value t.frontmatter) [ "title" ] with |
906 | - | Some title -> Ezjsonm.get_string title |
907 | - | None -> Slug.to_string t.slug |
908 | + type note = { |
909 | + frontmatter : Frontmatter.t; |
910 | + content : string; |
911 | + slug : Slug.t option; |
912 | + } |
913 | |
914 | - let get_description t = |
915 | - match Ezjsonm.find_opt (Ezjsonm.value t.frontmatter) [ "description" ] with |
916 | - | Some description -> Ezjsonm.get_string description |
917 | - | None -> "" |
918 | + and tree = Tree of (note * tree list) |
919 | |
920 | - let get_tags t = |
921 | - match Ezjsonm.find_opt (Ezjsonm.value t.frontmatter) [ "tags" ] with |
922 | - | Some tags -> Ezjsonm.get_strings tags |
923 | - | None -> [] |
924 | + let root_template = |
925 | + {| |
926 | + --- |
927 | + title: root |
928 | + description: all of my notes decend from here |
929 | + tags: [] |
930 | + --- |
931 | |
932 | - let get_path t = Slug.get_path t.slug |
933 | + # This is a Note! |
934 | + |
935 | + |} |
936 | |
937 | let rec extract_structured_data (accm : Ezjsonm.value list) (doc : Omd.doc) : |
938 | Ezjsonm.value list = |
939 | @@ -84,56 +127,149 @@ let rec extract_structured_data (accm : Ezjsonm.value list) (doc : Omd.doc) : |
940 | | _ -> extract_structured_data accm tl) |
941 | | _ -> extract_structured_data accm tl) |
942 | |
943 | - let get_data t = |
944 | - let data = extract_structured_data [] t.markdown in |
945 | - Ezjsonm.list (fun value -> value) data |
946 | - |
947 | - let to_json t = |
948 | + let to_json note = |
949 | + let data = |
950 | + note.content |> Omd.of_string |> extract_structured_data [] |
951 | + |> Ezjsonm.list (fun value -> value) |
952 | + in |
953 | Ezjsonm.dict |
954 | [ |
955 | - ("frontmatter", Ezjsonm.value t.frontmatter); |
956 | - ("content", Ezjsonm.string t.content); |
957 | - ("data", get_data t); |
958 | + ("frontmatter", Frontmatter.to_json note.frontmatter); |
959 | + ("content", Ezjsonm.string note.content); |
960 | + ("data", data); |
961 | ] |
962 | |
963 | - let to_string t = |
964 | - let yaml = Yaml.to_string_exn (Ezjsonm.value t.frontmatter) in |
965 | - "\n---\n" ^ yaml ^ "\n---\n" ^ t.content |
966 | + let to_string note = |
967 | + let yaml = Yaml.to_string_exn (Frontmatter.to_json note.frontmatter) in |
968 | + "\n---\n" ^ yaml ^ "\n---\n" ^ note.content |
969 | |
970 | - let of_string ~content slug = |
971 | + let of_string ?(slug = None) content = |
972 | let indexes = |
973 | String.substr_index_all ~may_overlap:true ~pattern:"---" content |
974 | in |
975 | if List.length indexes >= 2 then |
976 | + (* parsing the top half of the note *) |
977 | let meta_str = |
978 | String.slice content (List.nth_exn indexes 0 + 3) (List.nth_exn indexes 1) |
979 | in |
980 | - let frontmatter : Ezjsonm.t = |
981 | - match Yaml.of_string_exn meta_str with |
982 | - | `O v -> `O v |
983 | - | `A v -> `A v |
984 | - | _ -> |
985 | - failwith |
986 | - "frontmatter is a partial fragment, should be either a dictionary \ |
987 | - or list" |
988 | - in |
989 | - let parent = Ezjsonm.find_opt (Ezjsonm.value frontmatter) [ "parent" ] in |
990 | - let parent = |
991 | - match parent with Some json -> Some (term_of_json json) | None -> None |
992 | - in |
993 | - let markdown : Omd.doc = |
994 | - Omd.of_string |
995 | - (String.slice content |
996 | - (List.nth_exn indexes 1 + 3) |
997 | - (String.length content)) |
998 | - in |
999 | - { frontmatter; content; markdown; slug; parent } |
1000 | + let frontmatter = meta_str |> Yaml.of_string_exn |> Frontmatter.of_json in |
1001 | + { frontmatter; content; slug } |
1002 | + else { frontmatter = Frontmatter.empty; content; slug } |
1003 | + |
1004 | + let rec flatten ~accm tree = |
1005 | + let (Tree (note, others)) = tree in |
1006 | + List.fold ~init:(note :: accm) ~f:(fun accm note -> flatten ~accm note) others |
1007 | + |
1008 | + let match_term ?(operator = Operator.Or) ~(term : Term.t) note = |
1009 | + let open Re.Str in |
1010 | + let titles = |
1011 | + List.map |
1012 | + ~f:(fun exp -> |
1013 | + let note_title = note.frontmatter.title in |
1014 | + string_match exp note_title 0) |
1015 | + (List.map ~f:regexp term.title) |
1016 | + in |
1017 | + let tags = |
1018 | + List.map |
1019 | + ~f:(fun expr -> |
1020 | + Option.is_some |
1021 | + (List.find |
1022 | + ~f:(fun tag -> string_match expr tag 0) |
1023 | + note.frontmatter.tags)) |
1024 | + (List.map ~f:regexp term.tags) |
1025 | + in |
1026 | + let results = List.concat [ titles; tags ] in |
1027 | + (* if there are no conditions consider it matched *) |
1028 | + if List.length results = 0 then true |
1029 | else |
1030 | - let frontmatter = Ezjsonm.dict [] in |
1031 | - let markdown = Omd.of_string content in |
1032 | - { frontmatter; content; markdown; slug; parent = None } |
1033 | + match operator with |
1034 | + | And -> |
1035 | + List.length (List.filter ~f:(fun v -> v) results) = List.length results |
1036 | + | Or -> List.length (List.filter ~f:(fun v -> v) results) > 0 |
1037 | + |
1038 | + let rec find_many ?(operator = Operator.Or) ~(term : Term.t) ~notes tree = |
1039 | + let (Tree (note, others)) = tree in |
1040 | + let notes = |
1041 | + if match_term ~operator ~term note then note :: notes else notes |
1042 | + in |
1043 | + List.fold ~init:notes |
1044 | + ~f:(fun accm note -> find_many ~operator ~term ~notes:accm note) |
1045 | + others |
1046 | + |
1047 | + let find_one ?(operator = Operator.Or) ~(term : Term.t) tree = |
1048 | + tree |> find_many ~operator ~term ~notes:[] |> List.hd |
1049 | + |
1050 | + let find_one_exn ?(operator = Operator.Or) ~(term : Term.t) tree = |
1051 | + tree |> find_many ~operator ~term ~notes:[] |> List.hd_exn |
1052 | + |
1053 | + let rec length tree = |
1054 | + let (Tree (_, others)) = tree in |
1055 | + List.fold ~init:(List.length others) |
1056 | + ~f:(fun accm tree -> accm + length tree) |
1057 | + others |
1058 | + |
1059 | + let rec insert ?(operator = Operator.Or) ?(term = None) ~tree other = |
1060 | + let (Tree (note, others)) = tree in |
1061 | + match term with |
1062 | + | Some term -> |
1063 | + if match_term ~operator ~term note then |
1064 | + (Tree (note, other :: others), true) |
1065 | + else |
1066 | + let others = |
1067 | + List.map |
1068 | + ~f:(fun tree -> insert ~operator ~term:(Some term) ~tree other) |
1069 | + others |
1070 | + in |
1071 | + let result = |
1072 | + List.fold ~init:([], false) |
1073 | + ~f:(fun accm result -> |
1074 | + let others, updated = accm in |
1075 | + if updated then (fst result :: others, true) |
1076 | + else (fst result :: others, snd result)) |
1077 | + others |
1078 | + in |
1079 | + let others, updated = result in |
1080 | + (Tree (note, others), updated) |
1081 | + | None -> (Tree (note, other :: others), true) |
1082 | + |
1083 | + let buf_insert ~root notes = |
1084 | + let tree = |
1085 | + List.fold ~init:(root, []) |
1086 | + ~f:(fun accm note -> |
1087 | + let tree, buf = accm in |
1088 | + let term = note.frontmatter.parent in |
1089 | + let tree, inserted = insert ~term ~tree (Tree (note, [])) in |
1090 | + let buf = if inserted then buf else note :: buf in |
1091 | + (tree, buf)) |
1092 | + notes |
1093 | + in |
1094 | + tree |
1095 | + |
1096 | + let rec resolve ~root notes = |
1097 | + let tree, buf = buf_insert ~root notes in |
1098 | + match buf |> List.length with 0 -> tree | _ -> resolve ~root:tree buf |
1099 | + |
1100 | + let load path = |
1101 | + let notes = |
1102 | + path |> Slug.load |
1103 | + |> List.map ~f:(fun slug -> |
1104 | + slug.path |> In_channel.read_all |> of_string ~slug:(Some slug)) |
1105 | + in |
1106 | + (* check if a "root" note is defined *) |
1107 | + match |
1108 | + List.find |
1109 | + ~f:(fun note -> |
1110 | + note |
1111 | + |> match_term |
1112 | + ~term:{ title = [ "__root" ]; description = []; tags = [] }) |
1113 | + notes |
1114 | + with |
1115 | + | Some root -> notes |> resolve ~root:(Tree (root, [])) |
1116 | + | None -> notes |> resolve ~root:(Tree (of_string root_template, [])) |
1117 | |
1118 | module Util = struct |
1119 | + open ANSITerminal |
1120 | + |
1121 | let split_words str = |
1122 | List.filter_map |
1123 | ~f:(fun x -> |
1124 | @@ -155,146 +291,76 @@ module Util = struct |
1125 | let accm = accm @ split_words inline.il_desc in |
1126 | to_words accm tl |
1127 | | _ -> to_words accm tl) |
1128 | + |
1129 | + let paint_tag (styles : Config.StylePair.t list) text : string = |
1130 | + match |
1131 | + List.find ~f:(fun entry -> String.equal entry.pattern text) styles |
1132 | + with |
1133 | + | Some entry -> sprintf entry.styles "%s" text |
1134 | + | None -> sprintf [ Foreground Default ] "%s" text |
1135 | + |
1136 | + let to_cells ~columns ~styles (notes : note list) = |
1137 | + let header = |
1138 | + List.map |
1139 | + ~f:(fun column -> |
1140 | + let text_value = Config.Column.to_string column in |
1141 | + let text_length = String.length text_value in |
1142 | + let text_value = sprintf [ Bold; Underlined ] "%s" text_value in |
1143 | + (text_value, text_length, 1)) |
1144 | + columns |
1145 | + in |
1146 | + let note_cells = |
1147 | + let default_padding = 1 in |
1148 | + List.fold ~init:[] |
1149 | + ~f:(fun accm note -> |
1150 | + accm |
1151 | + @ [ |
1152 | + List.map |
1153 | + ~f:(fun column -> |
1154 | + match column with |
1155 | + | `Title -> |
1156 | + let text_value = note.frontmatter.title in |
1157 | + (text_value, String.length text_value, default_padding) |
1158 | + | `Description -> |
1159 | + let text_value = note.frontmatter.description in |
1160 | + (text_value, String.length text_value, default_padding) |
1161 | + | `Slug -> |
1162 | + let text_value = |
1163 | + match note.slug with |
1164 | + | Some slug -> slug |> Slug.shortname |
1165 | + | None -> "??" |
1166 | + in |
1167 | + (text_value, String.length text_value, default_padding) |
1168 | + | `Tags -> |
1169 | + let text_value = |
1170 | + String.concat ~sep:"|" note.frontmatter.tags |
1171 | + in |
1172 | + let text_length = String.length text_value in |
1173 | + let tags = note.frontmatter.tags in |
1174 | + let tags = |
1175 | + List.map ~f:(fun tag -> paint_tag styles tag) tags |
1176 | + in |
1177 | + let text_value = String.concat ~sep:"|" tags in |
1178 | + (text_value, text_length, default_padding) |
1179 | + | `WordCount -> |
1180 | + let text_value = |
1181 | + Core.sprintf "%d" |
1182 | + (List.length |
1183 | + (to_words [] (note.content |> Omd.of_string))) |
1184 | + in |
1185 | + (text_value, String.length text_value, default_padding)) |
1186 | + columns; |
1187 | + ]) |
1188 | + notes |
1189 | + in |
1190 | + [ header ] @ note_cells |
1191 | end |
1192 | |
1193 | module Encoding = struct |
1194 | let to_string ~style t = |
1195 | match style with |
1196 | - | `Raw -> In_channel.read_all (get_path t) |
1197 | + | `Raw -> t.content |
1198 | | `Json -> Ezjsonm.to_string (to_json t) |
1199 | | `Yaml -> Yaml.to_string_exn (to_json t) |
1200 | - | `Html -> Omd.to_html t.markdown |
1201 | + | `Html -> t.content |> Omd.of_string |> Omd.to_html |
1202 | end |
1203 | - |
1204 | - let find_many ~term notes = |
1205 | - let open Re.Str in |
1206 | - let n_titles, n_tags = (List.length term.titles, List.length term.tags) in |
1207 | - if n_titles + n_tags = 0 then notes |
1208 | - else |
1209 | - List.filter |
1210 | - ~f:(fun note -> |
1211 | - let has_title = |
1212 | - let result = |
1213 | - List.count |
1214 | - ~f:(fun expr -> string_match expr (get_title note) 0) |
1215 | - term.titles |
1216 | - in |
1217 | - match term.operator with Or -> result > 0 | And -> result = n_titles |
1218 | - and has_tags = |
1219 | - let result = |
1220 | - List.count |
1221 | - ~f:(fun expr -> |
1222 | - Option.is_some |
1223 | - (List.find |
1224 | - ~f:(fun tag -> string_match expr tag 0) |
1225 | - (get_tags note))) |
1226 | - term.tags |
1227 | - in |
1228 | - match term.operator with Or -> result > 0 | And -> result = n_tags |
1229 | - in |
1230 | - match term.operator with |
1231 | - | Or -> has_title || has_tags |
1232 | - | And -> has_title && has_tags) |
1233 | - notes |
1234 | - |
1235 | - let find_one ~term notes = |
1236 | - let results = find_many ~term notes in |
1237 | - match List.length results with 0 -> None | _ -> Some (List.hd_exn results) |
1238 | - |
1239 | - let find_one_exn ~term notes = |
1240 | - let result = find_one ~term notes in |
1241 | - match result with |
1242 | - | Some result -> result |
1243 | - | None -> failwith "not found" |
1244 | - |
1245 | - (* TODO terrible performance but who cares? *) |
1246 | - let resolve tree notes = |
1247 | - List.fold ~init:tree |
1248 | - ~f:(fun accm note -> |
1249 | - let slug_id = Slug.to_string note.slug in |
1250 | - match note.parent with |
1251 | - | Some term -> ( |
1252 | - match find_one ~term notes with |
1253 | - | Some parent -> ( |
1254 | - let parent_slug_id = Slug.to_string parent.slug in |
1255 | - let children = |
1256 | - List.Assoc.find ~equal:String.equal accm parent_slug_id |
1257 | - in |
1258 | - match children with |
1259 | - | Some children -> |
1260 | - List.Assoc.add ~equal:String.equal accm parent_slug_id |
1261 | - (List.append children [ slug_id ]) |
1262 | - | None -> |
1263 | - List.Assoc.add ~equal:String.equal accm parent_slug_id |
1264 | - [ slug_id ]) |
1265 | - | None -> failwith "cannot resolve parent") |
1266 | - | None -> |
1267 | - if List.Assoc.mem ~equal:String.equal accm slug_id then accm |
1268 | - else List.Assoc.add ~equal:String.equal accm slug_id []) |
1269 | - notes |
1270 | - |
1271 | - type node = Node of (note option * note list) |
1272 | - |
1273 | - let dump_tree notes = |
1274 | - let tree = resolve [] notes in |
1275 | - let tree = resolve tree notes in |
1276 | - List.iter |
1277 | - ~f:(fun (key, values) -> |
1278 | - print_endline (key ^ sprintf " -> %d" (List.length values))) |
1279 | - tree |
1280 | - |
1281 | - open ANSITerminal |
1282 | - |
1283 | - let paint_tag (styles : Config.StylePair.t list) text : string = |
1284 | - match List.find ~f:(fun entry -> String.equal entry.pattern text) styles with |
1285 | - | Some entry -> sprintf entry.styles "%s" text |
1286 | - | None -> sprintf [ Foreground Default ] "%s" text |
1287 | - |
1288 | - let to_cells ~columns ~styles notes = |
1289 | - let header = |
1290 | - List.map |
1291 | - ~f:(fun column -> |
1292 | - let text_value = Config.Column.to_string column in |
1293 | - let text_length = String.length text_value in |
1294 | - let text_value = sprintf [ Bold; Underlined ] "%s" text_value in |
1295 | - (text_value, text_length, 1)) |
1296 | - columns |
1297 | - in |
1298 | - let note_cells = |
1299 | - let default_padding = 1 in |
1300 | - List.fold ~init:[] |
1301 | - ~f:(fun accm note -> |
1302 | - accm |
1303 | - @ [ |
1304 | - List.map |
1305 | - ~f:(fun column -> |
1306 | - match column with |
1307 | - | `Title -> |
1308 | - let text_value = get_title note in |
1309 | - (text_value, String.length text_value, default_padding) |
1310 | - | `Description -> |
1311 | - let text_value = get_description note in |
1312 | - (text_value, String.length text_value, default_padding) |
1313 | - | `Tags -> |
1314 | - let text_value = String.concat ~sep:"|" (get_tags note) in |
1315 | - let text_length = String.length text_value in |
1316 | - let tags = get_tags note in |
1317 | - let tags = |
1318 | - List.map ~f:(fun tag -> paint_tag styles tag) tags |
1319 | - in |
1320 | - let text_value = String.concat ~sep:"|" tags in |
1321 | - (text_value, text_length, default_padding) |
1322 | - | `WordCount -> |
1323 | - let text_value = |
1324 | - Core.sprintf "%d" |
1325 | - (List.length (Util.to_words [] note.markdown)) |
1326 | - in |
1327 | - (text_value, String.length text_value, default_padding) |
1328 | - | `Slug -> |
1329 | - let text_value = Slug.to_string note.slug in |
1330 | - (text_value, String.length text_value, default_padding)) |
1331 | - columns; |
1332 | - ]) |
1333 | - notes |
1334 | - in |
1335 | - [ header ] @ note_cells |
1336 | diff --git a/lib/slug.ml b/lib/slug.ml |
1337 | index b01a9b0..9a2bd33 100644 |
1338 | --- a/lib/slug.ml |
1339 | +++ b/lib/slug.ml |
1340 | @@ -2,33 +2,26 @@ open Core |
1341 | |
1342 | type t = { path : string; date : Date.t; index : int } |
1343 | |
1344 | - let compare s1 s2 = |
1345 | - String.compare s1.path s2.path |
1346 | - |
1347 | - let sexp_of_t t : Sexp.t = |
1348 | - List [ Atom t.path ] |
1349 | + let shortname t = |
1350 | + let date_str = Date.format t.date "%Y%m%d" in |
1351 | + sprintf "note-%s-%d" date_str t.index |
1352 | |
1353 | - let get_path t = t.path |
1354 | + let compare s1 s2 = String.compare s1.path s2.path |
1355 | |
1356 | - let to_string t = |
1357 | - let date_str = Date.format t.date "%Y%m%d" in |
1358 | - sprintf "%s-%d" date_str t.index |
1359 | + let is_note path = |
1360 | + Filename.basename path |> String.is_substring ~substring:"note-" |
1361 | |
1362 | let of_path path = |
1363 | - (* note-20010103-0.md *) |
1364 | - if is_some (String.substr_index ~pattern:"note-" path) then |
1365 | - let slug = Filename.chop_extension (Filename.basename path) in |
1366 | - let split = String.split ~on:'-' slug in |
1367 | - (* TODO: add proper error handling *) |
1368 | - let date = Date.parse ~fmt:"%Y%m%d" (List.nth_exn split 1) in |
1369 | - let index = int_of_string (List.nth_exn split 2) in |
1370 | - Some { path; date; index } |
1371 | - else None |
1372 | + let slug = Filename.chop_extension (Filename.basename path) in |
1373 | + let split = String.split ~on:'-' slug in |
1374 | + let date = Date.parse ~fmt:"%Y%m%d" (List.nth_exn split 1) in |
1375 | + let index = int_of_string (List.nth_exn split 2) in |
1376 | + { path; date; index } |
1377 | |
1378 | let load state_dir = |
1379 | - List.filter_map |
1380 | - ~f:(fun path -> of_path (Filename.concat state_dir path)) |
1381 | - (Sys.ls_dir state_dir) |
1382 | + state_dir |> Sys.ls_dir |> List.filter ~f:is_note |
1383 | + |> List.map ~f:(Filename.concat state_dir) |
1384 | + |> List.map ~f:of_path |
1385 | |
1386 | let next state_dir = |
1387 | let slugs = load state_dir in |
1388 | diff --git a/test/display_test.ml b/test/display_test.ml |
1389 | new file mode 100644 |
1390 | index 0000000..667a66b |
1391 | --- /dev/null |
1392 | +++ b/test/display_test.ml |
1393 | @@ -0,0 +1,20 @@ |
1394 | + open Note_lib |
1395 | + |
1396 | + let test_tree () = |
1397 | + let open Display.Tree in |
1398 | + let expected = {| |
1399 | + A |
1400 | + ├──B |
1401 | + │ └──C |
1402 | + └──D |
1403 | + |} in |
1404 | + let result = |
1405 | + Tree ("A", [ Tree ("B", [ Tree ("C", []) ]); Tree ("D", []) ]) |
1406 | + |> to_lines |> String.concat "" |
1407 | + in |
1408 | + let result = "\n" ^ result in |
1409 | + Alcotest.(check string) "tree" expected result |
1410 | + |
1411 | + let () = |
1412 | + Alcotest.run "Display" |
1413 | + [ ("tree", [ Alcotest.test_case "display a tree" `Quick test_tree ]) ] |
1414 | diff --git a/test/dune b/test/dune |
1415 | index 5ed362e..ee6d694 100644 |
1416 | --- a/test/dune |
1417 | +++ b/test/dune |
1418 | @@ -1,4 +1,4 @@ |
1419 | (tests |
1420 | - (names e2e search) |
1421 | - (libraries note_lib) |
1422 | + (names display_test note_test slug_test) |
1423 | + (libraries note_lib alcotest) |
1424 | ) |
1425 | diff --git a/test/note-0.md b/test/note-0.md |
1426 | deleted file mode 100644 |
1427 | index 1aae215..0000000 |
1428 | --- a/test/note-0.md |
1429 | +++ /dev/null |
1430 | @@ -1,16 +0,0 @@ |
1431 | - # This is a Note |
1432 | - |
1433 | - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi lobortis euismod eros sit amet venenatis. Nulla vitae blandit lorem. Fusce sit amet ipsum massa. Morbi laoreet neque diam, in auctor lectus tempor ac. Cras ante lectus, sagittis et volutpat at, eleifend sit amet arcu. Aliquam vestibulum libero et lacus faucibus, at porttitor sapien imperdiet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eget purus varius, facilisis mi quis, placerat dui. Nullam quis gravida ante, sit amet commodo mi. Duis id semper nulla, a porttitor sapien. Vestibulum elementum efficitur dictum. Ut rhoncus lorem diam, ac congue enim luctus sed. |
1434 | - |
1435 | - ## Hello |
1436 | - |
1437 | - ```bash |
1438 | - fuu bar baz qux |
1439 | - ``` |
1440 | - |
1441 | - ### World |
1442 | - |
1443 | - * Item One |
1444 | - * Item Two |
1445 | - * Item Three |
1446 | - * Item Four |
1447 | diff --git a/test/note-1.md b/test/note-1.md |
1448 | deleted file mode 100644 |
1449 | index 2814b45..0000000 |
1450 | --- a/test/note-1.md |
1451 | +++ /dev/null |
1452 | @@ -1,35 +0,0 @@ |
1453 | - --- |
1454 | - title: "This Note Has Frontmatter" |
1455 | - tags: [fuu, bar, baz, qux] |
1456 | - and_some_random: [{arbitrary: {data: [3.145, 1.00000000]}}] |
1457 | - --- |
1458 | - # This is a Note |
1459 | - |
1460 | - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi lobortis euismod eros sit amet venenatis. Nulla vitae blandit lorem. Fusce sit amet ipsum massa. Morbi laoreet neque diam, in auctor lectus tempor ac. Cras ante lectus, sagittis et volutpat at, eleifend sit amet arcu. Aliquam vestibulum libero et lacus faucibus, at porttitor sapien imperdiet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eget purus varius, facilisis mi quis, placerat dui. Nullam quis gravida ante, sit amet commodo mi. Duis id semper nulla, a porttitor sapien. Vestibulum elementum efficitur dictum. Ut rhoncus lorem diam, ac congue enim luctus sed. |
1461 | - |
1462 | - ## Hello |
1463 | - |
1464 | - ```bash |
1465 | - fuu bar baz qux |
1466 | - ``` |
1467 | - |
1468 | - ### World |
1469 | - |
1470 | - * Item One |
1471 | - * Item Two |
1472 | - * Item Three |
1473 | - * Item Four |
1474 | - |
1475 | - |
1476 | - ```json |
1477 | - { |
1478 | - "fuu": ["bar", "baz", "qux"] |
1479 | - } |
1480 | - ``` |
1481 | - |
1482 | - ```yaml |
1483 | - fuu: |
1484 | - - bar |
1485 | - - baz |
1486 | - - qux |
1487 | - ``` |
1488 | diff --git a/test/note_test.ml b/test/note_test.ml |
1489 | new file mode 100644 |
1490 | index 0000000..fc612ce |
1491 | --- /dev/null |
1492 | +++ b/test/note_test.ml |
1493 | @@ -0,0 +1,269 @@ |
1494 | + open Core |
1495 | + open Note_lib |
1496 | + |
1497 | + let dump_notes (notes : Note.note list) = |
1498 | + List.iter ~f:(fun note -> print_endline note.frontmatter.title) notes |
1499 | + |
1500 | + let rec convert_tree tree = |
1501 | + let (Note.Tree (note, others)) = tree in |
1502 | + let title = note.frontmatter.title in |
1503 | + let title = "[" ^ title ^ "]" in |
1504 | + Display.Tree.Tree (title, List.map ~f:convert_tree others) |
1505 | + |
1506 | + let make_a_note () = |
1507 | + let note = |
1508 | + Note.of_string |
1509 | + {| |
1510 | + --- |
1511 | + title: this is a note |
1512 | + description: although it doesn't contain anything of value |
1513 | + tags: ["it", "is", "still", "a", "note"] |
1514 | + --- |
1515 | + |
1516 | + # What is the Purpose of Life? |
1517 | + |
1518 | + ```json |
1519 | + { |
1520 | + "answer": "it isn't clear" |
1521 | + } |
1522 | + ``` |
1523 | + |} |
1524 | + in |
1525 | + |
1526 | + let title = note.frontmatter.title in |
1527 | + Alcotest.(check string) "title" "this is a note" title; |
1528 | + let description = note.frontmatter.description in |
1529 | + Alcotest.(check string) |
1530 | + "description" "although it doesn't contain anything of value" description; |
1531 | + let tags = note.frontmatter.tags in |
1532 | + Alcotest.(check (list string)) |
1533 | + "tags" |
1534 | + [ "it"; "is"; "still"; "a"; "note" ] |
1535 | + tags; |
1536 | + let data = |
1537 | + Ezjsonm.find (note |> Note.to_json) [ "data" ] |
1538 | + |> Ezjsonm.get_list (fun x -> x) |
1539 | + |> List.hd_exn |
1540 | + in |
1541 | + let answer = Ezjsonm.find data [ "answer" ] |> Ezjsonm.get_string in |
1542 | + Alcotest.(check string) "answer" "it isn't clear" answer |
1543 | + |
1544 | + let note_of_title title = |
1545 | + let template = |
1546 | + sprintf {| |
1547 | + --- |
1548 | + title: %s |
1549 | + --- |
1550 | + |
1551 | + # Note %s |
1552 | + |
1553 | + Hello World |} title title |
1554 | + in |
1555 | + Note.of_string template |
1556 | + |
1557 | + (* |
1558 | + [root] |
1559 | + ├──[note-0] |
1560 | + ├──[note-1] |
1561 | + │ └──[note-2] |
1562 | + ├──[note-3] |
1563 | + ├──[note-4] |
1564 | + │ ├──[note-5] |
1565 | + │ │ └──[note-6] |
1566 | + │ └──[note-7] |
1567 | + └──[note-8] |
1568 | + *) |
1569 | + |
1570 | + let tree = |
1571 | + Note.Tree |
1572 | + ( note_of_title "root", |
1573 | + [ |
1574 | + Note.Tree (note_of_title "note-0", []); |
1575 | + Note.Tree |
1576 | + (note_of_title "note-1", [ Note.Tree (note_of_title "note-2", []) ]); |
1577 | + Note.Tree (note_of_title "note-3", []); |
1578 | + Note.Tree |
1579 | + ( note_of_title "note-4", |
1580 | + [ |
1581 | + Note.Tree |
1582 | + ( note_of_title "note-5", |
1583 | + [ Note.Tree (note_of_title "note-6", []) ] ); |
1584 | + Note.Tree (note_of_title "note-7", []); |
1585 | + ] ); |
1586 | + Note.Tree (note_of_title "note-8", []); |
1587 | + ] ) |
1588 | + |
1589 | + let find_many () = |
1590 | + let results = |
1591 | + Note.find_many |
1592 | + ~term:{ title = [ "note-3"; "note-4" ]; description = []; tags = [] } |
1593 | + ~notes:[] tree |
1594 | + in |
1595 | + dump_notes results; |
1596 | + let n_results = results |> List.length in |
1597 | + Alcotest.(check int) "two results" 2 n_results; |
1598 | + let n3 = List.nth_exn results 1 in |
1599 | + Alcotest.(check string) "first result" "note-3" n3.frontmatter.title; |
1600 | + let n4 = List.nth_exn results 0 in |
1601 | + Alcotest.(check string) "first result" "note-4" n4.frontmatter.title |
1602 | + |
1603 | + let insert_at () = |
1604 | + let n9 = note_of_title "note-9" in |
1605 | + let tree, inserted = |
1606 | + Note.insert |
1607 | + ~term:(Some { title = [ "note-3" ]; description = []; tags = [] }) |
1608 | + ~tree |
1609 | + (Note.Tree (n9, [])) |
1610 | + in |
1611 | + Alcotest.(check bool) "inserted" true inserted ; |
1612 | + let result = |
1613 | + Note.find_one |
1614 | + ~term:{ title = [ "note-9" ]; description = []; tags = [] } |
1615 | + tree |
1616 | + in |
1617 | + Alcotest.(check bool) "inserted" true (Option.is_some result) |
1618 | + |
1619 | + let test_structure () = |
1620 | + let open Display.Tree in |
1621 | + let expected = |
1622 | + {| |
1623 | + [root] |
1624 | + ├──[note-0] |
1625 | + ├──[note-1] |
1626 | + │ └──[note-2] |
1627 | + ├──[note-3] |
1628 | + ├──[note-4] |
1629 | + │ ├──[note-5] |
1630 | + │ │ └──[note-6] |
1631 | + │ └──[note-7] |
1632 | + └──[note-8] |
1633 | + |} |
1634 | + in |
1635 | + Alcotest.(check int) "length" 9 (Note.length tree); |
1636 | + let note_tree = tree |> convert_tree |> to_string in |
1637 | + Alcotest.(check string) "structure" expected note_tree |
1638 | + |
1639 | + (* |
1640 | + we are attempting to go from a flat list of things that may or may |
1641 | + not reference other items from within the list and construct a hiarchrial |
1642 | + tree. |
1643 | + |
1644 | + Example: |
1645 | + |
1646 | + [ |
1647 | + note-1 |
1648 | + note-2 [ref note-4] |
1649 | + note-3 |
1650 | + note-4 [ref note-1] |
1651 | + note-5 |
1652 | + ] |
1653 | + |
1654 | + . |
1655 | + ├──[note-1] |
1656 | + │ │──[note-4] |
1657 | + │ └──[note-2] |
1658 | + │note-3 |
1659 | + └note-5 |
1660 | + |
1661 | + |
1662 | + def resolve(tree, notes): |
1663 | + buf = [] |
1664 | + for note in notes: |
1665 | + if note.ref == None: |
1666 | + tree.add(note) |
1667 | + else: |
1668 | + inserted = tree.insert(note.ref) |
1669 | + if not inserted: |
1670 | + buf.append(note) |
1671 | + if buf.length > 0: |
1672 | + return resolve(tree, buf) |
1673 | + return tree |
1674 | + |
1675 | + *) |
1676 | + let n0 = Note.of_string {| |
1677 | + --- |
1678 | + title: note-0 |
1679 | + --- |
1680 | + # Note 0 |
1681 | + |} |
1682 | + |
1683 | + let n1 = |
1684 | + Note.of_string |
1685 | + {| |
1686 | + --- |
1687 | + title: note-1 |
1688 | + parent: {"title": ["note-0"]} |
1689 | + --- |
1690 | + # Note 1 |
1691 | + |} |
1692 | + |
1693 | + let n2 = Note.of_string {| |
1694 | + --- |
1695 | + title: note-2 |
1696 | + --- |
1697 | + # Note 2 |
1698 | + |} |
1699 | + |
1700 | + let n3 = |
1701 | + Note.of_string |
1702 | + {| |
1703 | + --- |
1704 | + title: note-3 |
1705 | + parent: {"title": ["note-1"]} |
1706 | + --- |
1707 | + # Note 3 |
1708 | + |} |
1709 | + |
1710 | + let n4 = |
1711 | + Note.of_string {| |
1712 | + --- |
1713 | + title: note-4 |
1714 | + parent: {"title": ["note-1"]} |
1715 | + --- |
1716 | + # Note 4 |
1717 | + |} |
1718 | + |
1719 | + let test_buf_insert () = |
1720 | + let root = Note.Tree (Note.of_string Note.root_template, []) in |
1721 | + let tree, buf = Note.buf_insert ~root [ n3; n2; n1; n0 ] in |
1722 | + Alcotest.(check int) "n" 2 (List.length buf); |
1723 | + let _, buf = Note.buf_insert ~root:tree buf in |
1724 | + Alcotest.(check int) "n" 0 (List.length buf) |
1725 | + |
1726 | + let test_resolve () = |
1727 | + let root = Note.Tree (Note.of_string Note.root_template, []) in |
1728 | + let expected = |
1729 | + {| |
1730 | + [root] |
1731 | + ├──[note-2] |
1732 | + └──[note-0] |
1733 | + └──[note-1] |
1734 | + ├──[note-4] |
1735 | + └──[note-3] |
1736 | + |} |
1737 | + in |
1738 | + let tree_as_string = |
1739 | + [ n3; n2; n1; n0 ; n4] |> Note.resolve ~root |> convert_tree |
1740 | + |> Display.Tree.to_string |
1741 | + in |
1742 | + Alcotest.(check string) "resolve" expected tree_as_string |
1743 | + |
1744 | + let () = |
1745 | + Alcotest.run "Note" |
1746 | + [ |
1747 | + ( "create", |
1748 | + [ Alcotest.test_case "create a note from a string" `Quick make_a_note ] |
1749 | + ); |
1750 | + ( "find-many", |
1751 | + [ |
1752 | + Alcotest.test_case "find notes with multiple criteria" `Quick |
1753 | + find_many; |
1754 | + ] ); |
1755 | + ( "insert", |
1756 | + [ Alcotest.test_case "insert a note into a tree" `Quick insert_at ] ); |
1757 | + ( "structure", |
1758 | + [ Alcotest.test_case "note tree structure" `Quick test_structure ] ); |
1759 | + ( "buf_insert", |
1760 | + [ Alcotest.test_case "buf insert flat list" `Quick test_buf_insert ] ); |
1761 | + ("resolve", [ Alcotest.test_case "resolve flat list" `Quick test_resolve ]); |
1762 | + ] |
1763 | diff --git a/test/search.ml b/test/search.ml |
1764 | deleted file mode 100644 |
1765 | index c98dfb4..0000000 |
1766 | --- a/test/search.ml |
1767 | +++ /dev/null |
1768 | @@ -1,69 +0,0 @@ |
1769 | - open Note_lib |
1770 | - |
1771 | - let make_notes = |
1772 | - let state_dir = Core.Filename.temp_dir "note-test" "" in |
1773 | - let note_1 = |
1774 | - Note.build ~tags:[ "fuu"; "bar" ] ~content:"" ~title:"A Very Important Note" |
1775 | - (Slug.next state_dir) |
1776 | - in |
1777 | - let note_2 = |
1778 | - Note.build ~tags:[ "fuu"; "bar"; "baz" ] ~content:"" |
1779 | - ~title:"Another Very Important Note" (Slug.next state_dir) |
1780 | - in |
1781 | - let note_3 = |
1782 | - Note.build ~tags:[ "fuu"; "bar"; "baz" ] ~content:"" ~title:"fuu" |
1783 | - (Slug.next state_dir) |
1784 | - in |
1785 | - [ note_1; note_2; note_3 ] |
1786 | - |
1787 | - let test_filter_by_keys = |
1788 | - let notes = make_notes in |
1789 | - let result = |
1790 | - Note.find_many |
1791 | - ~term: |
1792 | - { |
1793 | - titles = [ Re.Str.regexp "A Very Important Note" ]; |
1794 | - tags = []; |
1795 | - operator = Note.Or; |
1796 | - } |
1797 | - notes |
1798 | - in |
1799 | - assert (List.length result = 1); |
1800 | - let result = |
1801 | - Note.find_many |
1802 | - ~term: |
1803 | - { |
1804 | - titles = [ Re.Str.regexp "A Very Important Note" ]; |
1805 | - tags = []; |
1806 | - operator = Note.Or; |
1807 | - } |
1808 | - notes |
1809 | - in |
1810 | - assert (List.length result = 1); |
1811 | - let result = |
1812 | - Note.find_many |
1813 | - ~term: |
1814 | - { |
1815 | - titles = []; |
1816 | - tags = |
1817 | - [ Re.Str.regexp "fuu"; Re.Str.regexp "bar"; Re.Str.regexp "baz" ]; |
1818 | - operator = Note.And; |
1819 | - } |
1820 | - notes |
1821 | - in |
1822 | - assert (List.length result = 2) |
1823 | - |
1824 | - let test_filter_by_title_find_one = |
1825 | - let notes = make_notes in |
1826 | - let result = |
1827 | - Note.find_one |
1828 | - ~term:{ titles = [Re.Str.regexp "^A.*"]; tags = []; operator = Note.Or } |
1829 | - notes |
1830 | - in |
1831 | - assert (Option.is_some result); |
1832 | - let note = Option.get result in |
1833 | - assert (Note.get_title note = "A Very Important Note") |
1834 | - |
1835 | - let () = |
1836 | - test_filter_by_keys; |
1837 | - test_filter_by_title_find_one |
1838 | diff --git a/test/slug_test.ml b/test/slug_test.ml |
1839 | new file mode 100644 |
1840 | index 0000000..c02fa90 |
1841 | --- /dev/null |
1842 | +++ b/test/slug_test.ml |
1843 | @@ -0,0 +1,46 @@ |
1844 | + open Core |
1845 | + open Note_lib |
1846 | + |
1847 | + let test_slug_shortname () = |
1848 | + let s1 = Slug.of_path "fuu/note-19700101-0.md" in |
1849 | + Alcotest.(check string) "short name" "note-19700101-0" (Slug.shortname s1) |
1850 | + |
1851 | + let test_slug_comparable () = |
1852 | + let s1, s2 = |
1853 | + ( Slug.of_path "fuu/note-19700101-0.md", |
1854 | + Slug.of_path "fuu/note-19700101-0.md" ) |
1855 | + in |
1856 | + Alcotest.(check bool) "identical paths" true (Slug.compare s1 s2 = 0) |
1857 | + |
1858 | + let test_slug_path () = |
1859 | + let date_string = Time.format (Time.now ()) "%Y%m%d" ~zone:Time.Zone.utc in |
1860 | + let state_dir = Filename.temp_dir "note-test" "" in |
1861 | + let slug = Slug.next state_dir in |
1862 | + let expected = |
1863 | + Filename.concat state_dir (sprintf "note-%s-0.md" date_string) |
1864 | + in |
1865 | + Alcotest.(check string) "path" expected slug.path |
1866 | + |
1867 | + let test_slug_increment () = |
1868 | + let state_dir = Filename.temp_dir "note-test" "" in |
1869 | + let date_string = Time.format (Time.now ()) "%Y%m%d" ~zone:Time.Zone.utc in |
1870 | + for i = 0 to 5 do |
1871 | + let filename = |
1872 | + Filename.concat state_dir (sprintf "note-%s-%d.md" date_string i) |
1873 | + in |
1874 | + Out_channel.write_all filename ~data:"" |
1875 | + done; |
1876 | + let slug = Slug.next state_dir in |
1877 | + Alcotest.(check int) "index" 6 slug.index |
1878 | + |
1879 | + let () = |
1880 | + Alcotest.run "Slug" |
1881 | + [ |
1882 | + ( "slug-comparable", |
1883 | + [ Alcotest.test_case "compare" `Quick test_slug_comparable ] ); |
1884 | + ( "slug-shortname", |
1885 | + [ Alcotest.test_case "shortname" `Quick test_slug_shortname ] ); |
1886 | + ("path", [ Alcotest.test_case "path" `Quick test_slug_path ]); |
1887 | + ( "slug-increment", |
1888 | + [ Alcotest.test_case "increment" `Quick test_slug_increment ] ); |
1889 | + ] |