module OI = Colitur_kernel.Overlay_ini type t = { lang : string option; overlays : string list; template : string option; format : string option; flavour : string option; sigla_style : string option; sigla_book : string option; sigla_tradition : string option; unknown_keys : string list; unknown_sections : string list; } let empty = { lang = None; overlays = []; template = None; format = None; flavour = None; sigla_style = None; sigla_book = None; sigla_tradition = None; unknown_keys = []; unknown_sections = [] } let lang t = t.lang let overlays t = t.overlays let template t = t.template let format t = t.format let flavour t = t.flavour let sigla_style t = t.sigla_style let sigla_book t = t.sigla_book let sigla_tradition t = t.sigla_tradition let unknown_keys t = t.unknown_keys let unknown_sections t = t.unknown_sections let of_string text = match OI.parse_sections text with | Error e -> Error e | Ok sections -> (* Any section that is not [defaults] is unrecognised -- including a plain typo such as [deafults] -- and must be REPORTED, never silently dropped: that is precisely the failure this feature exists to surface. *) let unknown_sections = List.filter_map (fun (s : OI.section) -> if s.OI.name = "defaults" then None else Some s.OI.name) sections in (* Merge EVERY section named [defaults], not just the first: [lang.ml]'s [of_string] was fixed this morning to fold over all matching sections rather than take [List.find_opt]'s first match, because a hand-edited file WILL grow duplicate headers as a user appends to it over time. Both modules parse the same reader ([Overlay_ini.parse_sections]) over the same file format, so they must not disagree about what a duplicate [section] header means -- taking only the first [defaults] block here silently discarded every later one, with nothing pointing back at the parser. Folding a single accumulator across every matching section, in file order, keeps this consistent with the within-section behaviour below (last [k]-match wins): a scalar key repeated across two blocks resolves to the LATER value, and [overlay] keeps accumulating across every block, not only its first. *) let defaults_sections = List.filter (fun (s : OI.section) -> s.OI.name = "defaults") sections in let acc = (* Cons then reverse once at the very end, not `@ [v]` per line: the latter is O(n^2) over the field count, a real hang on a machine-generated file with many overlay lines. Reversing only after every section has been folded (not once per section) is what keeps [overlay] and [unknown_keys] in file order across block boundaries, not merely within one block. *) List.fold_left (fun acc (s : OI.section) -> List.fold_left (fun acc (k, v) -> match k with | "lang" -> { acc with lang = Some v } | "template" -> { acc with template = Some v } | "format" -> { acc with format = Some v } | "flavour" -> { acc with flavour = Some v } | "sigla_style" -> { acc with sigla_style = Some v } | "sigla_book" -> { acc with sigla_book = Some v } | "sigla_tradition" -> { acc with sigla_tradition = Some v } (* accumulates: a user has more than one overlay *) | "overlay" -> { acc with overlays = v :: acc.overlays } | other -> { acc with unknown_keys = other :: acc.unknown_keys }) acc s.OI.fields) empty defaults_sections in let acc = { acc with overlays = List.rev acc.overlays; unknown_keys = List.rev acc.unknown_keys } in Ok { acc with unknown_sections } let resolve ~flag ~config ~default = match flag with | Some v -> (v, "flag") | None -> ( match config with Some v -> (v, "config") | None -> (default, "default"))