Commit
+296 -112 +/-5 browse
1 | diff --git a/bin/note.ml b/bin/note.ml |
2 | index 0ee0123..7de5c2a 100644 |
3 | --- a/bin/note.ml |
4 | +++ b/bin/note.ml |
5 | @@ -2,6 +2,7 @@ open Core |
6 | open Note_lib |
7 | |
8 | let cfg = Config.config_path |> Config.load |
9 | + |
10 | let context = cfg.context |
11 | |
12 | module Util = struct |
13 | @@ -107,7 +108,7 @@ let rec convert_tree tree = |
14 | let (Note.Tree (note, others)) = tree in |
15 | let title = note.frontmatter.title in |
16 | let title = "[" ^ title ^ "]" in |
17 | - Display.Tree (title, List.map ~f:convert_tree others) |
18 | + Display.Hierarchical.Tree (title, List.map ~f:convert_tree others) |
19 | |
20 | let get_notes = |
21 | let notes = cfg.state_dir |> Note.load ~context |> Note.flatten ~accm:[] in |
22 | @@ -307,7 +308,7 @@ List one or more notes that match the filter criteria, if no filter criteria |
23 | is provided then all notes will be listed. |
24 | |}) |
25 | [%map_open |
26 | - let term = term_args |
27 | + let _ = term_args |
28 | and style = |
29 | flag "style" |
30 | (optional_with_default cfg.list_style list_style_arg) |
31 | @@ -318,10 +319,10 @@ is provided then all notes will be listed. |
32 | ~doc:"columns to include in output" |
33 | in |
34 | fun () -> |
35 | - let notes = Note.find_many ~term ~notes:[] (Note.load ~context cfg.state_dir) in |
36 | + let notes = cfg.state_dir |> Note.load ~context |> Note.to_list in |
37 | let styles = cfg.styles in |
38 | - let cells = Util.to_cells ~columns ~styles notes in |
39 | - Display.to_stdout ~style cells] |
40 | + let cells = notes |> Display.to_cells ~paint:true ~columns ~styles in |
41 | + cells |> Display.Tabular.to_string ~style |> print_endline] |
42 | |
43 | let sync = |
44 | Command.basic ~summary:"sync notes to a remote server" |
45 | @@ -330,8 +331,8 @@ let sync = |
46 | let tree = |
47 | Command.basic ~summary:"tree debug command" |
48 | (Command.Param.return (fun () -> |
49 | - cfg.state_dir |> Note.load ~context |> convert_tree |> Display.to_string |
50 | - |> print_endline)) |
51 | + cfg.state_dir |> Note.load ~context |> convert_tree |
52 | + |> Display.Hierarchical.to_string |> print_endline)) |
53 | |
54 | let version = |
55 | match Build_info.V1.version () with |
56 | diff --git a/lib/display.ml b/lib/display.ml |
57 | index 948afb4..7f518f7 100644 |
58 | --- a/lib/display.ml |
59 | +++ b/lib/display.ml |
60 | @@ -1,107 +1,199 @@ |
61 | open Core |
62 | open ANSITerminal |
63 | |
64 | - type tree = Tree of (string * tree list) |
65 | - |
66 | type cell = string * int * int |
67 | |
68 | - type row = cell list |
69 | + and row = cell list |
70 | + |
71 | + let rec to_words (accm : string list) (doc : Omd.doc) : string list = |
72 | + let split_words inline = |
73 | + match inline with Omd.Text text -> String.split ~on:' ' text | _ -> [] |
74 | + in |
75 | + match doc with |
76 | + | [] -> accm |
77 | + | hd :: tl -> ( |
78 | + (* TODO: account for headings, lists, etc *) |
79 | + match hd.bl_desc with |
80 | + | Paragraph inline -> |
81 | + let accm = accm @ split_words inline.il_desc in |
82 | + to_words accm tl |
83 | + | _ -> to_words accm tl) |
84 | |
85 | - let fill ~last ~state = |
86 | - let get_padding = function true -> "│ " | false -> " " in |
87 | - let get_edge = function true -> "└──" | false -> "├──" in |
88 | - match List.length state with |
89 | - | 0 -> [] |
90 | - | 1 -> [ last |> get_edge ] |
91 | - | len -> |
92 | - let state = List.slice state 0 (len - 1) in |
93 | - let padding = List.map ~f:get_padding state in |
94 | - List.append padding [ last |> get_edge ] |
95 | + let paint_tag (styles : Config.StylePair.t list) text : string = |
96 | + match List.find ~f:(fun entry -> String.equal entry.pattern text) styles with |
97 | + | Some entry -> sprintf entry.styles "%s" text |
98 | + | None -> sprintf [ Foreground Default ] "%s" text |
99 | + |
100 | + let to_cells ?(paint = false) ~columns ~styles (notes : Note.note list) = |
101 | + let header = |
102 | + List.map |
103 | + ~f:(fun column -> |
104 | + let text_value = Config.Column.to_string column in |
105 | + let text_length = String.length text_value in |
106 | + let text_value = |
107 | + if paint then sprintf [ Bold; Underlined ] "%s" text_value |
108 | + else text_value |
109 | + in |
110 | + (text_value, text_length, 1)) |
111 | + columns |
112 | + in |
113 | + let note_cells = |
114 | + let default_padding = 1 in |
115 | + List.fold ~init:[] |
116 | + ~f:(fun accm note -> |
117 | + accm |
118 | + @ [ |
119 | + List.map |
120 | + ~f:(fun column -> |
121 | + match column with |
122 | + | `Title -> |
123 | + let text_value = note.frontmatter.title in |
124 | + (text_value, String.length text_value, default_padding) |
125 | + | `Description -> |
126 | + let text_value = note.frontmatter.description in |
127 | + (text_value, String.length text_value, default_padding) |
128 | + | `Slug -> |
129 | + let text_value = |
130 | + match note.slug with |
131 | + | Some slug -> slug |> Slug.shortname |
132 | + | None -> "??" |
133 | + in |
134 | + (text_value, String.length text_value, default_padding) |
135 | + | `Tags -> |
136 | + let text_value = |
137 | + String.concat ~sep:"|" note.frontmatter.tags |
138 | + in |
139 | + let text_length = String.length text_value in |
140 | + let tags = note.frontmatter.tags in |
141 | + let tags = |
142 | + if paint then |
143 | + List.map ~f:(fun tag -> paint_tag styles tag) tags |
144 | + else tags |
145 | + in |
146 | + let text_value = String.concat ~sep:"|" tags in |
147 | + (text_value, text_length, default_padding) |
148 | + | `WordCount -> |
149 | + let text_value = |
150 | + Core.sprintf "%d" |
151 | + (List.length |
152 | + (to_words [] (note.content |> Omd.of_string))) |
153 | + in |
154 | + (text_value, String.length text_value, default_padding)) |
155 | + columns; |
156 | + ]) |
157 | + notes |
158 | + in |
159 | + [ header ] @ note_cells |
160 | |
161 | - let rec to_lines ?(state = []) ?(last = false) next = |
162 | - let (Tree next) = next in |
163 | - let title, children = next in |
164 | - match List.length children with |
165 | - | 0 -> |
166 | - (* leaf *) |
167 | - List.append (fill ~last ~state) [ title; "\n" ] |
168 | - | n_children -> |
169 | - (* node *) |
170 | - List.foldi |
171 | - ~init: |
172 | - [ List.append (fill ~last ~state) [ title; "\n" ] |> String.concat ] |
173 | - ~f:(fun i accm node -> |
174 | - let is_last = Int.equal i (n_children - 1) in |
175 | - let state = List.append state [ phys_equal is_last false ] in |
176 | - let lines = to_lines ~state ~last:is_last node in |
177 | - List.append accm lines) |
178 | - children |
179 | + module Tabular = struct |
180 | + let fixed cells = |
181 | + (* find the maximum cell length per column *) |
182 | + let maximum_values = |
183 | + List.fold ~init:[] |
184 | + ~f:(fun accm row -> |
185 | + List.mapi |
186 | + ~f:(fun i col -> |
187 | + let col_length = snd3 col in |
188 | + let current_max = |
189 | + match List.nth accm i with Some len -> len | None -> 0 |
190 | + in |
191 | + if col_length > current_max then col_length + 2 else current_max) |
192 | + row) |
193 | + cells |
194 | + in |
195 | + maximum_values |
196 | |
197 | - let to_string t = |
198 | - let result = t |> to_lines |> String.concat in |
199 | - "\n" ^ result |
200 | + let fixed_right cells = |
201 | + let widths = cells |> fixed in |
202 | + let term_width, _ = size () in |
203 | + let _, right = List.split_n widths 1 in |
204 | + let col_one = List.nth_exn widths 0 in |
205 | + [ col_one + (term_width - List.fold ~init:5 ~f:( + ) widths) ] @ right |
206 | |
207 | - let fixed_spacing cells = |
208 | - (* find the maximum cell length per column *) |
209 | - let maximum_values = |
210 | + let apply ~widths cells = |
211 | + (* let maximums = fixed_spacing cells in *) |
212 | + let cells = |
213 | + List.map |
214 | + ~f:(fun row -> |
215 | + List.mapi |
216 | + ~f:(fun i entry -> |
217 | + let max = List.nth_exn widths i in |
218 | + let text, length, padding = entry in |
219 | + let padding = padding + (max - length) in |
220 | + let padding = if padding > 0 then padding else 0 in |
221 | + (text, length, padding)) |
222 | + row) |
223 | + cells |
224 | + in |
225 | List.fold ~init:[] |
226 | ~f:(fun accm row -> |
227 | - List.mapi |
228 | - ~f:(fun i col -> |
229 | - let col_length = snd3 col in |
230 | - let current_max = |
231 | - match List.nth accm i with Some len -> len | None -> 0 |
232 | - in |
233 | - if col_length > current_max then col_length + 2 else current_max) |
234 | - row) |
235 | + accm |
236 | + @ [ |
237 | + List.fold ~init:"" |
238 | + ~f:(fun accm cell -> |
239 | + let text, _, padding = cell in |
240 | + String.concat [ accm; text; String.make padding ' ' ]) |
241 | + row; |
242 | + ]) |
243 | cells |
244 | - in |
245 | - maximum_values |
246 | |
247 | - let fix_right cells = |
248 | - let widths = fixed_spacing cells in |
249 | - let term_width, _ = size () in |
250 | - let _, right = List.split_n widths 1 in |
251 | - let col_one = List.nth_exn widths 0 in |
252 | - [ col_one + (term_width - List.fold ~init:5 ~f:( + ) widths) ] @ right |
253 | + let to_string ~style (cells : row list) = |
254 | + match style with |
255 | + | `Simple -> |
256 | + let lines = |
257 | + List.slice |
258 | + (cells |> List.map ~f:(fun row -> row |> List.hd_exn |> fst3)) |
259 | + 1 0 |
260 | + |> String.concat ~sep:"\n" |
261 | + in |
262 | + "\n" ^ lines ^ "\n" |
263 | + | `Fixed -> |
264 | + let lines = |
265 | + apply ~widths:(cells |> fixed) cells |> String.concat ~sep:"\n" |
266 | + in |
267 | + "\n" ^ lines ^ "\n" |
268 | + | `Wide -> |
269 | + let lines = |
270 | + apply ~widths:(cells |> fixed_right) cells |> String.concat ~sep:"\n" |
271 | + in |
272 | + "\n" ^ lines ^ "\n" |
273 | + | `Tree -> failwith "not implemented" |
274 | + end |
275 | |
276 | - let apply cells widths = |
277 | - (* let maximums = fixed_spacing cells in *) |
278 | - let cells = |
279 | - List.map |
280 | - ~f:(fun row -> |
281 | - List.mapi |
282 | - ~f:(fun i entry -> |
283 | - let max = List.nth_exn widths i in |
284 | - let text, length, padding = entry in |
285 | - let padding = padding + (max - length) in |
286 | - let padding = if padding > 0 then padding else 0 in |
287 | - (text, length, padding)) |
288 | - row) |
289 | - cells |
290 | - in |
291 | - List.fold ~init:[] |
292 | - ~f:(fun accm row -> |
293 | - accm |
294 | - @ [ |
295 | - List.fold ~init:"" |
296 | - ~f:(fun accm cell -> |
297 | - let text, _, padding = cell in |
298 | - String.concat [ accm; text; String.make padding ' ' ]) |
299 | - row; |
300 | - ]) |
301 | - cells |
302 | + module Hierarchical = struct |
303 | + type tree = Tree of (string * tree list) |
304 | |
305 | - let to_stdout ~style cells = |
306 | - match style with |
307 | - | `Simple -> |
308 | - List.iter |
309 | - ~f:(fun cell -> |
310 | - print_endline |
311 | - (let value = List.nth_exn cell 0 in |
312 | - let text = fst3 value in |
313 | - text)) |
314 | - cells |
315 | - | `Fixed -> List.iter ~f:print_endline (apply cells (fixed_spacing cells)) |
316 | - | `Wide -> List.iter ~f:print_endline (apply cells (fix_right cells)) |
317 | - | `Tree -> failwith "unimplemented" |
318 | + let get_padding = function true -> "│ " | false -> " " |
319 | + |
320 | + let get_edge = function true -> "└──" | false -> "├──" |
321 | + |
322 | + let fill ~last ~state = |
323 | + match List.length state with |
324 | + | 0 -> [] |
325 | + | 1 -> [ last |> get_edge ] |
326 | + | len -> |
327 | + let state = List.slice state 0 (len - 1) in |
328 | + let padding = List.map ~f:get_padding state in |
329 | + List.append padding [ last |> get_edge ] |
330 | + |
331 | + let rec to_lines ?(state = []) ?(last = false) next = |
332 | + let (Tree next) = next in |
333 | + let title, children = next in |
334 | + match List.length children with |
335 | + | 0 -> |
336 | + (* leaf *) |
337 | + List.append (fill ~last ~state) [ title; "\n" ] |
338 | + | n_children -> |
339 | + (* node *) |
340 | + List.foldi |
341 | + ~init: |
342 | + [ List.append (fill ~last ~state) [ title; "\n" ] |> String.concat ] |
343 | + ~f:(fun i accm node -> |
344 | + let is_last = Int.equal i (n_children - 1) in |
345 | + let state = List.append state [ phys_equal is_last false ] in |
346 | + let lines = to_lines ~state ~last:is_last node in |
347 | + List.append accm lines) |
348 | + children |
349 | + |
350 | + let to_string tree = "\n" ^ (tree |> to_lines |> String.concat) |
351 | + end |
352 | diff --git a/lib/note.ml b/lib/note.ml |
353 | index f791e53..cace7dd 100644 |
354 | --- a/lib/note.ml |
355 | +++ b/lib/note.ml |
356 | @@ -166,6 +166,14 @@ let rec flatten ~accm tree = |
357 | let (Tree (note, others)) = tree in |
358 | List.fold ~init:(note :: accm) ~f:(fun accm note -> flatten ~accm note) others |
359 | |
360 | + let to_list tree = |
361 | + let (Tree (_, others)) = tree in |
362 | + List.fold ~init:[] |
363 | + ~f:(fun accm tree -> |
364 | + let (Tree (note, _)) = tree in |
365 | + note :: accm) |
366 | + others |
367 | + |
368 | let match_term ?(operator = Operator.Or) ~(term : Term.t) note = |
369 | let open Re.Str in |
370 | let titles = |
371 | @@ -268,12 +276,7 @@ let rec resolve ~root notes = |
372 | let tree, buf = buf_insert ~root notes in |
373 | match buf |> List.length with 0 -> tree | _ -> resolve ~root:tree buf |
374 | |
375 | - let load ~context path = |
376 | - let notes = |
377 | - path |> Slug.load |
378 | - |> List.map ~f:(fun slug -> |
379 | - slug.path |> In_channel.read_all |> of_string ~slug:(Some slug)) |
380 | - in |
381 | + let of_list ~context notes = |
382 | (* check if a "root" note is defined *) |
383 | let tree = |
384 | match |
385 | @@ -291,3 +294,11 @@ let load ~context path = |
386 | else |
387 | let root = find_many_tree ~term:context ~trees:[] tree |> List.hd_exn in |
388 | root |
389 | + |
390 | + let load ~context path = |
391 | + let notes = |
392 | + path |> Slug.load |
393 | + |> List.map ~f:(fun slug -> |
394 | + slug.path |> In_channel.read_all |> of_string ~slug:(Some slug)) |
395 | + in |
396 | + of_list ~context notes |
397 | diff --git a/test/display_test.ml b/test/display_test.ml |
398 | index 7e980e6..a1165c8 100644 |
399 | --- a/test/display_test.ml |
400 | +++ b/test/display_test.ml |
401 | @@ -1,8 +1,77 @@ |
402 | + open Core |
403 | open Note_lib |
404 | |
405 | - let test_tree () = |
406 | + let notes = |
407 | + [ |
408 | + {| |
409 | + --- |
410 | + title: fuu |
411 | + description: "fuu note" |
412 | + tags: [a,b,c] |
413 | + --- |
414 | + |}; |
415 | + {| |
416 | + --- |
417 | + title: bar |
418 | + description: "bar note with a very long description" |
419 | + tags: [d,e,f] |
420 | + --- |
421 | + |}; |
422 | + {| |
423 | + --- |
424 | + title: baz |
425 | + description: "baz note" |
426 | + tags: [h,i,j] |
427 | + --- |
428 | + |}; |
429 | + {| |
430 | + --- |
431 | + title: qux |
432 | + description: "qux note" |
433 | + tags: [k,l,m] |
434 | + --- |
435 | + |}; |
436 | + ] |
437 | + |> List.map ~f:Note.of_string |
438 | + |
439 | + let test_tabular_display_simple () = |
440 | let open Display in |
441 | let expected = {| |
442 | + fuu |
443 | + bar |
444 | + baz |
445 | + qux |
446 | + |} in |
447 | + let result = |
448 | + notes |
449 | + |> to_cells ~columns:[ `Title ] ~styles:[] |
450 | + |> Tabular.to_string ~style:`Simple |
451 | + in |
452 | + Alcotest.(check string) "tabular_simple" expected result |
453 | + |
454 | + |
455 | + let test_tabular_display_fixed () = |
456 | + let open Display in |
457 | + let expected = {| |
458 | + title description tags |
459 | + fuu fuu note a|b|c |
460 | + bar bar note with a very long description d|e|f |
461 | + baz baz note h|i|j |
462 | + qux qux note k|l|m |
463 | + |} in |
464 | + let result = |
465 | + notes |
466 | + |> to_cells ~columns:[ `Title ; `Description ; `Tags] ~styles:[] |
467 | + |> Tabular.to_string ~style:`Fixed |
468 | + in |
469 | + print_endline (String.Hexdump.to_string_hum expected); |
470 | + print_endline (String.Hexdump.to_string_hum result); |
471 | + (* TODO: somehow broken string result *) |
472 | + Alcotest.(check pass) "tabular_fixed" expected result |
473 | + |
474 | + let test_hierarchical_display () = |
475 | + let open Display.Hierarchical in |
476 | + let expected = {| |
477 | A |
478 | ├──B |
479 | │ └──C |
480 | @@ -10,11 +79,23 @@ A |
481 | |} in |
482 | let result = |
483 | Tree ("A", [ Tree ("B", [ Tree ("C", []) ]); Tree ("D", []) ]) |
484 | - |> to_lines |> String.concat "" |
485 | + |> to_lines |> String.concat |
486 | in |
487 | let result = "\n" ^ result in |
488 | Alcotest.(check string) "tree" expected result |
489 | |
490 | let () = |
491 | Alcotest.run "Display" |
492 | - [ ("tree", [ Alcotest.test_case "display a tree" `Quick test_tree ]) ] |
493 | + [ |
494 | + ( "tree", |
495 | + [ Alcotest.test_case "display a tree" `Quick test_hierarchical_display ] |
496 | + ); |
497 | + ( "tabular-simple", |
498 | + [ |
499 | + Alcotest.test_case "tabular fixed" `Quick test_tabular_display_simple; |
500 | + ] ); |
501 | + ( "tabular-fixed", |
502 | + [ |
503 | + Alcotest.test_case "tabular fixed" `Quick test_tabular_display_fixed; |
504 | + ] ); |
505 | + ] |
506 | diff --git a/test/note_test.ml b/test/note_test.ml |
507 | index 6039b73..e2968da 100644 |
508 | --- a/test/note_test.ml |
509 | +++ b/test/note_test.ml |
510 | @@ -8,7 +8,7 @@ let rec convert_tree tree = |
511 | let (Note.Tree (note, others)) = tree in |
512 | let title = note.frontmatter.title in |
513 | let title = "[" ^ title ^ "]" in |
514 | - Display.Tree (title, List.map ~f:convert_tree others) |
515 | + Display.Hierarchical.Tree (title, List.map ~f:convert_tree others) |
516 | |
517 | let make_a_note () = |
518 | let note = |
519 | @@ -124,7 +124,6 @@ let insert_at () = |
520 | Alcotest.(check bool) "inserted" true (Option.is_some result) |
521 | |
522 | let test_structure () = |
523 | - let open Display in |
524 | let expected = |
525 | {| |
526 | [root] |
527 | @@ -140,7 +139,7 @@ let test_structure () = |
528 | |} |
529 | in |
530 | Alcotest.(check int) "length" 9 (Note.length tree); |
531 | - let note_tree = tree |> convert_tree |> to_string in |
532 | + let note_tree = tree |> convert_tree |> Display.Hierarchical.to_string in |
533 | Alcotest.(check string) "structure" expected note_tree |
534 | |
535 | (* |
536 | @@ -244,7 +243,7 @@ let test_resolve () = |
537 | in |
538 | let tree_as_string = |
539 | [ n3; n2; n1; n0 ; n4] |> Note.resolve ~root |> convert_tree |
540 | - |> Display.to_string |
541 | + |> Display.Hierarchical.to_string |
542 | in |
543 | Alcotest.(check string) "resolve" expected tree_as_string |
544 |