[OCaml] Mobile-friendly clone of cgit.
feat enhance Org README rendering and fix code block whitespace
Org mode inline markup: - =verbatim= and ~code~ text rendered as semantic elements - [[link][description]] and [[link]] rendered as clickable anchors Org mode lists: - Ordered lists (1. / 1)) with multi-line continuation support - Unordered lists (- / + / indented *) with multi-line continuation - Definition lists (- term :: description) with multi-line continuation Code block fix: - Pre-serialize highlighted spans into a single raw text node inside <pre><code> to prevent Dream_html's pretty-printer from injecting visible whitespace between tokens.
Changed files
lib/readme.ml
@@ -11,9 +11,13 @@
11
11
type block =
12
12
| Heading of int * string
13
13
| Paragraph of string
14
Removed:
| List of string list
14
Added:
| Unordered_list of string list
15
Added:
| Ordered_list of string list
16
Added:
| Definition_list of (string * string) list
15
17
| Code of string option * string
16
18
19
Added:
(* {1 Utilities} *)
20
Added:
17
21
let first_word text =
18
22
match
19
23
String.split_on_char ' ' text |> List.filter (fun word -> word <> "")
@@ -30,6 +34,95 @@
30
34
| ".org" -> Org
31
35
| _ -> Markdown
32
36
37
Added:
(* {1 Inline markup (Org)} *)
38
Added:
39
Added:
(** Parse Org inline markup: =verbatim=, ~code~, and [[link][desc]] / [[link]].
40
Added:
Returns a list of UI nodes with safe escaping. *)
41
Added:
let org_inline text =
42
Added:
let len = String.length text in
43
Added:
let buf = Buffer.create 64 in
44
Added:
let nodes = ref [] in
45
Added:
let flush () =
46
Added:
if Buffer.length buf > 0 then (
47
Added:
nodes := Ui.text (Buffer.contents buf) :: !nodes;
48
Added:
Buffer.clear buf)
49
Added:
in
50
Added:
let rec loop i =
51
Added:
if i >= len then flush ()
52
Added:
else
53
Added:
match text.[i] with
54
Added:
| '[' when i + 1 < len && text.[i + 1] = '[' ->
55
Added:
flush ();
56
Added:
parse_link (i + 2)
57
Added:
| ('=' | '~') as marker -> (
58
Added:
(* Look for closing marker — must not span across whitespace-only *)
59
Added:
let close = find_close marker (i + 1) in
60
Added:
match close with
61
Added:
| Some end_pos ->
62
Added:
flush ();
63
Added:
let content = String.sub text (i + 1) (end_pos - i - 1) in
64
Added:
let node =
65
Added:
match marker with
66
Added:
| '~' -> Ui.code_inline ~class_:"readme-code" content
67
Added:
| _ -> Ui.inline ~class_:"readme-verbatim" [ Ui.text content ]
68
Added:
in
69
Added:
nodes := node :: !nodes;
70
Added:
loop (end_pos + 1)
71
Added:
| None ->
72
Added:
Buffer.add_char buf text.[i];
73
Added:
loop (i + 1))
74
Added:
| c ->
75
Added:
Buffer.add_char buf c;
76
Added:
loop (i + 1)
77
Added:
and find_close marker start =
78
Added:
let rec search j =
79
Added:
if j >= len then None
80
Added:
else if text.[j] = marker then Some j
81
Added:
else if text.[j] = '\n' then None
82
Added:
else search (j + 1)
83
Added:
in
84
Added:
if start >= len then None else search start
85
Added:
and parse_link start =
86
Added:
(* Find matching ]] — possibly with ][desc] in between *)
87
Added:
let rec find_end j depth =
88
Added:
if j >= len then None
89
Added:
else if j + 1 < len && text.[j] = ']' && text.[j + 1] = ']' then Some j
90
Added:
else find_end (j + 1) depth
91
Added:
in
92
Added:
match find_end start 0 with
93
Added:
| None ->
94
Added:
Buffer.add_string buf "[[";
95
Added:
loop start
96
Added:
| Some close_pos ->
97
Added:
let inner = String.sub text start (close_pos - start) in
98
Added:
let href, desc =
99
Added:
match String.index_opt inner ']' with
100
Added:
| Some bracket_pos
101
Added:
when bracket_pos + 1 < String.length inner
102
Added:
&& inner.[bracket_pos + 1] = '[' ->
103
Added:
let href = String.sub inner 0 bracket_pos in
104
Added:
let desc =
105
Added:
String.sub inner (bracket_pos + 2)
106
Added:
(String.length inner - bracket_pos - 2)
107
Added:
in
108
Added:
(href, desc)
109
Added:
| _ -> (inner, inner)
110
Added:
in
111
Added:
let node = Ui.link ~class_:"readme-link" ~href [ Ui.text desc ] in
112
Added:
nodes := node :: !nodes;
113
Added:
loop (close_pos + 2)
114
Added:
in
115
Added:
loop 0;
116
Added:
List.rev !nodes
117
Added:
118
Added:
(** For Markdown, text is currently rendered as escaped prose without inline
119
Added:
parsing. *)
120
Added:
let plain_inline text = [ Ui.text text ]
121
Added:
122
Added:
let inline_of_format = function Org -> org_inline | Markdown -> plain_inline
123
Added:
124
Added:
(* {1 Line classification} *)
125
Added:
33
126
let trim_end_hashes text =
34
127
let text = String.trim text in
35
128
let rec last_non_hash index =
@@ -67,12 +160,70 @@
67
160
let heading_of_line format =
68
161
match format with Markdown -> markdown_heading | Org -> org_heading
69
162
70
Removed:
let list_item line =
163
Added:
(** Unordered list item: - / + / * followed by a space *)
164
Added:
let unordered_item line =
71
165
let length = String.length line in
72
Removed:
if length >= 2 && List.mem line.[0] [ '-'; '+'; '*' ] && line.[1] = ' ' then
73
Removed:
Some (String.sub line 2 (length - 2) |> String.trim)
166
Added:
let trimmed = String.trim line in
167
Added:
let tlen = String.length trimmed in
168
Added:
if tlen >= 2 && List.mem trimmed.[0] [ '-'; '+' ] && trimmed.[1] = ' ' then
169
Added:
Some (String.sub trimmed 2 (tlen - 2) |> String.trim)
170
Added:
else if
171
Added:
tlen >= 2
172
Added:
&& trimmed.[0] = '*'
173
Added:
&& trimmed.[1] = ' '
174
Added:
&& length > 0
175
Added:
&& (line.[0] = ' ' || line.[0] = '\t')
176
Added:
then
177
Added:
(* Org: * at BOL is a heading; indented * is a list item *)
178
Added:
Some (String.sub trimmed 2 (tlen - 2) |> String.trim)
74
179
else None
75
180
181
Added:
(** Ordered list item: 1. or 1) followed by a space *)
182
Added:
let ordered_item line =
183
Added:
let trimmed = String.trim line in
184
Added:
let tlen = String.length trimmed in
185
Added:
let rec digits i =
186
Added:
if i < tlen && trimmed.[i] >= '0' && trimmed.[i] <= '9' then digits (i + 1)
187
Added:
else i
188
Added:
in
189
Added:
let d = digits 0 in
190
Added:
if d = 0 || d >= tlen then None
191
Added:
else if
192
Added:
(trimmed.[d] = '.' || trimmed.[d] = ')')
193
Added:
&& d + 1 < tlen
194
Added:
&& trimmed.[d + 1] = ' '
195
Added:
then Some (String.sub trimmed (d + 2) (tlen - d - 2) |> String.trim)
196
Added:
else None
197
Added:
198
Added:
(** Org definition list item: - term :: description *)
199
Added:
let definition_item line =
200
Added:
let trimmed = String.trim line in
201
Added:
let tlen = String.length trimmed in
202
Added:
if tlen < 2 || trimmed.[0] <> '-' || trimmed.[1] <> ' ' then None
203
Added:
else
204
Added:
let rest = String.sub trimmed 2 (tlen - 2) in
205
Added:
match String.split_on_char ':' rest with
206
Added:
| [] -> None
207
Added:
| _ ->
208
Added:
(* Look for " :: " separator *)
209
Added:
let rec find_sep i =
210
Added:
if i + 3 >= String.length rest then None
211
Added:
else if
212
Added:
rest.[i] = ' '
213
Added:
&& rest.[i + 1] = ':'
214
Added:
&& rest.[i + 2] = ':'
215
Added:
&& rest.[i + 3] = ' '
216
Added:
then
217
Added:
let term = String.sub rest 0 i |> String.trim in
218
Added:
let desc =
219
Added:
String.sub rest (i + 4) (String.length rest - i - 4)
220
Added:
|> String.trim
221
Added:
in
222
Added:
Some (term, desc)
223
Added:
else find_sep (i + 1)
224
Added:
in
225
Added:
find_sep 0
226
Added:
76
227
let markdown_fence line =
77
228
let line = String.trim line in
78
229
if String.length line < 3 then None
@@ -114,17 +265,59 @@
114
265
let is_boundary format line =
115
266
String.trim line = ""
116
267
|| Option.is_some (heading_of_line format line)
117
Removed:
|| Option.is_some (list_item line)
268
Added:
|| Option.is_some (unordered_item line)
269
Added:
|| Option.is_some (ordered_item line)
270
Added:
|| Option.is_some (definition_item line)
118
271
|| is_code_opener format line
119
272
273
Added:
(* {1 Block parsing} *)
274
Added:
275
Added:
(** A continuation line belongs to the current list item if it is indented
276
Added:
(starts with whitespace) and is not blank. *)
277
Added:
let is_continuation line =
278
Added:
String.length line > 0
279
Added:
&& (line.[0] = ' ' || line.[0] = '\t')
280
Added:
&& String.trim line <> ""
281
Added:
120
282
let parse_blocks format lines =
121
Removed:
let rec take_list collected = function
283
Added:
let take_continuations rest =
284
Added:
let rec loop acc = function
285
Added:
| line :: rest when is_continuation line ->
286
Added:
loop (String.trim line :: acc) rest
287
Added:
| remaining -> (List.rev acc, remaining)
288
Added:
in
289
Added:
loop [] rest
290
Added:
in
291
Added:
let rec take_unordered collected = function
122
292
| line :: rest -> (
123
Removed:
match list_item line with
124
Removed:
| Some item -> take_list (item :: collected) rest
293
Added:
match unordered_item line with
294
Added:
| Some first_line ->
295
Added:
let continuations, rest = take_continuations rest in
296
Added:
let item = String.concat " " (first_line :: continuations) in
297
Added:
take_unordered (item :: collected) rest
125
298
| None -> (List.rev collected, line :: rest))
126
299
| [] -> (List.rev collected, [])
127
300
in
301
Added:
let rec take_ordered collected = function
302
Added:
| line :: rest -> (
303
Added:
match ordered_item line with
304
Added:
| Some first_line ->
305
Added:
let continuations, rest = take_continuations rest in
306
Added:
let item = String.concat " " (first_line :: continuations) in
307
Added:
take_ordered (item :: collected) rest
308
Added:
| None -> (List.rev collected, line :: rest))
309
Added:
| [] -> (List.rev collected, [])
310
Added:
in
311
Added:
let rec take_definitions collected = function
312
Added:
| line :: rest -> (
313
Added:
match definition_item line with
314
Added:
| Some (term, first_desc) ->
315
Added:
let continuations, rest = take_continuations rest in
316
Added:
let desc = String.concat " " (first_desc :: continuations) in
317
Added:
take_definitions ((term, desc) :: collected) rest
318
Added:
| None -> (List.rev collected, line :: rest))
319
Added:
| [] -> (List.rev collected, [])
320
Added:
in
128
321
let rec take_paragraph collected = function
129
322
| line :: _ as rest when is_boundary format line ->
130
323
(List.rev collected, rest)
@@ -153,17 +346,22 @@
153
346
(Code (language, String.concat "\n" lines) :: blocks)
154
347
rest
155
348
| None -> (
156
Removed:
match list_item line with
349
Added:
match unordered_item line with
157
350
| Some _ ->
158
Removed:
let items, rest = take_list [] (line :: rest) in
159
Removed:
loop (List items :: blocks) rest
160
Removed:
| None ->
161
Removed:
let paragraph, rest =
162
Removed:
take_paragraph [] (line :: rest)
163
Removed:
in
164
Removed:
loop
165
Removed:
(Paragraph (String.concat " " paragraph) :: blocks)
166
Removed:
rest))
351
Added:
let items, rest = take_unordered [] (line :: rest) in
352
Added:
loop (Unordered_list items :: blocks) rest
353
Added:
| None -> (
354
Added:
match ordered_item line with
355
Added:
| Some _ ->
356
Added:
let items, rest = take_ordered [] (line :: rest) in
357
Added:
loop (Ordered_list items :: blocks) rest
358
Added:
| None ->
359
Added:
let paragraph, rest =
360
Added:
take_paragraph [] (line :: rest)
361
Added:
in
362
Added:
loop
363
Added:
(Paragraph (String.concat " " paragraph) :: blocks)
364
Added:
rest)))
167
365
| Org -> (
168
366
match org_src_begin line with
169
367
| Some language ->
@@ -172,20 +370,37 @@
172
370
(Code (language, String.concat "\n" lines) :: blocks)
173
371
rest
174
372
| None -> (
175
Removed:
match list_item line with
373
Added:
match definition_item line with
176
374
| Some _ ->
177
Removed:
let items, rest = take_list [] (line :: rest) in
178
Removed:
loop (List items :: blocks) rest
179
Removed:
| None ->
180
Removed:
let paragraph, rest =
181
Removed:
take_paragraph [] (line :: rest)
182
Removed:
in
183
Removed:
loop
184
Removed:
(Paragraph (String.concat " " paragraph) :: blocks)
185
Removed:
rest))))
375
Added:
let items, rest = take_definitions [] (line :: rest) in
376
Added:
loop (Definition_list items :: blocks) rest
377
Added:
| None -> (
378
Added:
match unordered_item line with
379
Added:
| Some _ ->
380
Added:
let items, rest =
381
Added:
take_unordered [] (line :: rest)
382
Added:
in
383
Added:
loop (Unordered_list items :: blocks) rest
384
Added:
| None -> (
385
Added:
match ordered_item line with
386
Added:
| Some _ ->
387
Added:
let items, rest =
388
Added:
take_ordered [] (line :: rest)
389
Added:
in
390
Added:
loop (Ordered_list items :: blocks) rest
391
Added:
| None ->
392
Added:
let paragraph, rest =
393
Added:
take_paragraph [] (line :: rest)
394
Added:
in
395
Added:
loop
396
Added:
(Paragraph (String.concat " " paragraph)
397
Added:
:: blocks)
398
Added:
rest))))))
186
399
in
187
400
loop [] lines
188
401
402
Added:
(* {1 Org metadata} *)
403
Added:
189
404
let org_metadata_line line =
190
405
let prefix = "#+" in
191
406
let line = String.trim line in
@@ -212,6 +427,8 @@
212
427
([], []) lines
213
428
|> fun (metadata, body) -> (List.rev metadata, List.rev body)
214
429
430
Added:
(* {1 Rendering} *)
431
Added:
215
432
let new_anchor () =
216
433
let seen = Hashtbl.create 16 in
217
434
fun text ->
@@ -251,12 +468,22 @@
251
468
in
252
469
Ui.code_block ~class_:"readme-code-block" nodes
253
470
254
Removed:
let render_block anchor = function
471
Added:
let render_block format anchor = function
255
472
| Heading (level, text) -> heading anchor level text
256
Removed:
| Paragraph text -> Ui.paragraph ~class_:"readme-paragraph" [ Ui.text text ]
257
Removed:
| List items ->
473
Added:
| Paragraph text ->
474
Added:
Ui.paragraph ~class_:"readme-paragraph" (inline_of_format format text)
475
Added:
| Unordered_list items ->
476
Added:
let inline = inline_of_format format in
258
477
Ui.items ~class_:"readme-list"
259
Removed:
(List.map (fun item -> Ui.item [ Ui.text item ]) items)
478
Added:
(List.map (fun item -> Ui.item (inline item)) items)
479
Added:
| Ordered_list items ->
480
Added:
let inline = inline_of_format format in
481
Added:
Ui.ordered_items ~class_:"readme-list readme-ordered-list"
482
Added:
(List.map (fun item -> Ui.item (inline item)) items)
483
Added:
| Definition_list items ->
484
Added:
let inline = inline_of_format format in
485
Added:
Ui.definitions ~class_:"readme-definition-list"
486
Added:
(List.map (fun (term, desc) -> (term, inline desc)) items)
260
487
| Code (language, source) -> code_block language source
261
488
262
489
let render ~filename content =
@@ -292,4 +519,4 @@
292
519
in
293
520
Ui.region ~class_
294
521
(Option.to_list title @ [ metadata ]
295
Removed:
@ List.map (render_block anchor) (parse_blocks format lines))
522
Added:
@ List.map (render_block format anchor) (parse_blocks format lines))
lib/static/readme.css
@@ -71,6 +71,56 @@
71
71
margin-top: 0.3rem;
72
72
}
73
73
74
Added:
.readme-ordered-list {
75
Added:
list-style: decimal;
76
Added:
}
77
Added:
78
Added:
.readme-definition-list {
79
Added:
display: grid;
80
Added:
grid-template-columns: max-content 1fr;
81
Added:
gap: 0.25rem 0.75rem;
82
Added:
margin: 0.75rem 0;
83
Added:
padding: 0;
84
Added:
}
85
Added:
86
Added:
.readme-definition-list dt {
87
Added:
font-weight: 600;
88
Added:
color: #fabd2f;
89
Added:
}
90
Added:
91
Added:
.readme-definition-list dd {
92
Added:
margin: 0;
93
Added:
}
94
Added:
95
Added:
/* Org inline markup */
96
Added:
.readme-verbatim {
97
Added:
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
98
Added:
font-size: 0.88em;
99
Added:
padding: 0.15em 0.35em;
100
Added:
border-radius: 0.2rem;
101
Added:
background-color: #2a2a2a;
102
Added:
color: #d5c4a1;
103
Added:
}
104
Added:
105
Added:
.readme-code {
106
Added:
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
107
Added:
font-size: 0.88em;
108
Added:
padding: 0.15em 0.35em;
109
Added:
border-radius: 0.2rem;
110
Added:
background-color: #1d2021;
111
Added:
color: #8ec07c;
112
Added:
}
113
Added:
114
Added:
.readme-link {
115
Added:
color: var(--color-link-hover);
116
Added:
text-decoration: underline;
117
Added:
text-underline-offset: 0.15em;
118
Added:
}
119
Added:
120
Added:
.readme-link:hover {
121
Added:
color: white;
122
Added:
}
123
Added:
74
124
.readme-code-block {
75
125
margin: 1rem 0;
76
126
padding: 0.9rem 1rem;
lib/views/ui.ml
@@ -83,17 +83,36 @@
83
83
element (opt_id id @ opt_class class_) children
84
84
85
85
let code_block ?class_ children =
86
Removed:
HTML.pre (opt_class class_) [ HTML.code [] children ]
86
Added:
(* Pre-serialize the entire <code> block into a single raw text node placed
87
Added:
directly inside <pre>. This prevents the pretty-printer from injecting
88
Added:
visible whitespace between the <pre> open tag and the code content.
89
Added:
Dream_html.to_string appends a newline after each element; strip those to
90
Added:
avoid spurious line breaks between inline spans. *)
91
Added:
let raw_content =
92
Added:
children
93
Added:
|> List.map (fun node ->
94
Added:
let s = Dream_html.to_string node in
95
Added:
if String.length s > 0 && s.[String.length s - 1] = '\n' then
96
Added:
String.sub s 0 (String.length s - 1)
97
Added:
else s)
98
Added:
|> String.concat ""
99
Added:
in
100
Added:
HTML.pre (opt_class class_) [ txt ~raw:true "<code>%s</code>" raw_content ]
87
101
88
102
(* Lists *)
89
103
90
104
let items ?id ?class_ children = HTML.ul (opt_id id @ opt_class class_) children
91
105
106
Added:
let ordered_items ?id ?class_ children =
107
Added:
HTML.ol (opt_id id @ opt_class class_) children
108
Added:
92
109
let item ?class_ ?(current = false) children =
93
110
HTML.li (opt_class class_ @ flag_current current) children
94
111
95
112
let items_of ?id ?class_ render values =
96
113
items ?id ?class_ (List.map render values)
114
Added:
115
Added:
let code_inline ?class_ value = HTML.code (opt_class class_) [ text value ]
97
116
98
117
(* Badges *)
99
118
lib/views/ui.mli
@@ -113,6 +113,9 @@
113
113
val items : ?id:string -> ?class_:string -> node list -> node
114
114
(** An unordered list wrapping already-built {!item} nodes. *)
115
115
116
Added:
val ordered_items : ?id:string -> ?class_:string -> node list -> node
117
Added:
(** An ordered list wrapping already-built {!item} nodes. *)
118
Added:
116
119
val item : ?class_:string -> ?current:bool -> node list -> node
117
120
(** A list entry.
118
121
@@ -122,6 +125,9 @@
122
125
123
126
val items_of : ?id:string -> ?class_:string -> ('a -> node) -> 'a list -> node
124
127
(** A list built from values, saving callers a [List.map]. *)
128
Added:
129
Added:
val code_inline : ?class_:string -> string -> node
130
Added:
(** An inline code fragment. *)
125
131
126
132
(** {1 Badges} *)
127
133
test/test_readme.ml
@@ -66,6 +66,131 @@
66
66
"ordinary file is not a README" false
67
67
(Ogit.Readme.is_readme_filename "guide.md")
68
68
69
Added:
let test_org_inline_verbatim () =
70
Added:
let html =
71
Added:
render ~filename:"README.org" "* Notes\n\nUse =verbatim= for emphasis.\n"
72
Added:
in
73
Added:
Alcotest.(check bool)
74
Added:
"verbatim span" true
75
Added:
(contains html "<span class=\"readme-verbatim\">verbatim</span>");
76
Added:
Alcotest.(check bool) "surrounding text preserved" true (contains html "Use ")
77
Added:
78
Added:
let test_org_inline_code () =
79
Added:
let html =
80
Added:
render ~filename:"README.org" "* Notes\n\nCall ~List.map~ here.\n"
81
Added:
in
82
Added:
Alcotest.(check bool)
83
Added:
"code element" true
84
Added:
(contains html "<code class=\"readme-code\">List.map</code>")
85
Added:
86
Added:
let test_org_link_with_description () =
87
Added:
let html =
88
Added:
render ~filename:"README.org"
89
Added:
"* Links\n\nSee [[https://example.com][Example Site]] for details.\n"
90
Added:
in
91
Added:
Alcotest.(check bool)
92
Added:
"link href" true
93
Added:
(contains html "href=\"https://example.com\"");
94
Added:
Alcotest.(check bool) "link text" true (contains html ">Example Site</a>")
95
Added:
96
Added:
let test_org_link_bare () =
97
Added:
let html =
98
Added:
render ~filename:"README.org"
99
Added:
"* Links\n\nVisit [[https://example.com]] today.\n"
100
Added:
in
101
Added:
Alcotest.(check bool)
102
Added:
"bare link href" true
103
Added:
(contains html "href=\"https://example.com\"");
104
Added:
Alcotest.(check bool)
105
Added:
"bare link text equals URL" true
106
Added:
(contains html ">https://example.com</a>")
107
Added:
108
Added:
let test_org_unordered_list () =
109
Added:
let html =
110
Added:
render ~filename:"README.org"
111
Added:
"* Tasks\n\n- First item\n- Second item\n- Third item\n"
112
Added:
in
113
Added:
Alcotest.(check bool)
114
Added:
"unordered list" true
115
Added:
(contains html "<ul class=\"readme-list\">");
116
Added:
Alcotest.(check bool) "list item content" true (contains html "Second item")
117
Added:
118
Added:
let test_org_ordered_list () =
119
Added:
let html =
120
Added:
render ~filename:"README.org" "* Steps\n\n1. Download\n2. Install\n3. Run\n"
121
Added:
in
122
Added:
Alcotest.(check bool)
123
Added:
"ordered list" true
124
Added:
(contains html "<ol class=\"readme-list readme-ordered-list\">");
125
Added:
Alcotest.(check bool) "ordered item content" true (contains html "Install")
126
Added:
127
Added:
let test_org_definition_list () =
128
Added:
let html =
129
Added:
render ~filename:"README.org"
130
Added:
"* Glossary\n\n\
131
Added:
- Term One :: The first definition\n\
132
Added:
- Term Two :: The second definition\n"
133
Added:
in
134
Added:
Alcotest.(check bool)
135
Added:
"definition list" true
136
Added:
(contains html "<dl class=\"readme-definition-list\">");
137
Added:
Alcotest.(check bool)
138
Added:
"definition term" true
139
Added:
(contains html "<dt>Term One</dt>");
140
Added:
Alcotest.(check bool)
141
Added:
"definition description" true
142
Added:
(contains html "The first definition")
143
Added:
144
Added:
let test_org_inline_in_list () =
145
Added:
let html =
146
Added:
render ~filename:"README.org"
147
Added:
"* Items\n\n- Use ~code~ in lists\n- And =verbatim= too\n"
148
Added:
in
149
Added:
Alcotest.(check bool)
150
Added:
"code in list item" true
151
Added:
(contains html "<code class=\"readme-code\">code</code>");
152
Added:
Alcotest.(check bool)
153
Added:
"verbatim in list item" true
154
Added:
(contains html "<span class=\"readme-verbatim\">verbatim</span>")
155
Added:
156
Added:
let test_org_multiline_unordered () =
157
Added:
let html =
158
Added:
render ~filename:"README.org"
159
Added:
"* Notes\n\n- First item\n continues here\n- Second item\n"
160
Added:
in
161
Added:
Alcotest.(check bool)
162
Added:
"continuation joined to first item" true
163
Added:
(contains html "First item continues here");
164
Added:
Alcotest.(check bool)
165
Added:
"second item separate" true
166
Added:
(contains html "Second item")
167
Added:
168
Added:
let test_org_multiline_ordered () =
169
Added:
let html =
170
Added:
render ~filename:"README.org"
171
Added:
"* Steps\n\n1. Download the\n archive first\n2. Install it\n"
172
Added:
in
173
Added:
Alcotest.(check bool)
174
Added:
"ordered continuation joined" true
175
Added:
(contains html "Download the archive first");
176
Added:
Alcotest.(check bool)
177
Added:
"second ordered item separate" true
178
Added:
(contains html "Install it")
179
Added:
180
Added:
let test_org_multiline_definition () =
181
Added:
let html =
182
Added:
render ~filename:"README.org"
183
Added:
"* Glossary\n\n\
184
Added:
- Term :: First line\n\
185
Added:
\ of the definition\n\
186
Added:
- Other :: Short\n"
187
Added:
in
188
Added:
Alcotest.(check bool)
189
Added:
"definition continuation joined" true
190
Added:
(contains html "First line of the definition");
191
Added:
Alcotest.(check bool)
192
Added:
"second definition separate" true (contains html "Short")
193
Added:
69
194
let suite =
70
195
( "readme",
71
196
[
@@ -73,4 +198,20 @@
73
198
Alcotest.test_case "Org document" `Quick test_org_document;
74
199
Alcotest.test_case "README filename detection" `Quick
75
200
test_readme_filename_detection;
201
Added:
Alcotest.test_case "Org verbatim inline" `Quick test_org_inline_verbatim;
202
Added:
Alcotest.test_case "Org code inline" `Quick test_org_inline_code;
203
Added:
Alcotest.test_case "Org link with description" `Quick
204
Added:
test_org_link_with_description;
205
Added:
Alcotest.test_case "Org bare link" `Quick test_org_link_bare;
206
Added:
Alcotest.test_case "Org unordered list" `Quick test_org_unordered_list;
207
Added:
Alcotest.test_case "Org ordered list" `Quick test_org_ordered_list;
208
Added:
Alcotest.test_case "Org definition list" `Quick test_org_definition_list;
209
Added:
Alcotest.test_case "Org inline in list items" `Quick
210
Added:
test_org_inline_in_list;
211
Added:
Alcotest.test_case "Org multi-line unordered items" `Quick
212
Added:
test_org_multiline_unordered;
213
Added:
Alcotest.test_case "Org multi-line ordered items" `Quick
214
Added:
test_org_multiline_ordered;
215
Added:
Alcotest.test_case "Org multi-line definition items" `Quick
216
Added:
test_org_multiline_definition;
76
217
] )