let test_version () = Alcotest.(check string) "version" "0.1.0" Org.version (* Prescan tests *) let test_prescan_default () = let config = Org.Private.Prescan.scan "" in let kws = Org.Config.all_todo_keywords config in Alcotest.(check (list string)) "default keywords" [ "TODO"; "DONE" ] kws let test_prescan_custom_todo () = let input = "#+TODO: NEXT WAITING | DONE CANCELLED\n" in let config = Org.Private.Prescan.scan input in let kws = Org.Config.all_todo_keywords config in Alcotest.(check (list string)) "custom keywords" [ "NEXT"; "WAITING"; "DONE"; "CANCELLED" ] kws let test_prescan_multiple_sequences () = let input = "#+TODO: TODO | DONE\n#+TODO: OPEN | CLOSED\n" in let config = Org.Private.Prescan.scan input in let kws = Org.Config.all_todo_keywords config in Alcotest.(check (list string)) "multiple sequences" [ "TODO"; "DONE"; "OPEN"; "CLOSED" ] kws let test_prescan_seq_todo () = let input = "#+SEQ_TODO: NEXT ACTIVE | DONE\n" in let config = Org.Private.Prescan.scan input in Alcotest.(check bool) "NEXT is todo" true (Org.Config.is_todo_keyword config "NEXT"); Alcotest.(check bool) "DONE is todo" true (Org.Config.is_todo_keyword config "DONE"); Alcotest.(check bool) "UNKNOWN is not todo" false (Org.Config.is_todo_keyword config "UNKNOWN") let test_prescan_case_insensitive_key () = let input = "#+todo: BUG FIX | RESOLVED\n" in let config = Org.Private.Prescan.scan input in let kws = Org.Config.all_todo_keywords config in Alcotest.(check (list string)) "case insensitive key" [ "BUG"; "FIX"; "RESOLVED" ] kws let test_prescan_no_bar () = let input = "#+TODO: TODO DOING DONE\n" in let config = Org.Private.Prescan.scan input in let seqs = config.todo_sequences in let seq = List.hd seqs in Alcotest.(check (list string)) "all active" [ "TODO"; "DOING"; "DONE" ] seq.active; Alcotest.(check (list string)) "no done" [] seq.done_ (* Lexer tests *) module L = Org.Private.Lexer module T = Org.Private.Parser let config = Org.Config.default let lex input = L.tokenize ~config input |> List.map fst let tok_to_string = function | T.BLANK -> "Blank" | T.HEADING (n, s) -> Printf.sprintf "Heading(%d, %S)" n s | T.KEYWORD (k, v) -> Printf.sprintf "Keyword(%S, %S)" k v | T.AFFILIATED_KEYWORD (n, _, v) -> Printf.sprintf "Affiliated(%S, %S)" n v | T.BEGIN_BLOCK (n, p) -> Printf.sprintf "Begin_block(%S, %s)" n (match p with Some s -> Printf.sprintf "%S" s | None -> "None") | T.END_BLOCK n -> Printf.sprintf "End_block(%S)" n | T.DRAWER_BEGIN n -> Printf.sprintf "Drawer_begin(%S)" n | T.DRAWER_END -> "Drawer_end" | T.PROPERTY (n, v) -> Printf.sprintf "Property(%S, %s)" n (match v with Some s -> Printf.sprintf "%S" s | None -> "None") | T.PLANNING _ -> "Planning" | T.LIST_ITEM d -> Printf.sprintf "List_item(indent=%d, bullet=%S)" d.lid_indent d.lid_bullet | T.TABLE_ROW _ -> "Table_row" | T.FIXED_WIDTH s -> Printf.sprintf "Fixed_width(%S)" s | T.COMMENT_LINE s -> Printf.sprintf "Comment_line(%S)" s | T.HORIZONTAL_RULE -> "Horizontal_rule" | T.TEXT_LINE s -> Printf.sprintf "Text_line(%S)" s | T.EOF -> "Eof" let check_tok msg expected actual = Alcotest.(check string) msg expected (tok_to_string actual) let test_lex_blank () = let toks = lex "" in check_tok "empty is blank+eof" "Blank" (List.nth toks 0); check_tok "eof" "Eof" (List.nth toks 1) let test_lex_heading () = let toks = lex "* Hello world" in check_tok "heading" "Heading(1, \"Hello world\")" (List.nth toks 0) let test_lex_heading_level () = let toks = lex "*** Deep heading" in check_tok "heading level 3" "Heading(3, \"Deep heading\")" (List.nth toks 0) let test_lex_keyword () = let toks = lex "#+TITLE: My Document" in check_tok "keyword" "Keyword(\"TITLE\", \"My Document\")" (List.nth toks 0) let test_lex_affiliated () = let toks = lex "#+NAME: my-table" in check_tok "affiliated" "Affiliated(\"NAME\", \"my-table\")" (List.nth toks 0) let test_lex_begin_block () = let toks = lex "#+begin_src ocaml" in check_tok "begin src" "Begin_block(\"src\", \"ocaml\")" (List.nth toks 0) let test_lex_end_block () = let toks = lex "#+end_src" in check_tok "end src" "End_block(\"src\")" (List.nth toks 0) let test_lex_drawer () = let toks = lex ":PROPERTIES:" in check_tok "drawer begin" "Drawer_begin(\"PROPERTIES\")" (List.nth toks 0) let test_lex_drawer_end () = let toks = lex ":END:" in check_tok "drawer end" "Drawer_end" (List.nth toks 0) let test_lex_list_unordered () = let toks = lex "- item one" in check_tok "unordered list" "List_item(indent=0, bullet=\"- \")" (List.nth toks 0) let test_lex_list_ordered () = let toks = lex "1. first item" in check_tok "ordered list" "List_item(indent=0, bullet=\"1. \")" (List.nth toks 0) let test_lex_list_indented () = let toks = lex " - nested" in check_tok "indented list" "List_item(indent=2, bullet=\"- \")" (List.nth toks 0) let test_lex_table () = let toks = lex "| a | b | c |" in check_tok "table row" "Table_row" (List.nth toks 0) let test_lex_comment () = let toks = lex "# a comment" in check_tok "comment" "Comment_line(\"a comment\")" (List.nth toks 0) let test_lex_fixed_width () = let toks = lex ": fixed text" in check_tok "fixed width" "Fixed_width(\"fixed text\")" (List.nth toks 0) let test_lex_hrule () = let toks = lex "-----" in check_tok "horizontal rule" "Horizontal_rule" (List.nth toks 0) let test_lex_text () = let toks = lex "Just some text" in check_tok "text line" "Text_line(\"Just some text\")" (List.nth toks 0) let test_lex_planning () = let toks = lex "DEADLINE: <2024-01-15 Mon>" in check_tok "planning" "Planning" (List.nth toks 0) let test_lex_property () = let toks = lex " :CUSTOM_ID: my-id" in check_tok "property" "Property(\"CUSTOM_ID\", \"my-id\")" (List.nth toks 0) (* Parser tests *) module R = Org.Private.Raw_ast module A = Org.Ast let parse input = Org.Private.Parse.document ~config input let parse_inline input = Org.Private.Parse.inline input let test_parse_empty () = let doc = parse "" in Alcotest.(check int) "no headings" 0 (List.length doc.R.rd_headings) let test_parse_single_heading () = let doc = parse "* Hello" in Alcotest.(check int) "one heading" 1 (List.length doc.R.rd_headings); let h = List.hd doc.R.rd_headings in Alcotest.(check int) "level 1" 1 h.R.rh_level; Alcotest.(check string) "title" "Hello" h.R.rh_raw_title let test_parse_heading_with_body () = let input = "* Heading\nSome text\nMore text" in let doc = parse input in Alcotest.(check int) "one heading" 1 (List.length doc.R.rd_headings); let h = List.hd doc.R.rd_headings in let has_paragraph = List.exists (function R.Raw_paragraph ls -> List.length ls > 0 | _ -> false) h.R.rh_body in Alcotest.(check bool) "has paragraph body" true has_paragraph let test_parse_preamble () = let input = "Some preamble text\n\n* Heading" in let doc = parse input in let non_blank = List.filter (function R.Raw_paragraph [] -> false | _ -> true) doc.R.rd_preamble in Alcotest.(check bool) "has preamble" true (List.length non_blank > 0); Alcotest.(check int) "one heading" 1 (List.length doc.R.rd_headings) let test_parse_keyword () = let input = "#+TITLE: My Doc\n* Heading" in let doc = parse input in let has_kw = List.exists (function R.Raw_keyword ("TITLE", _) -> true | _ -> false) doc.R.rd_preamble in Alcotest.(check bool) "has title keyword" true has_kw let test_parse_block () = let input = "#+begin_src ocaml\nlet x = 42\n#+end_src" in let doc = parse input in let has_block = List.exists (function R.Raw_block _ -> true | _ -> false) doc.R.rd_preamble in Alcotest.(check bool) "has block" true has_block let test_parse_list () = let input = "- item one\n- item two" in let doc = parse input in let has_list = List.exists (function R.Raw_list_items _ -> true | _ -> false) doc.R.rd_preamble in Alcotest.(check bool) "has list" true has_list (* Inline parser tests *) let test_inline_plain () = let result = parse_inline "hello world" in match result with | [ A.Plain s ] -> Alcotest.(check string) "plain" "hello world" s | _ -> Alcotest.fail "expected single Plain" let test_inline_bold () = let result = parse_inline "*bold*" in match result with | [ A.Bold [ A.Plain s ] ] -> Alcotest.(check string) "bold content" "bold" s | _ -> Alcotest.fail "expected Bold" let test_inline_italic () = let result = parse_inline "/italic/" in match result with | [ A.Italic [ A.Plain s ] ] -> Alcotest.(check string) "italic content" "italic" s | _ -> Alcotest.fail "expected Italic" let test_inline_code () = let result = parse_inline "~code~" in match result with | [ A.Code s ] -> Alcotest.(check string) "code content" "code" s | _ -> Alcotest.fail "expected Code" let test_inline_verbatim () = let result = parse_inline "=verb=" in match result with | [ A.Verbatim s ] -> Alcotest.(check string) "verbatim content" "verb" s | _ -> Alcotest.fail "expected Verbatim" let test_inline_link () = let result = parse_inline "[[https://org.org][Org]]" in match result with | [ A.Link lnk ] -> ( (match lnk.target with | A.Url u -> Alcotest.(check string) "url" "https://org.org" u | _ -> Alcotest.fail "expected Url target"); match lnk.description with | Some [ A.Plain d ] -> Alcotest.(check string) "desc" "Org" d | _ -> Alcotest.fail "expected description") | _ -> Alcotest.fail "expected Link" let test_inline_link_no_desc () = let result = parse_inline "[[file:readme.org]]" in match result with | [ A.Link lnk ] -> (match lnk.target with | A.File f -> Alcotest.(check string) "file" "readme.org" f | _ -> Alcotest.fail "expected File target"); Alcotest.(check bool) "no description" true (lnk.description = None) | _ -> Alcotest.fail "expected Link" let test_inline_timestamp () = let result = parse_inline "<2024-01-15 Mon>" in match result with | [ A.Timestamp (A.Active (d, _)) ] -> Alcotest.(check int) "year" 2024 d.year; Alcotest.(check int) "month" 1 d.month; Alcotest.(check int) "day" 15 d.day | _ -> Alcotest.fail "expected active timestamp" let test_inline_plain_link () = let result = parse_inline "see https://example.com for info" in let has_link = List.exists (function A.Link _ -> true | _ -> false) result in Alcotest.(check bool) "has plain link" true has_link let test_inline_mixed () = let result = parse_inline "Hello *bold* and ~code~" in let has_bold = List.exists (function A.Bold _ -> true | _ -> false) result in let has_code = List.exists (function A.Code _ -> true | _ -> false) result in Alcotest.(check bool) "has bold" true has_bold; Alcotest.(check bool) "has code" true has_code (* ================================================================== *) (* Integration tests *) (* ================================================================== *) let full_fixture = {|#+TITLE: Integration Test Document #+AUTHOR: Test Author #+TODO: TODO NEXT WAITING | DONE CANCELLED This is the preamble paragraph with *bold*, /italic/, and ~code~. See https://orgmode.org for more information. * TODO [#A] First Heading :project:important: SCHEDULED: <2024-06-15 Sat> :PROPERTIES: :CUSTOM_ID: first-heading :EFFORT: 2h :END: A paragraph with [[https://example.com][a link]] and =verbatim text=. ** DONE Sub-heading CLOSED: [2024-06-10 Mon 14:30] Another paragraph with +strikethrough+ and _underline_. ** NEXT Another sub-heading :review: *** Deep heading Content in a deeply nested heading. * Heading without TODO ** Child heading Paragraph text. * Lists and Blocks ** Unordered list - First item - Second item with *bold* - Nested item - Another nested item - Third item ** Ordered list 1. First 2. Second 3. Third ** Descriptive list - Term one :: Description of term one - Term two :: Description of term two ** Source block #+begin_src ocaml let greeting = "Hello, Org!" let () = print_endline greeting #+end_src ** Quote block #+begin_quote This is a quoted paragraph. It spans multiple lines. #+end_quote ** Example block #+begin_example This is example text. Preserved verbatim. #+end_example * Tables | Name | Age | City | |-------+-----+----------| | Alice | 30 | New York | | Bob | 25 | London | * Miscellaneous A fixed-width section: : This is fixed width : Another fixed line A comment: # This is a comment # Another comment line ----- Text after horizontal rule. :NOTES: This is content inside a drawer. :END: * Timestamps An active timestamp: <2024-01-15 Mon 10:00>. An inactive timestamp: [2024-03-20 Wed]. A range: <2024-01-15 Mon>--<2024-01-20 Sat>. |} let full_doc = Org.parse full_fixture let test_integration_structure () = (* Directives *) let dirs = full_doc.A.directives in Alcotest.(check bool) "has directives" true (List.length dirs >= 2); let title_dir = List.find_opt (fun d -> d.A.dir_key = "TITLE") dirs in Alcotest.(check bool) "has TITLE" true (Option.is_some title_dir); Alcotest.(check string) "TITLE value" "Integration Test Document" (Option.get title_dir).A.dir_value; let author_dir = List.find_opt (fun d -> d.A.dir_key = "AUTHOR") dirs in Alcotest.(check bool) "has AUTHOR" true (Option.is_some author_dir); Alcotest.(check string) "AUTHOR value" "Test Author" (Option.get author_dir).A.dir_value; (* Preamble *) Alcotest.(check bool) "has preamble" true (List.length full_doc.A.preamble > 0); (* Top-level headings *) Alcotest.(check int) "6 top-level headings" 6 (List.length full_doc.A.headings) let test_integration_headings () = let h1 = List.nth full_doc.A.headings 0 in (* First heading: TODO, priority, tags *) Alcotest.(check (option string)) "h1 todo" (Some "TODO") h1.A.todo; Alcotest.(check (option char)) "h1 priority" (Some 'A') h1.A.priority; Alcotest.(check (list string)) "h1 tags" [ "project"; "important" ] h1.A.tags; Alcotest.(check int) "h1 level" 1 h1.A.level; (* Planning *) Alcotest.(check bool) "h1 has planning" true (Option.is_some h1.A.planning); let planning = Option.get h1.A.planning in Alcotest.(check bool) "h1 has scheduled" true (Option.is_some planning.A.scheduled); (* Properties *) Alcotest.(check bool) "h1 has properties" true (List.length h1.A.properties >= 2); let custom_id = List.find_opt (fun p -> p.A.prop_name = "CUSTOM_ID") h1.A.properties in Alcotest.(check bool) "has CUSTOM_ID" true (Option.is_some custom_id); Alcotest.(check (option string)) "CUSTOM_ID value" (Some "first-heading") (Option.get custom_id).A.prop_value; let effort = List.find_opt (fun p -> p.A.prop_name = "EFFORT") h1.A.properties in Alcotest.(check bool) "has EFFORT" true (Option.is_some effort); Alcotest.(check (option string)) "EFFORT value" (Some "2h") (Option.get effort).A.prop_value; (* Children of first heading *) Alcotest.(check int) "h1 has 2 children" 2 (List.length h1.A.children); let child1 = List.nth h1.A.children 0 in Alcotest.(check (option string)) "child1 todo" (Some "DONE") child1.A.todo; (* DONE sub-heading has CLOSED planning *) Alcotest.(check bool) "child1 has planning" true (Option.is_some child1.A.planning); let child1_planning = Option.get child1.A.planning in Alcotest.(check bool) "child1 has closed" true (Option.is_some child1_planning.A.closed); let child2 = List.nth h1.A.children 1 in Alcotest.(check (option string)) "child2 todo" (Some "NEXT") child2.A.todo; Alcotest.(check (list string)) "child2 tags" [ "review" ] child2.A.tags; (* Deep heading is a child of child2 *) Alcotest.(check int) "child2 has 1 child" 1 (List.length child2.A.children); let deep = List.nth child2.A.children 0 in Alcotest.(check int) "deep level" 3 deep.A.level; (* Heading without TODO *) let h2 = List.nth full_doc.A.headings 1 in Alcotest.(check (option string)) "h2 no todo" None h2.A.todo; Alcotest.(check int) "h2 has 1 child" 1 (List.length h2.A.children) let test_integration_elements () = (* Lists and Blocks heading is at index 2 *) let h3 = List.nth full_doc.A.headings 2 in (* h3 children: Unordered list, Ordered list, Descriptive list, Source block, Quote block, Example block *) Alcotest.(check bool) "Lists and Blocks has children" true (List.length h3.A.children >= 6); (* Unordered list *) let ul_heading = List.nth h3.A.children 0 in let ul_elems = ul_heading.A.contents in let has_unordered = List.exists (function | A.Plain_list (A.Unordered, items) -> List.length items >= 3 | _ -> false) ul_elems in Alcotest.(check bool) "has unordered list" true has_unordered; (* Ordered list *) let ol_heading = List.nth h3.A.children 1 in let ol_elems = ol_heading.A.contents in let has_ordered = List.exists (function | A.Plain_list (A.Ordered, items) -> List.length items >= 3 | _ -> false) ol_elems in Alcotest.(check bool) "has ordered list" true has_ordered; (* Descriptive list *) let dl_heading = List.nth h3.A.children 2 in let dl_elems = dl_heading.A.contents in let has_descriptive = List.exists (function | A.Plain_list (A.Descriptive, items) -> List.length items >= 2 | _ -> false) dl_elems in Alcotest.(check bool) "has descriptive list" true has_descriptive; (* Source block *) let src_heading = List.nth h3.A.children 3 in let src_elems = src_heading.A.contents in let has_src = List.exists (function | A.Block (A.Src_block sb) -> sb.A.src_language = Some "ocaml" && String.length sb.A.src_value > 0 | _ -> false) src_elems in Alcotest.(check bool) "has src block with ocaml" true has_src; (* Quote block *) let qt_heading = List.nth h3.A.children 4 in let qt_elems = qt_heading.A.contents in let has_quote = List.exists (function A.Block (A.Quote_block _) -> true | _ -> false) qt_elems in Alcotest.(check bool) "has quote block" true has_quote; (* Example block *) let ex_heading = List.nth h3.A.children 5 in let ex_elems = ex_heading.A.contents in let has_example = List.exists (function A.Block (A.Example_block _) -> true | _ -> false) ex_elems in Alcotest.(check bool) "has example block" true has_example; (* Tables heading is at index 3 *) let h4 = List.nth full_doc.A.headings 3 in let table_elems = h4.A.contents in let has_table = List.exists (function | A.Table t -> let has_standard = List.exists (function A.Table_row_standard _ -> true | _ -> false) t.A.rows in let has_rule = List.exists (function A.Table_row_rule -> true | _ -> false) t.A.rows in has_standard && has_rule | _ -> false) table_elems in Alcotest.(check bool) "has table with rows and rules" true has_table; (* Miscellaneous heading is at index 4 *) let h5 = List.nth full_doc.A.headings 4 in let misc_elems = h5.A.contents in let has_fixed_width = List.exists (function A.Fixed_width lines -> List.length lines >= 2 | _ -> false) misc_elems in Alcotest.(check bool) "has fixed-width" true has_fixed_width; let has_comment = List.exists (function A.Comment lines -> List.length lines >= 2 | _ -> false) misc_elems in Alcotest.(check bool) "has comment" true has_comment; let has_hrule = List.exists (function A.Horizontal_rule -> true | _ -> false) misc_elems in Alcotest.(check bool) "has horizontal rule" true has_hrule; let has_drawer = List.exists (function A.Drawer ("NOTES", _) -> true | _ -> false) misc_elems in Alcotest.(check bool) "has drawer" true has_drawer let test_integration_inline () = (* Preamble paragraph should have inline markup *) let preamble_para = List.find_opt (function A.Paragraph _ -> true | _ -> false) full_doc.A.preamble in Alcotest.(check bool) "preamble has paragraph" true (Option.is_some preamble_para); let inlines = match preamble_para with Some (A.Paragraph (il, _)) -> il | _ -> [] in let has_bold = List.exists (function A.Bold _ -> true | _ -> false) inlines in let has_italic = List.exists (function A.Italic _ -> true | _ -> false) inlines in let has_code = List.exists (function A.Code _ -> true | _ -> false) inlines in Alcotest.(check bool) "preamble has bold" true has_bold; Alcotest.(check bool) "preamble has italic" true has_italic; Alcotest.(check bool) "preamble has code" true has_code; (* Second preamble paragraph should have a plain link *) let preamble_paras = List.filter (function A.Paragraph _ -> true | _ -> false) full_doc.A.preamble in Alcotest.(check bool) "preamble has 2+ paragraphs" true (List.length preamble_paras >= 2); let para2_inlines = match List.nth preamble_paras 1 with A.Paragraph (il, _) -> il | _ -> [] in let has_link = List.exists (function A.Link _ -> true | _ -> false) para2_inlines in Alcotest.(check bool) "preamble para2 has link" true has_link; (* Timestamps heading (index 5) *) let h6 = List.nth full_doc.A.headings 5 in let ts_elems = h6.A.contents in let all_inlines = List.concat_map (function A.Paragraph (il, _) -> il | _ -> []) ts_elems in let has_active_ts = List.exists (function A.Timestamp (A.Active _) -> true | _ -> false) all_inlines in let has_inactive_ts = List.exists (function A.Timestamp (A.Inactive _) -> true | _ -> false) all_inlines in let has_range = List.exists (function A.Timestamp (A.Active_range _) -> true | _ -> false) all_inlines in Alcotest.(check bool) "has active timestamp" true has_active_ts; Alcotest.(check bool) "has inactive timestamp" true has_inactive_ts; Alcotest.(check bool) "has timestamp range" true has_range let () = Alcotest.run "org" [ ("basic", [ Alcotest.test_case "version" `Quick test_version ]); ( "prescan", [ Alcotest.test_case "default config" `Quick test_prescan_default; Alcotest.test_case "custom TODO" `Quick test_prescan_custom_todo; Alcotest.test_case "multiple sequences" `Quick test_prescan_multiple_sequences; Alcotest.test_case "SEQ_TODO" `Quick test_prescan_seq_todo; Alcotest.test_case "case insensitive key" `Quick test_prescan_case_insensitive_key; Alcotest.test_case "no bar separator" `Quick test_prescan_no_bar; ] ); ( "lexer", [ Alcotest.test_case "blank" `Quick test_lex_blank; Alcotest.test_case "heading" `Quick test_lex_heading; Alcotest.test_case "heading level" `Quick test_lex_heading_level; Alcotest.test_case "keyword" `Quick test_lex_keyword; Alcotest.test_case "affiliated keyword" `Quick test_lex_affiliated; Alcotest.test_case "begin block" `Quick test_lex_begin_block; Alcotest.test_case "end block" `Quick test_lex_end_block; Alcotest.test_case "drawer begin" `Quick test_lex_drawer; Alcotest.test_case "drawer end" `Quick test_lex_drawer_end; Alcotest.test_case "unordered list" `Quick test_lex_list_unordered; Alcotest.test_case "ordered list" `Quick test_lex_list_ordered; Alcotest.test_case "indented list" `Quick test_lex_list_indented; Alcotest.test_case "table row" `Quick test_lex_table; Alcotest.test_case "comment" `Quick test_lex_comment; Alcotest.test_case "fixed width" `Quick test_lex_fixed_width; Alcotest.test_case "horizontal rule" `Quick test_lex_hrule; Alcotest.test_case "text line" `Quick test_lex_text; Alcotest.test_case "planning" `Quick test_lex_planning; Alcotest.test_case "property" `Quick test_lex_property; ] ); ( "parser", [ Alcotest.test_case "empty document" `Quick test_parse_empty; Alcotest.test_case "single heading" `Quick test_parse_single_heading; Alcotest.test_case "heading with body" `Quick test_parse_heading_with_body; Alcotest.test_case "preamble" `Quick test_parse_preamble; Alcotest.test_case "keyword" `Quick test_parse_keyword; Alcotest.test_case "block" `Quick test_parse_block; Alcotest.test_case "list" `Quick test_parse_list; ] ); ( "inline", [ Alcotest.test_case "plain text" `Quick test_inline_plain; Alcotest.test_case "bold" `Quick test_inline_bold; Alcotest.test_case "italic" `Quick test_inline_italic; Alcotest.test_case "code" `Quick test_inline_code; Alcotest.test_case "verbatim" `Quick test_inline_verbatim; Alcotest.test_case "link with desc" `Quick test_inline_link; Alcotest.test_case "link no desc" `Quick test_inline_link_no_desc; Alcotest.test_case "timestamp" `Quick test_inline_timestamp; Alcotest.test_case "plain link" `Quick test_inline_plain_link; Alcotest.test_case "mixed inline" `Quick test_inline_mixed; ] ); ( "integration", [ Alcotest.test_case "structure" `Quick test_integration_structure; Alcotest.test_case "headings" `Quick test_integration_headings; Alcotest.test_case "elements" `Quick test_integration_elements; Alcotest.test_case "inline" `Quick test_integration_inline; ] ); ]