[OCaml] Org mode parser.
1
let test_version () = Alcotest.(check string) "version" "0.1.0" Org.version
2
3
(* Prescan tests *)
4
5
let test_prescan_default () =
6
let config = Org.Private.Prescan.scan "" in
7
let kws = Org.Config.all_todo_keywords config in
8
Alcotest.(check (list string)) "default keywords" [ "TODO"; "DONE" ] kws
9
10
let test_prescan_custom_todo () =
11
let input = "#+TODO: NEXT WAITING | DONE CANCELLED\n" in
12
let config = Org.Private.Prescan.scan input in
13
let kws = Org.Config.all_todo_keywords config in
14
Alcotest.(check (list string))
15
"custom keywords"
16
[ "NEXT"; "WAITING"; "DONE"; "CANCELLED" ]
17
kws
18
19
let test_prescan_multiple_sequences () =
20
let input = "#+TODO: TODO | DONE\n#+TODO: OPEN | CLOSED\n" in
21
let config = Org.Private.Prescan.scan input in
22
let kws = Org.Config.all_todo_keywords config in
23
Alcotest.(check (list string))
24
"multiple sequences"
25
[ "TODO"; "DONE"; "OPEN"; "CLOSED" ]
26
kws
27
28
let test_prescan_seq_todo () =
29
let input = "#+SEQ_TODO: NEXT ACTIVE | DONE\n" in
30
let config = Org.Private.Prescan.scan input in
31
Alcotest.(check bool)
32
"NEXT is todo" true
33
(Org.Config.is_todo_keyword config "NEXT");
34
Alcotest.(check bool)
35
"DONE is todo" true
36
(Org.Config.is_todo_keyword config "DONE");
37
Alcotest.(check bool)
38
"UNKNOWN is not todo" false
39
(Org.Config.is_todo_keyword config "UNKNOWN")
40
41
let test_prescan_case_insensitive_key () =
42
let input = "#+todo: BUG FIX | RESOLVED\n" in
43
let config = Org.Private.Prescan.scan input in
44
let kws = Org.Config.all_todo_keywords config in
45
Alcotest.(check (list string))
46
"case insensitive key"
47
[ "BUG"; "FIX"; "RESOLVED" ]
48
kws
49
50
let test_prescan_no_bar () =
51
let input = "#+TODO: TODO DOING DONE\n" in
52
let config = Org.Private.Prescan.scan input in
53
let seqs = config.todo_sequences in
54
let seq = List.hd seqs in
55
Alcotest.(check (list string))
56
"all active"
57
[ "TODO"; "DOING"; "DONE" ]
58
seq.active;
59
Alcotest.(check (list string)) "no done" [] seq.done_
60
61
(* Lexer tests *)
62
module L = Org.Private.Lexer
63
module T = Org.Private.Parser
64
65
let config = Org.Config.default
66
let lex input = L.tokenize ~config input |> List.map fst
67
68
let tok_to_string = function
69
| T.BLANK -> "Blank"
70
| T.HEADING (n, s) -> Printf.sprintf "Heading(%d, %S)" n s
71
| T.KEYWORD (k, v) -> Printf.sprintf "Keyword(%S, %S)" k v
72
| T.AFFILIATED_KEYWORD (n, _, v) -> Printf.sprintf "Affiliated(%S, %S)" n v
73
| T.BEGIN_BLOCK (n, p) ->
74
Printf.sprintf "Begin_block(%S, %s)" n
75
(match p with Some s -> Printf.sprintf "%S" s | None -> "None")
76
| T.END_BLOCK n -> Printf.sprintf "End_block(%S)" n
77
| T.DRAWER_BEGIN n -> Printf.sprintf "Drawer_begin(%S)" n
78
| T.DRAWER_END -> "Drawer_end"
79
| T.PROPERTY (n, v) ->
80
Printf.sprintf "Property(%S, %s)" n
81
(match v with Some s -> Printf.sprintf "%S" s | None -> "None")
82
| T.PLANNING _ -> "Planning"
83
| T.LIST_ITEM d ->
84
Printf.sprintf "List_item(indent=%d, bullet=%S)" d.lid_indent d.lid_bullet
85
| T.TABLE_ROW _ -> "Table_row"
86
| T.FIXED_WIDTH s -> Printf.sprintf "Fixed_width(%S)" s
87
| T.COMMENT_LINE s -> Printf.sprintf "Comment_line(%S)" s
88
| T.HORIZONTAL_RULE -> "Horizontal_rule"
89
| T.TEXT_LINE s -> Printf.sprintf "Text_line(%S)" s
90
| T.EOF -> "Eof"
91
92
let check_tok msg expected actual =
93
Alcotest.(check string) msg expected (tok_to_string actual)
94
95
let test_lex_blank () =
96
let toks = lex "" in
97
check_tok "empty is blank+eof" "Blank" (List.nth toks 0);
98
check_tok "eof" "Eof" (List.nth toks 1)
99
100
let test_lex_heading () =
101
let toks = lex "* Hello world" in
102
check_tok "heading" "Heading(1, \"Hello world\")" (List.nth toks 0)
103
104
let test_lex_heading_level () =
105
let toks = lex "*** Deep heading" in
106
check_tok "heading level 3" "Heading(3, \"Deep heading\")" (List.nth toks 0)
107
108
let test_lex_keyword () =
109
let toks = lex "#+TITLE: My Document" in
110
check_tok "keyword" "Keyword(\"TITLE\", \"My Document\")" (List.nth toks 0)
111
112
let test_lex_affiliated () =
113
let toks = lex "#+NAME: my-table" in
114
check_tok "affiliated" "Affiliated(\"NAME\", \"my-table\")" (List.nth toks 0)
115
116
let test_lex_begin_block () =
117
let toks = lex "#+begin_src ocaml" in
118
check_tok "begin src" "Begin_block(\"src\", \"ocaml\")" (List.nth toks 0)
119
120
let test_lex_end_block () =
121
let toks = lex "#+end_src" in
122
check_tok "end src" "End_block(\"src\")" (List.nth toks 0)
123
124
let test_lex_drawer () =
125
let toks = lex ":PROPERTIES:" in
126
check_tok "drawer begin" "Drawer_begin(\"PROPERTIES\")" (List.nth toks 0)
127
128
let test_lex_drawer_end () =
129
let toks = lex ":END:" in
130
check_tok "drawer end" "Drawer_end" (List.nth toks 0)
131
132
let test_lex_list_unordered () =
133
let toks = lex "- item one" in
134
check_tok "unordered list" "List_item(indent=0, bullet=\"- \")"
135
(List.nth toks 0)
136
137
let test_lex_list_ordered () =
138
let toks = lex "1. first item" in
139
check_tok "ordered list" "List_item(indent=0, bullet=\"1. \")"
140
(List.nth toks 0)
141
142
let test_lex_list_indented () =
143
let toks = lex " - nested" in
144
check_tok "indented list" "List_item(indent=2, bullet=\"- \")"
145
(List.nth toks 0)
146
147
let test_lex_table () =
148
let toks = lex "| a | b | c |" in
149
check_tok "table row" "Table_row" (List.nth toks 0)
150
151
let test_lex_comment () =
152
let toks = lex "# a comment" in
153
check_tok "comment" "Comment_line(\"a comment\")" (List.nth toks 0)
154
155
let test_lex_fixed_width () =
156
let toks = lex ": fixed text" in
157
check_tok "fixed width" "Fixed_width(\"fixed text\")" (List.nth toks 0)
158
159
let test_lex_hrule () =
160
let toks = lex "-----" in
161
check_tok "horizontal rule" "Horizontal_rule" (List.nth toks 0)
162
163
let test_lex_text () =
164
let toks = lex "Just some text" in
165
check_tok "text line" "Text_line(\"Just some text\")" (List.nth toks 0)
166
167
let test_lex_planning () =
168
let toks = lex "DEADLINE: <2024-01-15 Mon>" in
169
check_tok "planning" "Planning" (List.nth toks 0)
170
171
let test_lex_property () =
172
let toks = lex " :CUSTOM_ID: my-id" in
173
check_tok "property" "Property(\"CUSTOM_ID\", \"my-id\")" (List.nth toks 0)
174
175
(* Parser tests *)
176
module R = Org.Private.Raw_ast
177
module A = Org.Ast
178
179
let parse input = Org.Private.Parse.document ~config input
180
let parse_inline input = Org.Private.Parse.inline input
181
182
let test_parse_empty () =
183
let doc = parse "" in
184
Alcotest.(check int) "no headings" 0 (List.length doc.R.rd_headings)
185
186
let test_parse_single_heading () =
187
let doc = parse "* Hello" in
188
Alcotest.(check int) "one heading" 1 (List.length doc.R.rd_headings);
189
let h = List.hd doc.R.rd_headings in
190
Alcotest.(check int) "level 1" 1 h.R.rh_level;
191
Alcotest.(check string) "title" "Hello" h.R.rh_raw_title
192
193
let test_parse_heading_with_body () =
194
let input = "* Heading\nSome text\nMore text" in
195
let doc = parse input in
196
Alcotest.(check int) "one heading" 1 (List.length doc.R.rd_headings);
197
let h = List.hd doc.R.rd_headings in
198
let has_paragraph =
199
List.exists
200
(function R.Raw_paragraph ls -> List.length ls > 0 | _ -> false)
201
h.R.rh_body
202
in
203
Alcotest.(check bool) "has paragraph body" true has_paragraph
204
205
let test_parse_preamble () =
206
let input = "Some preamble text\n\n* Heading" in
207
let doc = parse input in
208
let non_blank =
209
List.filter
210
(function R.Raw_paragraph [] -> false | _ -> true)
211
doc.R.rd_preamble
212
in
213
Alcotest.(check bool) "has preamble" true (List.length non_blank > 0);
214
Alcotest.(check int) "one heading" 1 (List.length doc.R.rd_headings)
215
216
let test_parse_keyword () =
217
let input = "#+TITLE: My Doc\n* Heading" in
218
let doc = parse input in
219
let has_kw =
220
List.exists
221
(function R.Raw_keyword ("TITLE", _) -> true | _ -> false)
222
doc.R.rd_preamble
223
in
224
Alcotest.(check bool) "has title keyword" true has_kw
225
226
let test_parse_block () =
227
let input = "#+begin_src ocaml\nlet x = 42\n#+end_src" in
228
let doc = parse input in
229
let has_block =
230
List.exists
231
(function R.Raw_block _ -> true | _ -> false)
232
doc.R.rd_preamble
233
in
234
Alcotest.(check bool) "has block" true has_block
235
236
let test_parse_list () =
237
let input = "- item one\n- item two" in
238
let doc = parse input in
239
let has_list =
240
List.exists
241
(function R.Raw_list_items _ -> true | _ -> false)
242
doc.R.rd_preamble
243
in
244
Alcotest.(check bool) "has list" true has_list
245
246
(* Inline parser tests *)
247
let test_inline_plain () =
248
let result = parse_inline "hello world" in
249
match result with
250
| [ A.Plain s ] -> Alcotest.(check string) "plain" "hello world" s
251
| _ -> Alcotest.fail "expected single Plain"
252
253
let test_inline_bold () =
254
let result = parse_inline "*bold*" in
255
match result with
256
| [ A.Bold [ A.Plain s ] ] -> Alcotest.(check string) "bold content" "bold" s
257
| _ -> Alcotest.fail "expected Bold"
258
259
let test_inline_italic () =
260
let result = parse_inline "/italic/" in
261
match result with
262
| [ A.Italic [ A.Plain s ] ] ->
263
Alcotest.(check string) "italic content" "italic" s
264
| _ -> Alcotest.fail "expected Italic"
265
266
let test_inline_code () =
267
let result = parse_inline "~code~" in
268
match result with
269
| [ A.Code s ] -> Alcotest.(check string) "code content" "code" s
270
| _ -> Alcotest.fail "expected Code"
271
272
let test_inline_verbatim () =
273
let result = parse_inline "=verb=" in
274
match result with
275
| [ A.Verbatim s ] -> Alcotest.(check string) "verbatim content" "verb" s
276
| _ -> Alcotest.fail "expected Verbatim"
277
278
let test_inline_link () =
279
let result = parse_inline "[[https://org.org][Org]]" in
280
match result with
281
| [ A.Link lnk ] -> (
282
(match lnk.target with
283
| A.Url u -> Alcotest.(check string) "url" "https://org.org" u
284
| _ -> Alcotest.fail "expected Url target");
285
match lnk.description with
286
| Some [ A.Plain d ] -> Alcotest.(check string) "desc" "Org" d
287
| _ -> Alcotest.fail "expected description")
288
| _ -> Alcotest.fail "expected Link"
289
290
let test_inline_link_no_desc () =
291
let result = parse_inline "[[file:readme.org]]" in
292
match result with
293
| [ A.Link lnk ] ->
294
(match lnk.target with
295
| A.File f -> Alcotest.(check string) "file" "readme.org" f
296
| _ -> Alcotest.fail "expected File target");
297
Alcotest.(check bool) "no description" true (lnk.description = None)
298
| _ -> Alcotest.fail "expected Link"
299
300
let test_inline_timestamp () =
301
let result = parse_inline "<2024-01-15 Mon>" in
302
match result with
303
| [ A.Timestamp (A.Active (d, _)) ] ->
304
Alcotest.(check int) "year" 2024 d.year;
305
Alcotest.(check int) "month" 1 d.month;
306
Alcotest.(check int) "day" 15 d.day
307
| _ -> Alcotest.fail "expected active timestamp"
308
309
let test_inline_plain_link () =
310
let result = parse_inline "see https://example.com for info" in
311
let has_link =
312
List.exists (function A.Link _ -> true | _ -> false) result
313
in
314
Alcotest.(check bool) "has plain link" true has_link
315
316
let test_inline_mixed () =
317
let result = parse_inline "Hello *bold* and ~code~" in
318
let has_bold =
319
List.exists (function A.Bold _ -> true | _ -> false) result
320
in
321
let has_code =
322
List.exists (function A.Code _ -> true | _ -> false) result
323
in
324
Alcotest.(check bool) "has bold" true has_bold;
325
Alcotest.(check bool) "has code" true has_code
326
327
(* ================================================================== *)
328
(* Integration tests *)
329
(* ================================================================== *)
330
331
let full_fixture =
332
{|#+TITLE: Integration Test Document
333
#+AUTHOR: Test Author
334
#+TODO: TODO NEXT WAITING | DONE CANCELLED
335
336
This is the preamble paragraph with *bold*, /italic/, and ~code~.
337
338
See https://orgmode.org for more information.
339
340
* TODO [#A] First Heading :project:important:
341
SCHEDULED: <2024-06-15 Sat>
342
:PROPERTIES:
343
:CUSTOM_ID: first-heading
344
:EFFORT: 2h
345
:END:
346
347
A paragraph with [[https://example.com][a link]] and =verbatim text=.
348
349
** DONE Sub-heading
350
CLOSED: [2024-06-10 Mon 14:30]
351
352
Another paragraph with +strikethrough+ and _underline_.
353
354
** NEXT Another sub-heading :review:
355
356
*** Deep heading
357
358
Content in a deeply nested heading.
359
360
* Heading without TODO
361
362
** Child heading
363
364
Paragraph text.
365
366
* Lists and Blocks
367
368
** Unordered list
369
370
- First item
371
- Second item with *bold*
372
- Nested item
373
- Another nested item
374
- Third item
375
376
** Ordered list
377
378
1. First
379
2. Second
380
3. Third
381
382
** Descriptive list
383
384
- Term one :: Description of term one
385
- Term two :: Description of term two
386
387
** Source block
388
389
#+begin_src ocaml
390
let greeting = "Hello, Org!"
391
let () = print_endline greeting
392
#+end_src
393
394
** Quote block
395
396
#+begin_quote
397
This is a quoted paragraph.
398
It spans multiple lines.
399
#+end_quote
400
401
** Example block
402
403
#+begin_example
404
This is example text.
405
Preserved verbatim.
406
#+end_example
407
408
* Tables
409
410
| Name | Age | City |
411
|-------+-----+----------|
412
| Alice | 30 | New York |
413
| Bob | 25 | London |
414
415
* Miscellaneous
416
417
A fixed-width section:
418
419
: This is fixed width
420
: Another fixed line
421
422
A comment:
423
424
# This is a comment
425
# Another comment line
426
427
-----
428
429
Text after horizontal rule.
430
431
:NOTES:
432
This is content inside a drawer.
433
:END:
434
435
* Timestamps
436
437
An active timestamp: <2024-01-15 Mon 10:00>.
438
439
An inactive timestamp: [2024-03-20 Wed].
440
441
A range: <2024-01-15 Mon>--<2024-01-20 Sat>.
442
|}
443
444
let full_doc = Org.parse full_fixture
445
446
let test_integration_structure () =
447
(* Directives *)
448
let dirs = full_doc.A.directives in
449
Alcotest.(check bool) "has directives" true (List.length dirs >= 2);
450
let title_dir = List.find_opt (fun d -> d.A.dir_key = "TITLE") dirs in
451
Alcotest.(check bool) "has TITLE" true (Option.is_some title_dir);
452
Alcotest.(check string)
453
"TITLE value" "Integration Test Document" (Option.get title_dir).A.dir_value;
454
let author_dir = List.find_opt (fun d -> d.A.dir_key = "AUTHOR") dirs in
455
Alcotest.(check bool) "has AUTHOR" true (Option.is_some author_dir);
456
Alcotest.(check string)
457
"AUTHOR value" "Test Author" (Option.get author_dir).A.dir_value;
458
(* Preamble *)
459
Alcotest.(check bool) "has preamble" true (List.length full_doc.A.preamble > 0);
460
(* Top-level headings *)
461
Alcotest.(check int)
462
"6 top-level headings" 6
463
(List.length full_doc.A.headings)
464
465
let test_integration_headings () =
466
let h1 = List.nth full_doc.A.headings 0 in
467
(* First heading: TODO, priority, tags *)
468
Alcotest.(check (option string)) "h1 todo" (Some "TODO") h1.A.todo;
469
Alcotest.(check (option char)) "h1 priority" (Some 'A') h1.A.priority;
470
Alcotest.(check (list string)) "h1 tags" [ "project"; "important" ] h1.A.tags;
471
Alcotest.(check int) "h1 level" 1 h1.A.level;
472
(* Planning *)
473
Alcotest.(check bool) "h1 has planning" true (Option.is_some h1.A.planning);
474
let planning = Option.get h1.A.planning in
475
Alcotest.(check bool)
476
"h1 has scheduled" true
477
(Option.is_some planning.A.scheduled);
478
(* Properties *)
479
Alcotest.(check bool)
480
"h1 has properties" true
481
(List.length h1.A.properties >= 2);
482
let custom_id =
483
List.find_opt (fun p -> p.A.prop_name = "CUSTOM_ID") h1.A.properties
484
in
485
Alcotest.(check bool) "has CUSTOM_ID" true (Option.is_some custom_id);
486
Alcotest.(check (option string))
487
"CUSTOM_ID value" (Some "first-heading") (Option.get custom_id).A.prop_value;
488
let effort =
489
List.find_opt (fun p -> p.A.prop_name = "EFFORT") h1.A.properties
490
in
491
Alcotest.(check bool) "has EFFORT" true (Option.is_some effort);
492
Alcotest.(check (option string))
493
"EFFORT value" (Some "2h") (Option.get effort).A.prop_value;
494
(* Children of first heading *)
495
Alcotest.(check int) "h1 has 2 children" 2 (List.length h1.A.children);
496
let child1 = List.nth h1.A.children 0 in
497
Alcotest.(check (option string)) "child1 todo" (Some "DONE") child1.A.todo;
498
(* DONE sub-heading has CLOSED planning *)
499
Alcotest.(check bool)
500
"child1 has planning" true
501
(Option.is_some child1.A.planning);
502
let child1_planning = Option.get child1.A.planning in
503
Alcotest.(check bool)
504
"child1 has closed" true
505
(Option.is_some child1_planning.A.closed);
506
let child2 = List.nth h1.A.children 1 in
507
Alcotest.(check (option string)) "child2 todo" (Some "NEXT") child2.A.todo;
508
Alcotest.(check (list string)) "child2 tags" [ "review" ] child2.A.tags;
509
(* Deep heading is a child of child2 *)
510
Alcotest.(check int) "child2 has 1 child" 1 (List.length child2.A.children);
511
let deep = List.nth child2.A.children 0 in
512
Alcotest.(check int) "deep level" 3 deep.A.level;
513
(* Heading without TODO *)
514
let h2 = List.nth full_doc.A.headings 1 in
515
Alcotest.(check (option string)) "h2 no todo" None h2.A.todo;
516
Alcotest.(check int) "h2 has 1 child" 1 (List.length h2.A.children)
517
518
let test_integration_elements () =
519
(* Lists and Blocks heading is at index 2 *)
520
let h3 = List.nth full_doc.A.headings 2 in
521
(* h3 children: Unordered list, Ordered list, Descriptive list,
522
Source block, Quote block, Example block *)
523
Alcotest.(check bool)
524
"Lists and Blocks has children" true
525
(List.length h3.A.children >= 6);
526
(* Unordered list *)
527
let ul_heading = List.nth h3.A.children 0 in
528
let ul_elems = ul_heading.A.contents in
529
let has_unordered =
530
List.exists
531
(function
532
| A.Plain_list (A.Unordered, items) -> List.length items >= 3
533
| _ -> false)
534
ul_elems
535
in
536
Alcotest.(check bool) "has unordered list" true has_unordered;
537
(* Ordered list *)
538
let ol_heading = List.nth h3.A.children 1 in
539
let ol_elems = ol_heading.A.contents in
540
let has_ordered =
541
List.exists
542
(function
543
| A.Plain_list (A.Ordered, items) -> List.length items >= 3 | _ -> false)
544
ol_elems
545
in
546
Alcotest.(check bool) "has ordered list" true has_ordered;
547
(* Descriptive list *)
548
let dl_heading = List.nth h3.A.children 2 in
549
let dl_elems = dl_heading.A.contents in
550
let has_descriptive =
551
List.exists
552
(function
553
| A.Plain_list (A.Descriptive, items) -> List.length items >= 2
554
| _ -> false)
555
dl_elems
556
in
557
Alcotest.(check bool) "has descriptive list" true has_descriptive;
558
(* Source block *)
559
let src_heading = List.nth h3.A.children 3 in
560
let src_elems = src_heading.A.contents in
561
let has_src =
562
List.exists
563
(function
564
| A.Block (A.Src_block sb) ->
565
sb.A.src_language = Some "ocaml" && String.length sb.A.src_value > 0
566
| _ -> false)
567
src_elems
568
in
569
Alcotest.(check bool) "has src block with ocaml" true has_src;
570
(* Quote block *)
571
let qt_heading = List.nth h3.A.children 4 in
572
let qt_elems = qt_heading.A.contents in
573
let has_quote =
574
List.exists
575
(function A.Block (A.Quote_block _) -> true | _ -> false)
576
qt_elems
577
in
578
Alcotest.(check bool) "has quote block" true has_quote;
579
(* Example block *)
580
let ex_heading = List.nth h3.A.children 5 in
581
let ex_elems = ex_heading.A.contents in
582
let has_example =
583
List.exists
584
(function A.Block (A.Example_block _) -> true | _ -> false)
585
ex_elems
586
in
587
Alcotest.(check bool) "has example block" true has_example;
588
(* Tables heading is at index 3 *)
589
let h4 = List.nth full_doc.A.headings 3 in
590
let table_elems = h4.A.contents in
591
let has_table =
592
List.exists
593
(function
594
| A.Table t ->
595
let has_standard =
596
List.exists
597
(function A.Table_row_standard _ -> true | _ -> false)
598
t.A.rows
599
in
600
let has_rule =
601
List.exists
602
(function A.Table_row_rule -> true | _ -> false)
603
t.A.rows
604
in
605
has_standard && has_rule
606
| _ -> false)
607
table_elems
608
in
609
Alcotest.(check bool) "has table with rows and rules" true has_table;
610
(* Miscellaneous heading is at index 4 *)
611
let h5 = List.nth full_doc.A.headings 4 in
612
let misc_elems = h5.A.contents in
613
let has_fixed_width =
614
List.exists
615
(function A.Fixed_width lines -> List.length lines >= 2 | _ -> false)
616
misc_elems
617
in
618
Alcotest.(check bool) "has fixed-width" true has_fixed_width;
619
let has_comment =
620
List.exists
621
(function A.Comment lines -> List.length lines >= 2 | _ -> false)
622
misc_elems
623
in
624
Alcotest.(check bool) "has comment" true has_comment;
625
let has_hrule =
626
List.exists (function A.Horizontal_rule -> true | _ -> false) misc_elems
627
in
628
Alcotest.(check bool) "has horizontal rule" true has_hrule;
629
let has_drawer =
630
List.exists
631
(function A.Drawer ("NOTES", _) -> true | _ -> false)
632
misc_elems
633
in
634
Alcotest.(check bool) "has drawer" true has_drawer
635
636
let test_integration_inline () =
637
(* Preamble paragraph should have inline markup *)
638
let preamble_para =
639
List.find_opt
640
(function A.Paragraph _ -> true | _ -> false)
641
full_doc.A.preamble
642
in
643
Alcotest.(check bool)
644
"preamble has paragraph" true
645
(Option.is_some preamble_para);
646
let inlines =
647
match preamble_para with Some (A.Paragraph (il, _)) -> il | _ -> []
648
in
649
let has_bold =
650
List.exists (function A.Bold _ -> true | _ -> false) inlines
651
in
652
let has_italic =
653
List.exists (function A.Italic _ -> true | _ -> false) inlines
654
in
655
let has_code =
656
List.exists (function A.Code _ -> true | _ -> false) inlines
657
in
658
Alcotest.(check bool) "preamble has bold" true has_bold;
659
Alcotest.(check bool) "preamble has italic" true has_italic;
660
Alcotest.(check bool) "preamble has code" true has_code;
661
(* Second preamble paragraph should have a plain link *)
662
let preamble_paras =
663
List.filter
664
(function A.Paragraph _ -> true | _ -> false)
665
full_doc.A.preamble
666
in
667
Alcotest.(check bool)
668
"preamble has 2+ paragraphs" true
669
(List.length preamble_paras >= 2);
670
let para2_inlines =
671
match List.nth preamble_paras 1 with A.Paragraph (il, _) -> il | _ -> []
672
in
673
let has_link =
674
List.exists (function A.Link _ -> true | _ -> false) para2_inlines
675
in
676
Alcotest.(check bool) "preamble para2 has link" true has_link;
677
(* Timestamps heading (index 5) *)
678
let h6 = List.nth full_doc.A.headings 5 in
679
let ts_elems = h6.A.contents in
680
let all_inlines =
681
List.concat_map (function A.Paragraph (il, _) -> il | _ -> []) ts_elems
682
in
683
let has_active_ts =
684
List.exists
685
(function A.Timestamp (A.Active _) -> true | _ -> false)
686
all_inlines
687
in
688
let has_inactive_ts =
689
List.exists
690
(function A.Timestamp (A.Inactive _) -> true | _ -> false)
691
all_inlines
692
in
693
let has_range =
694
List.exists
695
(function A.Timestamp (A.Active_range _) -> true | _ -> false)
696
all_inlines
697
in
698
Alcotest.(check bool) "has active timestamp" true has_active_ts;
699
Alcotest.(check bool) "has inactive timestamp" true has_inactive_ts;
700
Alcotest.(check bool) "has timestamp range" true has_range
701
702
let () =
703
Alcotest.run "org"
704
[
705
("basic", [ Alcotest.test_case "version" `Quick test_version ]);
706
( "prescan",
707
[
708
Alcotest.test_case "default config" `Quick test_prescan_default;
709
Alcotest.test_case "custom TODO" `Quick test_prescan_custom_todo;
710
Alcotest.test_case "multiple sequences" `Quick
711
test_prescan_multiple_sequences;
712
Alcotest.test_case "SEQ_TODO" `Quick test_prescan_seq_todo;
713
Alcotest.test_case "case insensitive key" `Quick
714
test_prescan_case_insensitive_key;
715
Alcotest.test_case "no bar separator" `Quick test_prescan_no_bar;
716
] );
717
( "lexer",
718
[
719
Alcotest.test_case "blank" `Quick test_lex_blank;
720
Alcotest.test_case "heading" `Quick test_lex_heading;
721
Alcotest.test_case "heading level" `Quick test_lex_heading_level;
722
Alcotest.test_case "keyword" `Quick test_lex_keyword;
723
Alcotest.test_case "affiliated keyword" `Quick test_lex_affiliated;
724
Alcotest.test_case "begin block" `Quick test_lex_begin_block;
725
Alcotest.test_case "end block" `Quick test_lex_end_block;
726
Alcotest.test_case "drawer begin" `Quick test_lex_drawer;
727
Alcotest.test_case "drawer end" `Quick test_lex_drawer_end;
728
Alcotest.test_case "unordered list" `Quick test_lex_list_unordered;
729
Alcotest.test_case "ordered list" `Quick test_lex_list_ordered;
730
Alcotest.test_case "indented list" `Quick test_lex_list_indented;
731
Alcotest.test_case "table row" `Quick test_lex_table;
732
Alcotest.test_case "comment" `Quick test_lex_comment;
733
Alcotest.test_case "fixed width" `Quick test_lex_fixed_width;
734
Alcotest.test_case "horizontal rule" `Quick test_lex_hrule;
735
Alcotest.test_case "text line" `Quick test_lex_text;
736
Alcotest.test_case "planning" `Quick test_lex_planning;
737
Alcotest.test_case "property" `Quick test_lex_property;
738
] );
739
( "parser",
740
[
741
Alcotest.test_case "empty document" `Quick test_parse_empty;
742
Alcotest.test_case "single heading" `Quick test_parse_single_heading;
743
Alcotest.test_case "heading with body" `Quick
744
test_parse_heading_with_body;
745
Alcotest.test_case "preamble" `Quick test_parse_preamble;
746
Alcotest.test_case "keyword" `Quick test_parse_keyword;
747
Alcotest.test_case "block" `Quick test_parse_block;
748
Alcotest.test_case "list" `Quick test_parse_list;
749
] );
750
( "inline",
751
[
752
Alcotest.test_case "plain text" `Quick test_inline_plain;
753
Alcotest.test_case "bold" `Quick test_inline_bold;
754
Alcotest.test_case "italic" `Quick test_inline_italic;
755
Alcotest.test_case "code" `Quick test_inline_code;
756
Alcotest.test_case "verbatim" `Quick test_inline_verbatim;
757
Alcotest.test_case "link with desc" `Quick test_inline_link;
758
Alcotest.test_case "link no desc" `Quick test_inline_link_no_desc;
759
Alcotest.test_case "timestamp" `Quick test_inline_timestamp;
760
Alcotest.test_case "plain link" `Quick test_inline_plain_link;
761
Alcotest.test_case "mixed inline" `Quick test_inline_mixed;
762
] );
763
( "integration",
764
[
765
Alcotest.test_case "structure" `Quick test_integration_structure;
766
Alcotest.test_case "headings" `Quick test_integration_headings;
767
Alcotest.test_case "elements" `Quick test_integration_elements;
768
Alcotest.test_case "inline" `Quick test_integration_inline;
769
] );
770
]
771