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.

Commit
ce7b7de8f002166a8dde20fd18800528be8ac289
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/readme.ml
index dd6c384e..088e66ad 100644..100644
@@ -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
index 85553f11..97c56f27 100644..100644
@@ -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
index bafd1e9f..743fd0f3 100644..100644
@@ -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
index 756aedad..ebd27960 100644..100644
@@ -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
index e6020a32..eabd855f 100644..100644
@@ -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 ] )