Skip to main content

Module text

Module text 

Source
Expand description

“Inline content → plain text”, once.

Two walkers need this operation and they cannot be merged, because they read different inputs at different times:

walkerinputwhenconsumer
[events_to_text]&[pulldown_cmark::Event]during parse, before the AST existsthe <hN id> slug
crate::ast::plain_text::inlines_to_plain_text&[Inline]after parsesuper::extract’s autocomplete label

What they must NOT do is disagree about what a piece of content looks like as text. They did: collect_heading_text in ast/parser.rs and inlines_to_text in extract_headings.rs were independent match arms over independent enums, so a heading’s slug and its autocomplete label could drift apart — which is exactly what the July 2026 math cluster found ($f*g$ came out of one and $fg$ out of the other).

So: one policy, two adapters. [crate::ast::plain_text::TextAtom] is the vocabulary the policy speaks; push_atom IS the policy and is the only place that decides what an atom’s text is; each walker’s job is reduced to classifying its own node type into an atom. Changing what math (or a line break, or code) looks like in plain text is a one-line edit in one function, and both surfaces move together by construction.

The &[Inline] half of the policy (and its adapter, crate::ast::plain_text::inlines_to_plain_text) moved to ast/plain_text.rs (ADR-036) once a third non-heading consumer (build::page::meta::extract_description) appeared — exactly the trigger that module’s promotion doc comment named in advance. This module keeps only [events_to_text], the mid-parse event-stream half that has no AST to walk yet.

§The one difference that remains, and why it is not a bug to fix here

The two walkers see different vocabularies, not different policies: the event stream carries SoftBreak/HardBreak events that the slug walker has always ignored, while the AST folds a SoftBreak into Inline::Text("\n") and a HardBreak into [Inline::LineBreak]. A multi-line setext heading therefore slugs as foobar but labels as foo bar. That predates this module and is a behavioral question — changing it moves live anchors. It is pinned by setext_soft_break_divergence_is_pinned_not_fixed below so the next person meets it as a decision rather than as a surprise.