[OCaml] Mobile-friendly clone of cgit.
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