(** A flat INI front end for overlay files. This is a CONVENIENCE FORMAT, not a second data model. It parses to exactly the {!Overlay.t} the S-expression form parses to, and everything downstream -- merge, diagnostics, validation -- is the same code on the same values. There is deliberately no second semantics to keep in step. It is also deliberately LESS EXPRESSIVE than the sexp form. It covers [Add], [Suppress] and single-field [Edit], which is what a diocesan or parish calendar needs; [Replace], multi-field edits and citation edits are not expressible and the parser says so by name rather than failing obscurely. Anything it cannot say is a reason to write sexp, not a reason to grow this. {1 Format} Section names are slugs. A [\[overlay\]] section carries the file's id. {v [overlay] id = my-parish [our-patron] date = 07-11 rank = class-3 colour = white name.en = St Example, Patron [some-universal-slug] suppress = yes v} Dates take three forms, matching {!Date_spec}: [MM-DD], [easter+N] or [easter-N], and [mon/day/nth] such as [oct/sun/1] or [oct/sun/-1]. *) (** [parse ~rank_of_string text] is the overlay [text] denotes. [rank_of_string] is supplied by the rite, exactly as [Overlay.load] takes [rank_of_sexp]: the kernel does not know one rite's rank vocabulary from another's. Errors carry the section name and the offending value, never a source-file path -- this format exists for people who are not reading the source. *) val parse : rank_of_string:(string -> 'r option) -> string -> ('r Overlay.t, string) result (** [to_sexp_string t] renders [t] as the S-expression form, with a header noting that it was generated. *) val to_sexp_string : ('r -> Sexplib0.Sexp.t) -> 'r Overlay.t -> string (** [convert ~rank_of_string ~rank_to_sexp ~rank_of_sexp text] parses [text], renders it, and PROVES the rendering before returning it: the emitted text is parsed back with the very function the engine uses to load an overlay, and the result must equal what the INI denoted. That check is the point of this module. A transpiler that emits syntactically valid but semantically wrong output is the failure mode a convenience format invites, and it is one [colitur check] could not catch, since the emitted file would parse cleanly and simply mean something else. Verifying the round trip here makes that class of bug impossible to ship rather than merely unlikely. [Error] on a parse failure, and on a round-trip mismatch -- which is a bug in this module, and says so. *) val convert : rank_of_string:(string -> 'r option) -> rank_to_sexp:('r -> Sexplib0.Sexp.t) -> rank_of_sexp:(Sexplib0.Sexp.t -> 'r) -> string -> (string, string) result