Something went wrong. Try again.
Provider-neutral LLM requests, transcripts and responses, I/O-free
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455(*--------------------------------------------------------------------------- Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*)
(** Named sections of a Markdown document, and their replacement.
A section is an ATX heading ({{:https://spec.commonmark.org/0.31.2/#atx-headings} CommonMark 0.31.2, 4.2}) and the lines after it up to the next heading of the same or a higher level, or the end of the document. Lines inside a fenced code block ({{:https://spec.commonmark.org/0.31.2/#fenced-code-blocks} 4.5}) are never headings. The heading line stays; only the body is replaced. *)
type document(** The type for a document cut into its sections. *)
val of_string : string -> document(** [of_string s] is [s] cut into lines and headings. *)
type t = { heading : string; (** The heading line, without its line ending. *) level : int; (** 1 to 6. *) body : string; (** The lines after the heading, line endings included. *)}(** A section found in a document. *)
val pp : t Fmt.t(** [pp] formats a section as its heading line, level and body length. *)
val get : document -> string -> (t, string) result(** [get d name] is the one section whose heading text is [name], compared after trimming and without its [#] marks, or whose heading line is [name]. It is an error naming [name] when no heading or several match. *)
val fences : string -> string list(** [fences s] is every fenced code block of [s], opening and closing lines included, byte for byte, in order. An unclosed fence runs to the end. *)
val headings : string -> (int * string) list(** [headings s] is the level and line of every ATX heading of [s] outside a fenced code block, in order. *)
val body_of_reply : t -> string -> string(** [body_of_reply s reply] is the body a model's [reply] stands for: [reply] with a leading copy of [s]'s heading line removed, trimmed, and framed by the blank lines that framed [s.body]. *)
val replace : document -> (t * string) list -> (string, string) result(** [replace d edits] is the document with each section's body replaced by the paired body. It is refused, naming the section, when a new body does not hold the original body's fenced code blocks byte for byte and in order, or holds a heading at the section's level or higher; and refused when the text outside the named sections differs from the original after the splice. *)