Source file quill_project.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
type notebook = { title : string; path : string }
type toc_item =
| Notebook of notebook * toc_item list
| Section of string
| Separator
type config = {
title : string option;
authors : string list;
description : string option;
output : string option;
edit_url : string option;
}
type t = { title : string; root : string; toc : toc_item list; config : config }
let default_config =
{
title = None;
authors = [];
description = None;
output = None;
edit_url = None;
}
let trim = String.trim
let leading_spaces s =
let len = String.length s in
let rec loop i = if i < len && s.[i] = ' ' then loop (i + 1) else i in
loop 0
let s =
let s = trim s in
String.length s = 0 || s.[0] = '#'
let is_separator s = trim s = "---"
let is_section s =
let s = trim s in
let len = String.length s in
len >= 2 && s.[0] = '[' && s.[len - 1] = ']'
let parse_section s =
let s = trim s in
String.sub s 1 (String.length s - 2)
let parse_kv s =
match String.index_opt s '=' with
| None -> None
| Some i ->
let key = trim (String.sub s 0 i) in
let value = trim (String.sub s (i + 1) (String.length s - i - 1)) in
Some (key, value)
let is_toc_entry s = parse_kv s <> None || is_section s || is_separator s
let parse_metadata (cfg : config) key value =
match key with
| "title" -> { cfg with title = Some value }
| "authors" ->
let authors = List.map trim (String.split_on_char ',' value) in
{ cfg with authors }
| "description" -> { cfg with description = Some value }
| "output" -> { cfg with output = Some value }
| "edit-url" -> { cfg with edit_url = Some value }
| _ -> cfg
type toc_entry =
| E_notebook of string * string * int
| E_section of string
| E_separator
let collect_toc_entries lines =
let entries = ref [] in
List.iter
(fun line ->
if is_comment_or_blank line then ()
else if is_separator line then entries := E_separator :: !entries
else if is_section line then
entries := E_section (parse_section line) :: !entries
else
let indent = leading_spaces line in
match parse_kv line with
| Some (title, path) ->
entries := E_notebook (title, path, indent) :: !entries
| None -> ())
lines;
List.rev !entries
let rec build_toc entries =
match entries with
| [] -> ([], [])
| entry :: rest -> (
match entry with
| E_separator ->
let siblings, remaining = build_toc rest in
(Separator :: siblings, remaining)
| E_section title ->
let siblings, remaining = build_toc rest in
(Section title :: siblings, remaining)
| E_notebook (title, path, indent) ->
let children, after_children = collect_children (indent + 1) rest in
let nb = { title; path } in
let siblings, remaining = build_toc after_children in
(Notebook (nb, children) :: siblings, remaining))
and collect_children min_indent entries =
match entries with
| E_notebook (_, _, indent) :: _ when indent >= min_indent ->
let item, rest = take_one_child min_indent entries in
let more_children, remaining = collect_children min_indent rest in
(item :: more_children, remaining)
| _ -> ([], entries)
and take_one_child min_indent entries =
match entries with
| E_notebook (title, path, indent) :: rest when indent >= min_indent ->
let children, remaining = collect_children (indent + 1) rest in
let nb = { title; path } in
(Notebook (nb, children), remaining)
| _ -> failwith "take_one_child: expected notebook entry"
let parse_config source =
let lines = String.split_on_char '\n' source in
let in_metadata = ref true in
let meta_lines = ref [] in
let toc_lines = ref [] in
List.iter
(fun line ->
if !in_metadata then
if is_comment_or_blank line then ()
else if
is_toc_entry (String.trim line) && not (is_comment_or_blank line)
then (
match parse_kv line with
| Some (_, _) when (not (is_section line)) && leading_spaces line = 0
->
let trimmed = trim line in
let value =
match String.index_opt trimmed '=' with
| Some i ->
trim
(String.sub trimmed (i + 1)
(String.length trimmed - i - 1))
| None -> ""
in
if
String.contains value '/'
|| String.contains value '.'
&& String.length value > 0
&& value <> ""
then (
in_metadata := false;
toc_lines := line :: !toc_lines)
else if value = "" then (
let key =
match String.index_opt trimmed '=' with
| Some i -> trim (String.sub trimmed 0 i)
| None -> trimmed
in
match key with
| "title" | "authors" | "description" | "output" | "edit-url" ->
meta_lines := line :: !meta_lines
| _ ->
in_metadata := false;
toc_lines := line :: !toc_lines)
else meta_lines := line :: !meta_lines
| _ ->
in_metadata := false;
toc_lines := line :: !toc_lines)
else meta_lines := line :: !meta_lines
else toc_lines := line :: !toc_lines)
lines;
let config =
List.fold_left
(fun cfg line ->
match parse_kv line with
| Some (key, value) -> parse_metadata cfg key value
| None -> cfg)
default_config (List.rev !meta_lines)
in
let toc_entries = collect_toc_entries (List.rev !toc_lines) in
let toc, _ = build_toc toc_entries in
Ok (config, toc)
let title_of_filename path =
let base = Filename.basename path in
let name = Filename.remove_extension base in
let len = String.length name in
let start = ref 0 in
while
!start < len
&&
let c = name.[!start] in
(c >= '0' && c <= '9') || c = '-' || c = '_'
do
incr start
done;
let name =
if !start >= len then name else String.sub name !start (len - !start)
in
let buf = Buffer.create (String.length name) in
String.iter
(fun c ->
match c with
| '-' | '_' -> Buffer.add_char buf ' '
| c -> Buffer.add_char buf c)
name;
let result = Buffer.contents buf in
if String.length result > 0 then
let first = Char.uppercase_ascii result.[0] in
let rest = String.sub result 1 (String.length result - 1) in
String.make 1 first ^ rest
else result
let rec all_notebooks toc =
List.concat_map
(fun item ->
match item with
| Notebook (nb, children) -> nb :: all_notebooks children
| Section _ | Separator -> [])
toc
let is_placeholder nb = nb.path = ""
let notebooks project =
List.filter (fun nb -> not (is_placeholder nb)) (all_notebooks project.toc)
let notebooks_array project = Array.of_list (notebooks project)
let find_notebook_index project nb =
let nbs = notebooks_array project in
let rec loop i =
if i >= Array.length nbs then None
else if nbs.(i).path = nb.path then Some i
else loop (i + 1)
in
loop 0
let prev_notebook project nb =
match find_notebook_index project nb with
| Some i when i > 0 -> Some (notebooks_array project).(i - 1)
| _ -> None
let next_notebook project nb =
let nbs = notebooks_array project in
match find_notebook_index project nb with
| Some i when i < Array.length nbs - 1 -> Some nbs.(i + 1)
| _ -> None
let number toc nb =
let rec search counter = function
| [] -> None
| Notebook (n, children) :: rest ->
incr counter;
if n.path = nb.path then Some [ !counter ]
else begin
match search (ref 0) children with
| Some sub -> Some (!counter :: sub)
| None -> search counter rest
end
| Section _ :: rest ->
counter := 0;
search counter rest
| Separator :: rest -> search counter rest
in
match search (ref 0) toc with Some ns -> ns | None -> []
let number_string = function
| [] -> ""
| ns -> String.concat "." (List.map string_of_int ns)