(** Pre-scan phase for file-local declarations. Scans the entire input for syntax-affecting keywords like #+TODO, #+SEQ_TODO, and #+TYP_TODO before the main parse. These declarations may appear anywhere in the file and affect how headings are interpreted. *) let lstrip = String_util.lstrip let strip = String_util.strip (** Parse a TODO keyword line value into a todo_sequence. Format: "KEYWORD1 KEYWORD2 | DONE1 DONE2" If no | is present, all keywords are active and there are no done keywords. *) let parse_todo_value value = let value = strip value in if String.length value = 0 then None else let words = String.split_on_char ' ' value |> List.filter (fun s -> s <> "") in let rec split_at_bar acc = function | [] -> (List.rev acc, []) | "|" :: rest -> (List.rev acc, rest) | w :: rest -> split_at_bar (w :: acc) rest in let active, done_ = split_at_bar [] words in if active = [] && done_ = [] then None else Some { Config.active; done_ } (** Extract the value part after #+KEY: from a line. *) let extract_keyword_value line = (* Line format: #+KEY: VALUE *) let line = lstrip line in if String.length line < 2 || line.[0] <> '#' || line.[1] <> '+' then None else let rest = String.sub line 2 (String.length line - 2) in match String.index_opt rest ':' with | None -> None | Some colon_pos -> let key = String.uppercase_ascii (String.sub rest 0 colon_pos) in let value = if colon_pos + 1 < String.length rest then String.sub rest (colon_pos + 1) (String.length rest - colon_pos - 1) else "" in Some (key, strip value) (** Scan the input for TODO keyword declarations. Returns a [Config.t] with all discovered TODO sequences. *) let scan input = let lines = String.split_on_char '\n' input in let sequences = List.filter_map (fun line -> match extract_keyword_value line with | Some (key, value) when key = "TODO" || key = "SEQ_TODO" || key = "TYP_TODO" -> parse_todo_value value | _ -> None) lines in match sequences with | [] -> Config.default | seqs -> { Config.todo_sequences = seqs }