pub fn validate_section_content<'a>(
sections: impl Iterator<Item = (&'a str, &'a str)>,
catch_all: Option<CatchAllContext<'_>>,
) -> Result<(), ValidationError>Expand description
Refuse section content that would round-trip through the compose
pipeline as a section delimiter. The compose-then-reparse loop’s
parser anchors on (?m)^## (.+)$ over the masked body, so a
section body that shows a ^## line to that scan gets split at
that heading on the next read — content after the heading lands
under a different section key (or a fabricated one). Deeper
headings (### and below) are safe — the parser only matches
level 2.
The guard is applied to the content as the reparse will see it, which is what makes it exact rather than approximate:
- the content is trimmed first, because the splitter stores the
trimmed body — an indented block opening a section loses its
indent on write-back, so
## Not A Headingbecomes a real column-0 delimiter on the next parse. Checking the still-indented provided content missed that fork entirely; - code blocks are masked first, by the same CommonMark referee the
splitter uses (
crate::markdown) — a##inside a fenced or indented code block never splits anything, so refusing it was the write path disagreeing with the read path about what a code block is.
catch_all names the type’s catch-all section and its declared headings,
when the caller knows them (consistency-sweep 04/01, criterion 6). Inside
the CATCH-ALL body only, a ## line whose heading the type does not
declare is accepted, because the reparse absorbs it straight back into the
catch-all: the content does not land under a different key, which is the
whole basis of this guard. That case is not hypothetical — it is what the
engine itself emits, since the catch-all builder re-emits absorbed content
under its original heading line, and an agent that read an entity and wrote
that section back in replace mode was refused its own value.
This does NOT weaken the guard. A DECLARED heading inside the catch-all
still refuses, because that one really does fork: the reparse would move
the content to the declared key. Every other section is unchanged, and a
caller who passes None gets exactly the old behaviour.