[OCaml] Mobile-friendly clone of cgit.
1
(** Org mode documentation format.
2
3
Parses Org headings, #+BEGIN_SRC blocks, metadata directives, unordered,
4
ordered, and definition lists, and paragraphs. Inline markup handles
5
=verbatim=, ~code~, and [[link][desc]] syntax. *)
6
7
(* {1 Inline markup} *)
8
9
let org_inline text =
10
let len = String.length text in
11
let buf = Buffer.create 64 in
12
let nodes = ref [] in
13
let flush () =
14
if Buffer.length buf > 0 then (
15
nodes := Ui.text (Buffer.contents buf) :: !nodes;
16
Buffer.clear buf)
17
in
18
let rec loop i =
19
if i >= len then flush ()
20
else
21
match text.[i] with
22
| '[' when i + 1 < len && text.[i + 1] = '[' ->
23
flush ();
24
parse_link (i + 2)
25
| ('=' | '~') as marker -> (
26
let close = find_close marker (i + 1) in
27
match close with
28
| Some end_pos ->
29
flush ();
30
let content = String.sub text (i + 1) (end_pos - i - 1) in
31
let node =
32
match marker with
33
| '~' -> Ui.code_inline ~class_:"readme-code" content
34
| _ -> Ui.inline ~class_:"readme-verbatim" [ Ui.text content ]
35
in
36
nodes := node :: !nodes;
37
loop (end_pos + 1)
38
| None ->
39
Buffer.add_char buf text.[i];
40
loop (i + 1))
41
| c ->
42
Buffer.add_char buf c;
43
loop (i + 1)
44
and find_close marker start =
45
let rec search j =
46
if j >= len then None
47
else if text.[j] = marker then Some j
48
else if text.[j] = '\n' then None
49
else search (j + 1)
50
in
51
if start >= len then None else search start
52
and parse_link start =
53
let rec find_end j _depth =
54
if j >= len then None
55
else if j + 1 < len && text.[j] = ']' && text.[j + 1] = ']' then Some j
56
else find_end (j + 1) 0
57
in
58
match find_end start 0 with
59
| None ->
60
Buffer.add_string buf "[[";
61
loop start
62
| Some close_pos ->
63
let inner = String.sub text start (close_pos - start) in
64
let href, desc =
65
match String.index_opt inner ']' with
66
| Some bracket_pos
67
when bracket_pos + 1 < String.length inner
68
&& inner.[bracket_pos + 1] = '[' ->
69
let href = String.sub inner 0 bracket_pos in
70
let desc =
71
String.sub inner (bracket_pos + 2)
72
(String.length inner - bracket_pos - 2)
73
in
74
(href, desc)
75
| _ -> (inner, inner)
76
in
77
let node = Ui.link ~class_:"readme-link" ~href [ Ui.text desc ] in
78
nodes := node :: !nodes;
79
loop (close_pos + 2)
80
in
81
loop 0;
82
List.rev !nodes
83
84
(* {1 Line classifiers} *)
85
86
let heading line =
87
let length = String.length line in
88
let rec count_stars index =
89
if index < length && line.[index] = '*' then count_stars (index + 1)
90
else index
91
in
92
let level = count_stars 0 in
93
if level = 0 || level >= length || line.[level] <> ' ' then None
94
else
95
Some
96
( min 6 level,
97
String.sub line (level + 1) (length - level - 1) |> String.trim )
98
99
let unordered_item = Format.unordered_item
100
let ordered_item = Format.ordered_item
101
102
let definition_item line =
103
let trimmed = String.trim line in
104
let tlen = String.length trimmed in
105
if tlen < 2 || trimmed.[0] <> '-' || trimmed.[1] <> ' ' then None
106
else
107
let rest = String.sub trimmed 2 (tlen - 2) in
108
let rec find_sep i =
109
if i + 3 >= String.length rest then None
110
else if
111
rest.[i] = ' '
112
&& rest.[i + 1] = ':'
113
&& rest.[i + 2] = ':'
114
&& rest.[i + 3] = ' '
115
then
116
let term = String.sub rest 0 i |> String.trim in
117
let desc =
118
String.sub rest (i + 4) (String.length rest - i - 4) |> String.trim
119
in
120
Some (term, desc)
121
else find_sep (i + 1)
122
in
123
find_sep 0
124
125
let src_begin line =
126
let prefix = "#+begin_src" in
127
let lower = String.lowercase_ascii (String.trim line) in
128
if not (String.starts_with ~prefix lower) then None
129
else
130
let language =
131
String.sub lower (String.length prefix)
132
(String.length lower - String.length prefix)
133
|> String.trim |> Format.first_word
134
in
135
Some language
136
137
let is_src_end line =
138
String.trim line |> String.lowercase_ascii
139
|> String.starts_with ~prefix:"#+end_src"
140
141
let is_example_begin line =
142
String.trim line |> String.lowercase_ascii
143
|> String.starts_with ~prefix:"#+begin_example"
144
145
let is_example_end line =
146
String.trim line |> String.lowercase_ascii
147
|> String.starts_with ~prefix:"#+end_example"
148
149
(* {1 Block parsing} *)
150
151
let is_boundary_common line =
152
Format.is_boundary_common line || Option.is_some (definition_item line)
153
154
let parse_blocks lines =
155
let is_boundary line =
156
is_boundary_common line
157
|| Option.is_some (heading line)
158
|| Option.is_some (src_begin line)
159
|| is_example_begin line
160
in
161
let rec take_paragraph collected = function
162
| line :: _ as rest when is_boundary line -> (List.rev collected, rest)
163
| line :: rest -> take_paragraph (String.trim line :: collected) rest
164
| [] -> (List.rev collected, [])
165
in
166
let rec take_unordered collected = function
167
| line :: rest -> (
168
match unordered_item line with
169
| Some first_line ->
170
let continuations, rest = Format.take_continuations rest in
171
let item = String.concat " " (first_line :: continuations) in
172
take_unordered (item :: collected) rest
173
| None -> (List.rev collected, line :: rest))
174
| [] -> (List.rev collected, [])
175
in
176
let rec take_ordered collected = function
177
| line :: rest -> (
178
match ordered_item line with
179
| Some first_line ->
180
let continuations, rest = Format.take_continuations rest in
181
let item = String.concat " " (first_line :: continuations) in
182
take_ordered (item :: collected) rest
183
| None -> (List.rev collected, line :: rest))
184
| [] -> (List.rev collected, [])
185
in
186
let rec take_definitions collected = function
187
| line :: rest -> (
188
match definition_item line with
189
| Some (term, first_desc) ->
190
let continuations, rest = Format.take_continuations rest in
191
let desc = String.concat " " (first_desc :: continuations) in
192
take_definitions ((term, desc) :: collected) rest
193
| None -> (List.rev collected, line :: rest))
194
| [] -> (List.rev collected, [])
195
in
196
let open Format in
197
let rec loop blocks = function
198
| [] -> List.rev blocks
199
| line :: rest when String.trim line = "" -> loop blocks rest
200
| line :: rest -> (
201
match heading line with
202
| Some (level, text) -> loop (Heading (level, text) :: blocks) rest
203
| None -> (
204
match src_begin line with
205
| Some language ->
206
let lines, rest = Format.take_until is_src_end [] rest in
207
loop
208
(Code_block (language, String.concat "\n" lines) :: blocks)
209
rest
210
| None when is_example_begin line ->
211
let lines, rest = Format.take_until is_example_end [] rest in
212
loop
213
(Code_block (None, String.concat "\n" lines) :: blocks)
214
rest
215
| None -> (
216
match definition_item line with
217
| Some _ ->
218
let items, rest = take_definitions [] (line :: rest) in
219
loop (Definition_list items :: blocks) rest
220
| None -> (
221
match unordered_item line with
222
| Some _ ->
223
let items, rest = take_unordered [] (line :: rest) in
224
loop (Unordered_list items :: blocks) rest
225
| None -> (
226
match ordered_item line with
227
| Some _ ->
228
let items, rest = take_ordered [] (line :: rest) in
229
loop (Ordered_list items :: blocks) rest
230
| None ->
231
let paragraph, rest =
232
take_paragraph [] (line :: rest)
233
in
234
loop
235
(Paragraph (String.concat " " paragraph) :: blocks)
236
rest)))))
237
in
238
loop [] lines
239
240
(* {1 Org metadata} *)
241
242
let metadata_line line =
243
let prefix = "#+" in
244
let line = String.trim line in
245
if not (String.starts_with ~prefix line) then None
246
else
247
match String.index_opt line ':' with
248
| None -> None
249
| Some colon ->
250
let key = String.sub line 2 (colon - 2) |> String.lowercase_ascii in
251
let value =
252
String.sub line (colon + 1) (String.length line - colon - 1)
253
|> String.trim
254
in
255
if List.mem key [ "title"; "author"; "date"; "email"; "language" ] then
256
Some (key, value)
257
else None
258
259
let split_metadata lines =
260
List.fold_left
261
(fun (metadata, body) line ->
262
match metadata_line line with
263
| None -> (metadata, line :: body)
264
| Some entry -> (entry :: metadata, body))
265
([], []) lines
266
|> fun (metadata, body) -> (List.rev metadata, List.rev body)
267
268
(* {1 Format definition} *)
269
270
let format : Format.format =
271
{
272
name = "org";
273
css_class = "readme-document readme-org";
274
parse =
275
(fun content ->
276
let lines = String.split_on_char '\n' content in
277
let metadata, lines = split_metadata lines in
278
let title =
279
List.find_opt (fun (key, _) -> key = "title") metadata
280
|> Option.map snd
281
in
282
let metadata_entries =
283
List.filter (fun (key, _) -> key <> "title") metadata
284
in
285
{
286
Format.title;
287
metadata = metadata_entries;
288
blocks = parse_blocks lines;
289
});
290
inline = org_inline;
291
}
292