[OCaml] Org mode parser.
1
(** Pre-scan phase for file-local declarations.
2
3
Scans the entire input for syntax-affecting keywords like #+TODO,
4
#+SEQ_TODO, and #+TYP_TODO before the main parse. These declarations may
5
appear anywhere in the file and affect how headings are interpreted. *)
6
7
let lstrip = String_util.lstrip
8
let strip = String_util.strip
9
10
(** Parse a TODO keyword line value into a todo_sequence. Format: "KEYWORD1
11
KEYWORD2 | DONE1 DONE2" If no | is present, all keywords are active and
12
there are no done keywords. *)
13
let parse_todo_value value =
14
let value = strip value in
15
if String.length value = 0 then None
16
else
17
let words =
18
String.split_on_char ' ' value |> List.filter (fun s -> s <> "")
19
in
20
let rec split_at_bar acc = function
21
| [] -> (List.rev acc, [])
22
| "|" :: rest -> (List.rev acc, rest)
23
| w :: rest -> split_at_bar (w :: acc) rest
24
in
25
let active, done_ = split_at_bar [] words in
26
if active = [] && done_ = [] then None else Some { Config.active; done_ }
27
28
(** Extract the value part after #+KEY: from a line. *)
29
let extract_keyword_value line =
30
(* Line format: #+KEY: VALUE *)
31
let line = lstrip line in
32
if String.length line < 2 || line.[0] <> '#' || line.[1] <> '+' then None
33
else
34
let rest = String.sub line 2 (String.length line - 2) in
35
match String.index_opt rest ':' with
36
| None -> None
37
| Some colon_pos ->
38
let key = String.uppercase_ascii (String.sub rest 0 colon_pos) in
39
let value =
40
if colon_pos + 1 < String.length rest then
41
String.sub rest (colon_pos + 1) (String.length rest - colon_pos - 1)
42
else ""
43
in
44
Some (key, strip value)
45
46
(** Scan the input for TODO keyword declarations. Returns a [Config.t] with all
47
discovered TODO sequences. *)
48
let scan input =
49
let lines = String.split_on_char '\n' input in
50
let sequences =
51
List.filter_map
52
(fun line ->
53
match extract_keyword_value line with
54
| Some (key, value)
55
when key = "TODO" || key = "SEQ_TODO" || key = "TYP_TODO" ->
56
parse_todo_value value
57
| _ -> None)
58
lines
59
in
60
match sequences with
61
| [] -> Config.default
62
| seqs -> { Config.todo_sequences = seqs }
63