View raw

1 (** README documentation rendering. *) 2 3 let contains text fragment = 4 let text_length = String.length text in 5 let fragment_length = String.length fragment in 6 let rec loop index = 7 if index > text_length - fragment_length then false 8 else if String.sub text index fragment_length = fragment then true 9 else loop (index + 1) 10 in 11 fragment_length <= text_length && loop 0 12 13 let count text fragment = 14 let text_length = String.length text in 15 let fragment_length = String.length fragment in 16 let rec loop index acc = 17 if index > text_length - fragment_length then acc 18 else if String.sub text index fragment_length = fragment then 19 loop (index + 1) (acc + 1) 20 else loop (index + 1) acc 21 in 22 if fragment_length > text_length then 0 else loop 0 0 23 24 let render ~filename content = 25 Prose.Render.render ~filename content |> Dream_html.to_string 26 27 let test_markdown_document () = 28 let html = 29 render ~filename:"README.md" 30 "# Project\n\nIntroduction.\n\n## Installation\n\n- Download\n- Run\n" 31 in 32 Alcotest.(check bool) 33 "documentation container" true 34 (contains html "readme-document readme-markdown"); 35 Alcotest.(check bool) "h1 heading" true (contains html "<h1 id=\"project\""); 36 Alcotest.(check bool) 37 "heading fragment link" true 38 (contains html "<a href=\"#project\" class=\"readme-heading-link\">Project"); 39 Alcotest.(check bool) 40 "nested h2 heading" true 41 (contains html "<h2 id=\"installation\""); 42 Alcotest.(check bool) 43 "no line-number anchors" false 44 (contains html "line-anchor") 45 46 let test_org_document () = 47 let html = 48 render ~filename:"README.org" 49 "#+TITLE: Org Project\n\ 50 #+AUTHOR: Ada Lovelace\n\ 51 #+DATE: 2026-08-01\n\n\ 52 * Overview\n\n\ 53 #+BEGIN_SRC ocaml\n\ 54 let answer = 42\n\ 55 #+END_SRC\n" 56 in 57 Alcotest.(check bool) 58 "Org document container" true 59 (contains html "readme-document readme-org"); 60 Alcotest.(check bool) 61 "title heading" true 62 (contains html "<h1 id=\"org-project\""); 63 Alcotest.(check bool) 64 "metadata definition list" true 65 (contains html "<dl class=\"readme-org-metadata\""); 66 Alcotest.(check bool) "author metadata" true (contains html "<dt>Author</dt>"); 67 Alcotest.(check bool) "Org heading" true (contains html "<h1 id=\"overview\""); 68 Alcotest.(check bool) 69 "highlighted source block" true 70 (contains html "source.ocaml-keyword") 71 72 let test_readme_filename_detection () = 73 Alcotest.(check bool) 74 "case-insensitive README name" true 75 (Prose.Render.is_readme_filename "ReadMe.ORG"); 76 Alcotest.(check bool) 77 "ordinary file is not a README" false 78 (Prose.Render.is_readme_filename "guide.md") 79 80 let test_markdown_toc () = 81 let html = 82 render ~filename:"README.md" 83 "# Project\n\n## Getting Started\n\nText.\n\n## API\n\nMore text.\n" 84 in 85 Alcotest.(check bool) 86 "TOC disclosure present" true 87 (contains html "readme-toc"); 88 Alcotest.(check bool) 89 "TOC contains summary" true 90 (contains html "Table of Contents"); 91 Alcotest.(check bool) 92 "TOC links to first heading" true 93 (contains html "href=\"#getting-started\""); 94 Alcotest.(check bool) 95 "TOC links to second heading" true 96 (contains html "href=\"#api\""); 97 Alcotest.(check bool) 98 "TOC has nested list for subheadings" true 99 (count html "toc-list" >= 2) 100 101 let test_org_toc () = 102 let html = 103 render ~filename:"README.org" 104 "#+TITLE: My Project\n\ 105 #+AUTHOR: Ada\n\n\ 106 * Overview\n\n\ 107 ** Installation\n\n\ 108 Text.\n\n\ 109 * Usage\n" 110 in 111 Alcotest.(check bool) 112 "Org TOC disclosure present" true 113 (contains html "readme-toc"); 114 Alcotest.(check bool) 115 "Org TOC links to Overview" true 116 (contains html "href=\"#overview\""); 117 Alcotest.(check bool) 118 "Org TOC links to Installation" true 119 (contains html "href=\"#installation\""); 120 Alcotest.(check bool) 121 "Org TOC links to Usage" true 122 (contains html "href=\"#usage\""); 123 Alcotest.(check bool) 124 "Org TOC has nested list for subheadings" true 125 (count html "toc-list" >= 2); 126 let find s sub = 127 let slen = String.length s and sublen = String.length sub in 128 let rec f i = 129 if i > slen - sublen then -1 130 else if String.sub s i sublen = sub then i 131 else f (i + 1) 132 in 133 f 0 134 in 135 let toc_at = find html "readme-toc\"" in 136 let meta_at = find html "readme-org-metadata" in 137 Alcotest.(check bool) 138 "Org metadata appears before TOC" true 139 (toc_at >= 0 && meta_at >= 0 && meta_at < toc_at) 140 141 let test_no_toc_without_headings () = 142 let html = render ~filename:"README.md" "Just a paragraph.\n" in 143 Alcotest.(check bool) 144 "no TOC when no headings" false 145 (contains html "readme-toc") 146 147 let test_org_inline_verbatim () = 148 let html = 149 render ~filename:"README.org" "* Notes\n\nUse =verbatim= for emphasis.\n" 150 in 151 Alcotest.(check bool) 152 "verbatim span" true 153 (contains html "<span class=\"readme-verbatim\">verbatim</span>"); 154 Alcotest.(check bool) "surrounding text preserved" true (contains html "Use ") 155 156 let test_org_inline_code () = 157 let html = 158 render ~filename:"README.org" "* Notes\n\nCall ~List.map~ here.\n" 159 in 160 Alcotest.(check bool) 161 "code element" true 162 (contains html "<code class=\"readme-code\">List.map</code>") 163 164 let test_org_link_with_description () = 165 let html = 166 render ~filename:"README.org" 167 "* Links\n\nSee [[https://example.com][Example Site]] for details.\n" 168 in 169 Alcotest.(check bool) 170 "link href" true 171 (contains html "href=\"https://example.com\""); 172 Alcotest.(check bool) "link text" true (contains html ">Example Site</a>") 173 174 let test_org_link_bare () = 175 let html = 176 render ~filename:"README.org" 177 "* Links\n\nVisit [[https://example.com]] today.\n" 178 in 179 Alcotest.(check bool) 180 "bare link href" true 181 (contains html "href=\"https://example.com\""); 182 Alcotest.(check bool) 183 "bare link text equals URL" true 184 (contains html ">https://example.com</a>") 185 186 let test_org_unordered_list () = 187 let html = 188 render ~filename:"README.org" 189 "* Tasks\n\n- First item\n- Second item\n- Third item\n" 190 in 191 Alcotest.(check bool) 192 "unordered list" true 193 (contains html "<ul class=\"readme-list\">"); 194 Alcotest.(check bool) "list item content" true (contains html "Second item") 195 196 let test_org_ordered_list () = 197 let html = 198 render ~filename:"README.org" "* Steps\n\n1. Download\n2. Install\n3. Run\n" 199 in 200 Alcotest.(check bool) 201 "ordered list" true 202 (contains html "<ol class=\"readme-list readme-ordered-list\">"); 203 Alcotest.(check bool) "ordered item content" true (contains html "Install") 204 205 let test_org_definition_list () = 206 let html = 207 render ~filename:"README.org" 208 "* Glossary\n\n\ 209 - Term One :: The first definition\n\ 210 - Term Two :: The second definition\n" 211 in 212 Alcotest.(check bool) 213 "definition list" true 214 (contains html "<dl class=\"readme-definition-list\">"); 215 Alcotest.(check bool) 216 "definition term" true 217 (contains html "<dt>Term One</dt>"); 218 Alcotest.(check bool) 219 "definition description" true 220 (contains html "The first definition") 221 222 let test_org_inline_in_list () = 223 let html = 224 render ~filename:"README.org" 225 "* Items\n\n- Use ~code~ in lists\n- And =verbatim= too\n" 226 in 227 Alcotest.(check bool) 228 "code in list item" true 229 (contains html "<code class=\"readme-code\">code</code>"); 230 Alcotest.(check bool) 231 "verbatim in list item" true 232 (contains html "<span class=\"readme-verbatim\">verbatim</span>") 233 234 let test_org_multiline_unordered () = 235 let html = 236 render ~filename:"README.org" 237 "* Notes\n\n- First item\n continues here\n- Second item\n" 238 in 239 Alcotest.(check bool) 240 "continuation joined to first item" true 241 (contains html "First item continues here"); 242 Alcotest.(check bool) 243 "second item separate" true 244 (contains html "Second item") 245 246 let test_org_multiline_ordered () = 247 let html = 248 render ~filename:"README.org" 249 "* Steps\n\n1. Download the\n archive first\n2. Install it\n" 250 in 251 Alcotest.(check bool) 252 "ordered continuation joined" true 253 (contains html "Download the archive first"); 254 Alcotest.(check bool) 255 "second ordered item separate" true 256 (contains html "Install it") 257 258 let test_org_multiline_definition () = 259 let html = 260 render ~filename:"README.org" 261 "* Glossary\n\n\ 262 - Term :: First line\n\ 263 \ of the definition\n\ 264 - Other :: Short\n" 265 in 266 Alcotest.(check bool) 267 "definition continuation joined" true 268 (contains html "First line of the definition"); 269 Alcotest.(check bool) 270 "second definition separate" true (contains html "Short") 271 272 let suite = 273 ( "readme", 274 [ 275 Alcotest.test_case "Markdown document" `Quick test_markdown_document; 276 Alcotest.test_case "Org document" `Quick test_org_document; 277 Alcotest.test_case "README filename detection" `Quick 278 test_readme_filename_detection; 279 Alcotest.test_case "Markdown TOC" `Quick test_markdown_toc; 280 Alcotest.test_case "Org TOC" `Quick test_org_toc; 281 Alcotest.test_case "No TOC without headings" `Quick 282 test_no_toc_without_headings; 283 Alcotest.test_case "Org verbatim inline" `Quick test_org_inline_verbatim; 284 Alcotest.test_case "Org code inline" `Quick test_org_inline_code; 285 Alcotest.test_case "Org link with description" `Quick 286 test_org_link_with_description; 287 Alcotest.test_case "Org bare link" `Quick test_org_link_bare; 288 Alcotest.test_case "Org unordered list" `Quick test_org_unordered_list; 289 Alcotest.test_case "Org ordered list" `Quick test_org_ordered_list; 290 Alcotest.test_case "Org definition list" `Quick test_org_definition_list; 291 Alcotest.test_case "Org inline in list items" `Quick 292 test_org_inline_in_list; 293 Alcotest.test_case "Org multi-line unordered items" `Quick 294 test_org_multiline_unordered; 295 Alcotest.test_case "Org multi-line ordered items" `Quick 296 test_org_multiline_ordered; 297 Alcotest.test_case "Org multi-line definition items" `Quick 298 test_org_multiline_definition; 299 ] ) 300