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
type slide = { title : string; points : (int * string) list }
let indent line =
let n = String.length line in
let rec go i cols = if i < n && line.[i] = ' ' then go (i + 1) (cols + 1) else if i < n && line.[i] = '\t' then go (i + 1) (cols + 2) else (i, cols) in
let i, cols = go 0 0 in
(cols, String.sub line i (n - i))
let parse text =
let close cur acc = match cur with Some s -> { s with points = List.rev s.points } :: acc | None -> acc in
let cur, acc =
List.fold_left
(fun (cur, acc) line ->
let cols, rest = indent line in
if String.trim rest = "" then (cur, acc)
else if cols = 0 then (Some { title = rest; points = [] }, close cur acc)
else
let level = max 1 (cols / 2) in
let s = Option.value cur ~default:{ title = ""; points = [] } in
(Some { s with points = (level, String.trim rest) :: s.points }, acc))
(None, []) (String.split_on_char '\n' text)
in
List.rev (close cur acc)
let slide_at text offset =
let before = String.sub text 0 (max 0 (min offset (String.length text))) in
let lines = String.split_on_char '\n' before in
let titles = List.filter (fun l -> let cols, rest = indent l in cols = 0 && String.trim rest <> "") lines in
let starts_untitled =
match List.find_opt (fun l -> String.trim l <> "") (String.split_on_char '\n' text) with
| Some l -> fst (indent l) > 0
| None -> false
in
max 0 (List.length titles - 1 + if starts_untitled then 1 else 0)
let start_of text n =
let rec titles pos acc =
if pos > String.length text then List.rev acc
else
let e = match String.index_from_opt text pos '\n' with Some e -> e | None -> String.length text in
let line = String.sub text pos (e - pos) in
let cols, rest = indent line in
let acc = if cols = 0 && String.trim rest <> "" then pos :: acc else acc in
titles (e + 1) acc
in
match List.nth_opt (titles 0 []) n with Some pos -> pos | None -> String.length text
let lines_of text n =
let numbered = List.filter (fun (_, l) -> String.trim l <> "") (List.mapi (fun i l -> (i, l)) (String.split_on_char '\n' text)) in
let slides =
List.fold_left
(fun acc (i, l) ->
if fst (indent l) = 0 then (Some i, []) :: acc
else match acc with (t, ps) :: rest -> (t, i :: ps) :: rest | [] -> [ (None, [ i ]) ])
[] numbered
in
match List.nth_opt (List.rev slides) n with Some (t, ps) -> (t, List.rev ps) | None -> (None, [])
let line_span text k =
let rec start pos k = if k = 0 then pos else match String.index_from_opt text pos '\n' with Some e -> start (e + 1) (k - 1) | None -> String.length text in
let a = start 0 k in
let e = match String.index_from_opt text a '\n' with Some e -> e | None -> String.length text in
let rec content i = if i < e && (text.[i] = ' ' || text.[i] = '\t') then content (i + 1) else i in
(a, content a, e)
let to_text slides =
String.concat "\n"
(List.concat_map
(fun s -> s.title :: List.map (fun (level, p) -> String.make (2 * level) ' ' ^ p) s.points)
slides)